BlogGuides

Deploy FastAPI to Your Own VPS with One Config File

Put a FastAPI app with uvicorn, Postgres and Alembic on your own Ubuntu server, with HTTPS and no Docker, using one small ox.toml file you can copy.

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.

In this guide:

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:

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

This installs the ox agent and Caddy. Then press New project and pick your repo. The quickstart shows these screens.

What ox finds in a FastAPI repo 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.

The config reference lists every key you can put in ox.toml.

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 FastAPI to Postgres with SQLAlchemy

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. ox backs the database up every day, and the backups page shows how to restore one.

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. The variables page has the details.

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:

Shell
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:

Code
  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 the deploy 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. More causes are in troubleshooting.

Key takeaways

  • FastAPI needs no Docker on a VPS: uvicorn runs as a systemd service behind Caddy, which serves HTTPS.
  • ox often finds the install step, the start command, Alembic migrations and PostgreSQL from your repo alone.
  • One short ox.toml adds your domain, a health check, two uvicorn workers and a database.
  • Change postgres:// to postgresql+psycopg:// for SQLAlchemy, and point Alembic at the same URL.
  • ox check catches a missing key before anything on the server changes.

Frequently asked questions

Do I need Docker to deploy FastAPI to a VPS?

No. uvicorn runs well as a plain systemd service on Ubuntu, with Caddy in front of it for HTTPS. ox sets up both from your repo and your ox.toml, so there is no image to build and no container to run. Your API is a normal process on a server you own.

Does ox find my FastAPI app without an ox.toml?

Often, yes. When uvicorn or fastapi[standard] is a dependency and main.py, app.py, app/main.py or src/main.py makes the app with FastAPI(, ox proposes the uvicorn start command. If it is not sure, it proposes none, and ox check prints a hint saying why.

Why does SQLAlchemy reject my DATABASE_URL?

Because the DATABASE_URL ox gives your app starts with postgres://, and SQLAlchemy wants the driver in the name. Replace the start with postgresql+psycopg:// when you read it, and point Alembic's env.py at the same URL. When SQLAlchemy is a dependency, ox check prints this fix as a hint.

What happens if an Alembic migration fails?

The deploy stops at the migrate step, and Alembic's own output is right above the error. ox takes a snapshot of the database before it runs your migrations, and if an older release is live, it keeps serving. Run the migration against a local database, fix it, and push again.

Try it

ox is free while it is in beta. Sign up with GitHub at deploywithox.com. The full FastAPI guide has every detail from this post. The config reference lists every key you can put in ox.toml. The story says why I built ox, and the features page shows what else you get.

Keep reading

Deploy to a server you own

ox sets up systemd, Caddy and PostgreSQL on your Ubuntu server and deploys on every push. Free during the beta.

Sign up with GitHub Read the docs