# Deploy FastAPI to Your Own VPS with One Config File

To deploy a FastAPI app to your own VPS, you need a server, a start command for uvicorn, and a way to get HTTPS, a database and migrations in place. With ox, all of that lives in one small file called `ox.toml`. You run `ox check`, then `ox deploy`, and your API is live on your own Ubuntu server. This post walks through it step by step.

## What you need

*   A FastAPI app that runs with uvicorn, in a GitHub repo.
    
*   A fresh Ubuntu LTS server with at least 1 GB of memory, 10 GB of free disk, and ports 80 and 443 free.
    
*   An ox account. You sign in with GitHub.
    

In the ox dashboard, press Add a server. It shows one command with a fresh token in it. Run it on the server as root:

```bash
curl -fsSL https://deploywithox.com/install/<token> | sudo bash
```

This installs the ox agent and Caddy. Then press New project and pick your repo.

## What ox finds by itself

ox reads the Python files at the root of your repo. Often you need no config at all. It looks for:

*   **The install step**, from your lockfile. For `uv.lock` that is `uv sync --frozen --no-dev`. It also knows `poetry.lock`, `requirements.txt` and a plain `pyproject.toml`.
    
*   **The Python version**, from `.python-version`, `.tool-versions` or `mise.toml`.
    
*   **A start command.** This works when uvicorn or `fastapi[standard]` is a dependency, and one of `main.py`, `app.py`, `app/main.py` or `src/main.py` makes the app with `FastAPI(`.
    
*   **Migrations.** When `alembic.ini` is in the repo and alembic is a dependency, it runs `uv run alembic upgrade head`.
    
*   **Services.** When the repo has no `ox.toml`, it adds PostgreSQL if psycopg, psycopg2 or asyncpg is a dependency, and Redis if redis is.
    

If ox is not sure, it does not guess a start command. `ox check` prints a hint that says why. Then you set it yourself.

## The one config file

When you want your own domain, a health check and full control, add this `ox.toml` to the root of your repo:

```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 = {}
```

Here is what each line does:

*   `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.
    

## Add a health route

The health route should answer without touching the database, so it stays fast:

```python
from fastapi import FastAPI

app = FastAPI()


@app.get("/health")
def health():
    return {"ok": True}
```

This route matters. On each deploy, the new release starts next to the old one. Traffic moves only after `/health` answers. So a broken release never takes your API down.

## Connect to Postgres

With `postgres = {}`, ox makes a database and gives your app `DATABASE_URL`. Add `redis = {}` and you also get `REDIS_URL`.

There is one small catch. `DATABASE_URL` starts with `postgres://`. SQLAlchemy wants the driver in the name, so change the start of it when you read it:

```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`. That way your migrations and your app use one database.

## Set your secrets

ox also gives you `PORT`, `HOST`, `PUBLIC_URL` and `PUBLIC_HOST`. Set your own keys, like `SECRET_KEY`, on the dashboard. Or use `ox vars set <project> 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, then ship it

Run these from your repo:

```bash
ox check                      # in the repo: prints the plan and "Ready to deploy."
ox deploy <project> --wait    # stream the deploy, exit with its result
ox logs <project> --follow    # the app's own logs
```

For the `ox.toml` above, `ox check` ends like this:

```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.
```

The check runs before anything on the server changes. If a key is missing, you find out now, not after a broken deploy.

## If it fails

*   **"nothing to run"**: ox found no start command. The hint above it says why, such as uvicorn not being a dependency. Add uvicorn, or set `[app] start`.
    
*   **The app exited while starting, or** `/health` **did not answer within 120s**: read the app's log above it. A wrong module path like `main:app` instead of `app.main:app` is a common cause. So is a `KeyError` for a missing variable.
    
*   **The migrate step failed**: Alembic's own output is above it. 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.

## Try it

ox is free while it is in beta. Sign up with GitHub at [deploywithox.com](https://deploywithox.com). The full [FastAPI guide](https://deploywithox.com/docs/guides/fastapi) has every detail from this post. The [config reference](https://deploywithox.com/docs/config) lists every key you can put in `ox.toml`.

---

![ox](https://deploywithox.com/static/apple-touch-icon.png)

**Tired of doing all this by hand?** [ox](https://deploywithox.com) deploys your repo to your own server for you. No Docker, no config maze. [Try deploywithox.com](https://deploywithox.com)
