# Deploy Flask to a VPS with gunicorn and PostgreSQL Deploy a Flask app to your own VPS: ox installs with uv from requirements.txt, runs gunicorn app:app under systemd behind Caddy, and adds PostgreSQL. To deploy Flask to a VPS with ox, add the repo as a project and deploy it, with no Dockerfile and no ox.toml. ox reads `app.py` and `requirements.txt`, runs `uv venv --allow-existing && uv pip install -r requirements.txt`, then starts `.venv/bin/gunicorn app:app --bind 127.0.0.1:$PORT` under systemd behind Caddy with HTTPS, and sets up postgres 18 for it. Run `ox check` in your repo to see the same plan before the first deploy. **Plan verified, real-server test pending.** The plan below is what `ox check` prints for a minimal Flask repo in ox's tests. A deploy on a real server is not recorded yet. ox reads `app.py` and `requirements.txt`, builds the plan on the card, and starts `.venv/bin/gunicorn app:app --bind 127.0.0.1:$PORT` on your server. The cylinders are the services the repo asked for. ## What ox detects in a Flask repo This is the plan `ox check` prints for a minimal a Flask app with PostgreSQL, with no ox.toml. Each row says where its value came from: a file in the repo, or ox's default. | Field | Value | Source | | --- | --- | --- | | `app.start` | `.venv/bin/gunicorn app:app --bind 127.0.0.1:$PORT` | `detected:app.py` | | `build.install` | `uv venv --allow-existing && uv pip install -r requirements.txt` | `detected:requirements.txt` | | `tools.uv` | `0.11` | `default` | | `services.postgres` | `postgres 18 (shared)` | `detected:requirements.txt` | ## The ox.toml for Flask None is needed: the plan above comes from the repo alone. Write one to serve your own domain or to change what ox detected. This one passes `ox check` against the same repo: `[app]` keeps the detected start command and build. Once a repo has an ox.toml, ox adds no service itself, so list each one under `[services]`. ox.toml, optional: ```toml domains = ["www.example.com"] [app] [services] postgres = {} ``` ## Check and deploy Flask Add the repo as a project, then check and ship it from a terminal: Check, deploy and read the logs: ```sh ox check # in the repo: the plan above, offline ox new --server # add the repo as a project ox deploy --wait # stream the deploy, exit with its result ox logs --follow # the app's own logs ``` ## Variables to set for Flask None before the first deploy: `ox check` asks for no variable. Add your own on the dashboard's Variables tab or with `ox vars set KEY`. ox provides these to the app itself: `PORT`, `HOST`, `OX_ENV`, `OX_PROJECT`, `OX_RELEASE`, `OX_DATA_DIR`, `PUBLIC_URL`, `PUBLIC_HOST` and `DATABASE_URL`. ## If the Flask deploy fails - `Flask found in app.py, but gunicorn is not a dependency; add gunicorn or set [app] start`: Add `gunicorn` to `requirements.txt` so ox can run the app. [More](https://deploywithox.com/docs/config#app). - `` Flask found in app.py, but no top-level ` = Flask(...)`, so ox cannot tell which object to serve; set [app] start ``: Create the app at the top level of the file, or set `[app] start` to your gunicorn command. [More](https://deploywithox.com/docs/config#app). - `TCP connect to 127.0.0.1:8001 did not answer within 120s: dial tcp 127.0.0.1:8001: connect: connection refused`: The app started but never answered on the port ox gave it. Listen on `127.0.0.1` at `$PORT`; the lines above the message are the app's own output. [More](https://deploywithox.com/docs/troubleshooting#health-check-failed). Your visitors never see a failed deploy: the previous release keeps serving until the new one passes its health check. ## Next steps - [The Django guide](https://deploywithox.com/docs/guides/django): gunicorn, migrations and static files. - [The FastAPI guide](https://deploywithox.com/docs/guides/fastapi): uvicorn, uv and Alembic. - [The `[app]` keys](https://deploywithox.com/docs/config#app): start, health check, memory and the rest. - [Service dashboards](https://deploywithox.com/docs/operate/services): your database's status, size, backups and data. - [Add a custom domain](https://deploywithox.com/docs/operate/domains) with one A record; Caddy gets the HTTPS certificate. - [Read the logs](https://deploywithox.com/docs/operate/logs) live, search them, or download them. ## Flask FAQ ### Do I need a Dockerfile or an ox.toml to deploy Flask to a VPS? No. ox does not use Docker, and `ox check` on a Flask repo with no ox.toml prints `Ready to deploy.` Write an ox.toml only to change what ox detected. ### What does ox install on the server for Flask? Only what the plan names: `uv 0.11` (ox's default). Your app's own dependencies are installed by the build into the release folder, not system-wide. ### Does ox work with a Flask app factory? Yes. With `create_app()` in `app/__init__.py` and no top-level app object, ox runs `gunicorn 'app:create_app()'`. ## Related guides - [**Symfony**Deploy Symfony to your own VPS: ox runs composer install for prod, Doctrine migrations and FrankenPHP under systemd, and asks for APP_SECRET first.](https://deploywithox.com/docs/guides/symfony) - [**Plain PHP**Deploy a plain PHP site to your own VPS: ox finds index.php, installs FrankenPHP, and serves the site under systemd behind Caddy with HTTPS, no ox.toml.](https://deploywithox.com/docs/guides/php) Every stack ox deploys, and the ones it does not yet, is on the [Stacks page](https://deploywithox.com/docs/guides). Last updated 2026-10-09. The page as HTML: https://deploywithox.com/docs/guides/flask