# Deploy FastAPI to a VPS with uvicorn and Postgres Deploy FastAPI to your own VPS: ox detects the uvicorn start command, installs from uv.lock, runs Alembic migrations, and adds PostgreSQL from one ox.toml. To deploy FastAPI to a VPS with ox, add the repo as a project and deploy: ox detects the uvicorn start command, installs your packages from `uv.lock`, `poetry.lock` or `requirements.txt`, and runs Alembic migrations after a database snapshot. uvicorn runs as a systemd service behind Caddy, which serves HTTPS on your domain. Write an ox.toml, as below, when you want a domain, a health path or PostgreSQL. ## What ox detects in a FastAPI repo ox reads the Python files at the repo root. It proposes: - The install step from your lockfile: `uv sync --frozen --no-dev` for `uv.lock`, `poetry install --only main --no-root` for `poetry.lock`, `uv venv --allow-existing && uv pip install -r requirements.txt` for `requirements.txt`, or `uv sync --no-dev` for a `pyproject.toml` alone. - The Python version from `.python-version`, `.tool-versions` or `mise.toml`. - A start command, when `uvicorn` or `fastapi[standard]` is a dependency and one of `main.py`, `app.py`, `app/main.py` or `src/main.py` contains `FastAPI(`. ox reads the object's name from the line that makes it, such as `app = FastAPI()`. For `app/main.py` with a `uv.lock` that is `uv run uvicorn app.main:app --host 127.0.0.1 --port $PORT`. - The migrate step `uv run alembic upgrade head`, when `alembic.ini` is in the repo and `alembic` is a dependency. - When the repo has no ox.toml: PostgreSQL when `psycopg`, `psycopg2` or `asyncpg` is a dependency, and Redis when `redis` is. When ox cannot be sure, it proposes no start and `ox check` prints a hint saying why: uvicorn is not a dependency, the app is made inside a function, or the file makes more than one FastAPI object. Set `[app] start` yourself then. ## The ox.toml for FastAPI ox.toml for a FastAPI app with PostgreSQL and Alembic: ```toml domains = ["api.example.com"] [app] start = "uv run uvicorn app.main:app --host 127.0.0.1 --port $PORT --workers 2" health = "/health" [build] install = "uv sync --frozen --no-dev" migrate = "uv run alembic upgrade head" [services] postgres = {} ``` - `domains`: the names your API answers on. ox gets the HTTPS certificate for each. - `start`: runs uvicorn on the port ox picks, with two worker processes. - `health`: a path that must answer with a 2xx or 3xx status before a new release gets traffic. - `install`: installs your packages from `uv.lock`. - `migrate`: runs your Alembic migrations on every deploy, after ox takes a snapshot of the database. - `postgres`: a database for this app. The health route should answer without touching the database, so it stays fast. app/main.py: ```python from fastapi import FastAPI app = FastAPI() @app.get("/health") def health(): return {"ok": True} ``` ## PostgreSQL with SQLAlchemy and Alembic With `postgres = {}`, ox creates a database and gives your app `DATABASE_URL` ([[services]](https://deploywithox.com/docs/config#services)). Add `redis = {}` and you also get `REDIS_URL`. Once a repo has an ox.toml, ox runs only the services it lists. `DATABASE_URL` starts with `postgres://`. SQLAlchemy wants the driver in the name, so change the start of it when you read it. When SQLAlchemy is a dependency, `ox check` prints this line for your driver as a hint. app/db.py: ```python import os from sqlalchemy import create_engine url = os.environ["DATABASE_URL"].replace("postgres://", "postgresql+psycopg://", 1) engine = create_engine(url) ``` Point Alembic's `env.py` at the same `url`, so migrations and the app use one database. ## Variables and secrets ox also gives you `PORT`, `HOST`, `PUBLIC_URL` and `PUBLIC_HOST`. Set your own keys, like `SECRET_KEY`, on the dashboard or with `ox vars set SECRET_KEY`, which asks for the value. Every key named in `.env.example` must be set before a deploy can start. ox never reads your `.env` file. ## Check and deploy the FastAPI app Check, deploy, and watch: ```sh ox check # in the repo: prints the plan and "Ready to deploy." ox deploy --wait # stream the deploy, exit with its result ox logs --follow # the app's own logs ``` For the ox.toml above, `ox check` ends like this: ox check, last lines: ```text Provided by ox: PORT, HOST, OX_ENV, OX_PROJECT, OX_RELEASE, OX_DATA_DIR, PUBLIC_URL, PUBLIC_HOST, DATABASE_URL Set on the dashboard before the first deploy: SECRET_KEY hint: SQLAlchemy (from uv.lock) rejects DATABASE_URL's postgres:// scheme; in your code, use os.environ["DATABASE_URL"].replace("postgres://", "postgresql+psycopg://", 1) Ready to deploy. ``` ## If the FastAPI deploy fails - `nothing to run: ox found no start command, static site, worker, or cron job; set [app] start (or declare [workers])`. `ox check` says this when ox found no start. The hint above it says why, such as `FastAPI found in main.py, but uvicorn is not a dependency`. Add `uvicorn` to your dependencies, or set `[app] start`. With no hint, your app file is somewhere ox does not look. - `the app exited while starting (…)`, or `GET http://127.0.0.1:/health did not answer within 120s`. Read the app's log above it. A wrong module path such as `main:app` instead of `app.main:app`, or a `KeyError` for a missing variable, are the usual causes. - `migrate: uv run alembic upgrade head: …` with `fix: the command's own output is above; fix it in the repo and push`. Alembic failed. Run the migration against a local database, fix it, and push. If an older release is live, it keeps serving while you fix any of these. More causes and fixes are in [Troubleshooting](https://deploywithox.com/docs/troubleshooting), and every key is in the [config reference](https://deploywithox.com/docs/config). ## Next steps - [Set SECRET_KEY and other variables](https://deploywithox.com/docs/operate/variables#set) from the dashboard or the CLI. - [Add your own domain with HTTPS](https://deploywithox.com/docs/operate/domains): one A record, and Caddy gets the certificate. - [Read and search the app's logs](https://deploywithox.com/docs/operate/logs), live or for a time range. - [Roll back a bad deploy](https://deploywithox.com/docs/operate/rollback) to a kept release without a rebuild. - [See the daily PostgreSQL backups](https://deploywithox.com/docs/operate/backups) and restore one. Last updated 2026-10-08. The page as HTML: https://deploywithox.com/docs/guides/fastapi