[Blog](https://console.f1b10.testing.getox.in/blog) [Guides](https://console.f1b10.testing.getox.in/blog#c-guides) # A FastAPI App on Your Own Server 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. Saurav Sharma Oct 8, 2026 7 min read On this page - [What you need](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#heading-what-you-need) - [What ox finds in a FastAPI repo by itself](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#heading-detect) - [The one config file](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#heading-config) - [Add a health route](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#heading-health) - [Connect FastAPI to Postgres with SQLAlchemy](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#heading-postgres) - [Set your secrets](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#heading-secrets) - [Check, then ship it](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#heading-ship) - [If the deploy fails](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#heading-fails) - [Key takeaways](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#key-takeaways) - [Frequently asked questions](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#faq) - [Try it](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#heading-try-it) 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](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#heading-what-you-need) - [What ox finds in a FastAPI repo by itself](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#heading-detect) - [The one config file](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#heading-config) - [Add a health route](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#heading-health) - [Connect FastAPI to Postgres with SQLAlchemy](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#heading-postgres) - [Set your secrets](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#heading-secrets) - [Check, then ship it](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#heading-ship) - [If the deploy fails](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#heading-fails) - [Frequently asked questions](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#faq) ## 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 ```bash curl -fsSL https://deploywithox.com/install/ | sudo bash ``` This installs the ox agent and Caddy. Then press New project and pick your repo. The [quickstart](https://console.f1b10.testing.getox.in/docs/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 ```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](https://uvicorn.dev/) 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](https://console.f1b10.testing.getox.in/docs/config) 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 ```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 `postgresql://`, which [SQLAlchemy](https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls) reads as the psycopg2 driver. This app uses psycopg 3, so put the driver in the name when you read it: Python ```python import os import re from sqlalchemy import create_engine url = re.sub(r"^postgres(ql)?://", "postgresql+psycopg://", os.environ["DATABASE_URL"]) engine = create_engine(url) ``` Point [Alembic](https://alembic.sqlalchemy.org/en/latest/tutorial.html)'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](https://console.f1b10.testing.getox.in/docs/operate/backups) 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 SECRET_KEY`, which asks for the value. The [variables page](https://console.f1b10.testing.getox.in/docs/operate/variables) has the details. Every key in `.env.example` that needs your answer (empty, a secret, or a placeholder) must be set before a deploy can start; a key with a real value keeps the app's default. ox never reads your `.env` file. ## Check, then ship it Run these from your repo: Shell ```bash 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: Code ```text Provided by ox: PORT, HOST, OX_ENV, OX_PROJECT, OX_RELEASE, OX_DATA_DIR, PUBLIC_URL, PUBLIC_HOST, PUBLIC_SCHEME, PUBLIC_PORT, DATABASE_URL Set on the dashboard before the first deploy: SECRET_KEY hint: SQLAlchemy (from uv.lock) reads DATABASE_URL's postgresql:// as psycopg2, which it does not list; in your code, use re.sub(r"^postgres(ql)?://", "postgresql+psycopg://", os.environ["DATABASE_URL"]) 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](https://console.f1b10.testing.getox.in/docs/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 `postgresql://` to `postgresql+psycopg://` for SQLAlchemy on psycopg 3, 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 ask for psycopg2? Because the `DATABASE_URL` ox gives your app starts with `postgresql://`, and SQLAlchemy reads that as the psycopg2 driver. If your app uses psycopg 3, 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 on another driver, `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 for your first project, and ox Pro is $10 a month for as many as you like. Sign up with GitHub at [deploywithox.com](https://console.f1b10.testing.getox.in/). The full [FastAPI guide](https://console.f1b10.testing.getox.in/docs/guides/fastapi) has every detail from this post. The [config reference](https://console.f1b10.testing.getox.in/docs/config) lists every key you can put in `ox.toml`. [The story](https://console.f1b10.testing.getox.in/story) says why I built ox, and [the features page](https://console.f1b10.testing.getox.in/features) shows what else you get. New to running apps on your own server? [How ox self-hosts your apps](https://console.f1b10.testing.getox.in/self-hosting) shows what ox sets up on your server. On this page - [What you need](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#heading-what-you-need) - [What ox finds in a FastAPI repo by itself](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#heading-detect) - [The one config file](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#heading-config) - [Add a health route](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#heading-health) - [Connect FastAPI to Postgres with SQLAlchemy](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#heading-postgres) - [Set your secrets](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#heading-secrets) - [Check, then ship it](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#heading-ship) - [If the deploy fails](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#heading-fails) - [Key takeaways](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#key-takeaways) - [Frequently asked questions](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#faq) - [Do I need Docker to deploy FastAPI to a VPS?](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#heading-faq-docker) - [Does ox find my FastAPI app without an ox.toml?](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#heading-faq-detect) - [Why does SQLAlchemy ask for psycopg2?](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#heading-faq-sqlalchemy) - [What happens if an Alembic migration fails?](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#heading-faq-migration) - [Try it](https://console.f1b10.testing.getox.in/blog/deploy-fastapi-to-your-own-vps#heading-try-it) Written by Saurav Sharma Builds ox. Writes about running apps on servers you own, without Docker and without a platform bill. [Older post Deploy Django Without Docker: Gunicorn, systemd and Caddy](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker) [Newer post Self-Hosted Deploy Tools Felt Too Busy, So I Built ox](https://console.f1b10.testing.getox.in/blog/why-i-built-ox-simple-self-hosted-deploys) ## Keep reading - Guides [How to Self-Host Web Apps on Your Own VPS (2026 Guide)](https://console.f1b10.testing.getox.in/blog/self-host-web-apps-on-your-own-vps) What you need to self-host a web app on a VPS: server, domain, HTTPS, database, backups. Four ways to set it up, their trade-offs, and how ox does it. Saurav Sharma Oct 15, 2026 8 min read - Guides [Deploy Django Without Docker: Gunicorn, systemd and Caddy](https://console.f1b10.testing.getox.in/blog/deploy-django-to-a-vps-without-docker) Deploy Django without Docker on your Ubuntu VPS: gunicorn under systemd, Caddy for HTTPS and PostgreSQL. Every step by hand, then the same deploy with ox. Saurav Sharma Oct 8, 2026 9 min read - Guides [Deploy Django and Vue.js on a VPS: Nginx, Gunicorn, Celery](https://console.f1b10.testing.getox.in/blog/deployment-guide-for-django-vuejs-project-on-vps) Deploy a Django and Vue.js app on a VPS: Nginx serves the Vue build, Gunicorn runs Django, Celery and Redis under systemd, with HTTPS from Certbot. Saurav Sharma Jun 4, 2025 14 min read ## Deploy to a server you own ox sets up systemd, Caddy and PostgreSQL on your Ubuntu server and deploys on every push. Free for your first project. [Sign up with GitHub](https://console.f1b10.testing.getox.in/login/github) [Read the docs](https://console.f1b10.testing.getox.in/docs)