← All posts

How to Deploy a FastAPI + Postgres App with git push

October 24, 2026

TL;DR: A FastAPI app deploys cleanly from git when it does four things: pins its dependencies, starts with a production server that listens on 0.0.0.0 and the PORT variable, reads its database from DATABASE_URL with the right driver scheme, and runs migrations before it starts serving. Get those right and every push becomes a deploy.

This guide walks through each one with files you can copy, using SQLAlchemy and Alembic, the most common FastAPI + Postgres setup.

What does a deployable FastAPI project look like?

Here’s a minimal layout:

my-api/
├── app/
│   ├── __init__.py
│   ├── main.py        # FastAPI() lives here
│   └── db.py          # engine + session
├── alembic/
│   └── versions/
├── alembic.ini
├── requirements.txt
├── scripts/
│   └── start.sh
└── .candorfile

Two files matter most for detection. On Candor Deploy, a pyproject.toml or requirements.txt marks the repo as a Python app, and FastAPI is one of the frameworks it recognizes (builds and configuration docs). The build itself runs on Nixpacks, so there’s no Dockerfile to write unless you want one.

Pin your dependencies so the server builds exactly what you tested:

fastapi==0.115.*
uvicorn[standard]==0.32.*
sqlalchemy==2.0.*
psycopg[binary]==3.2.*
alembic==1.14.*

Version ranges like these keep you on known minor versions. If you use a lockfile tool, commit the lockfile too.

Why won’t my FastAPI app answer after it deploys?

Almost always, it’s the port or the interface. Locally you run uvicorn app.main:app --reload, which listens on 127.0.0.1:8000. In a container, 127.0.0.1 means “only this container,” so the platform’s router can’t reach it, and port 8000 is usually not the port it’s sending traffic to.

On Candor Deploy the contract is one line: listen on the port in the PORT environment variable. It defaults to 8080, traffic is routed to it, and the app is health-checked by opening connections to it. An app on a hard-coded port builds fine and then never comes live (builds and configuration docs). The troubleshooting guide adds the second half: listen on all interfaces, 0.0.0.0, not just localhost.

So the production command is:

uvicorn app.main:app --host 0.0.0.0 --port "${PORT:-8080}"

Drop --reload in production. It watches files for changes, which costs resources and does nothing useful in a container.

How should a FastAPI app connect to Postgres?

Read the connection string from the environment, never from code. One detail trips up most SQLAlchemy projects: psycopg 3 wants the URL scheme postgresql+psycopg://, while many hosts hand you plain postgresql://. With the plain scheme, SQLAlchemy looks for the older psycopg2 driver and crashes at startup.

A small normalizer makes your app work either way:

# app/db.py
import os
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

def database_url() -> str:
    url = os.environ["DATABASE_URL"]
    if url.startswith("postgres://"):
        url = "postgresql://" + url[len("postgres://"):]
    if url.startswith("postgresql://"):
        url = "postgresql+psycopg://" + url[len("postgresql://"):]
    return url

engine = create_engine(database_url(), pool_pre_ping=True)
SessionLocal = sessionmaker(bind=engine, autoflush=False)

pool_pre_ping=True checks a pooled connection before using it, which saves you from stale-connection errors after a database restart.

On Candor Deploy, the Attach a database control on your app’s Environment tab writes the connection URI into your app as DATABASE_URL, and detection is driver-aware: a SQLAlchemy project on psycopg 3 gets a postgresql+psycopg:// URL (builds and configuration docs). The databases docs also say to edit the variable if your framework complains about the driver. The normalizer above means you don’t have to think about it.

Keep the database URL in one place. Alembic should read the same function rather than a hard-coded sqlalchemy.url in alembic.ini:

# alembic/env.py (excerpt)
from app.db import database_url
config.set_main_option("sqlalchemy.url", database_url())

Where do Alembic migrations run?

In your app’s own startup, before the server begins listening. The Candor Deploy databases docs are direct about it: schema migrations are your app’s job, run them when your app starts, and there’s no separate release-phase hook. The database is also reachable only from your own services, never from the internet, so you can’t run alembic upgrade head from your laptop against production.

A start script handles both steps:

#!/bin/sh
# scripts/start.sh
set -e

echo "Applying migrations..."
alembic upgrade head

echo "Starting FastAPI on port ${PORT:-8080}"
exec uvicorn app.main:app --host 0.0.0.0 --port "${PORT:-8080}"

set -e stops at the first failure, so a broken migration never leads to a half-started app. exec hands the process over to uvicorn so it receives shutdown signals directly.

Because the new version only goes live after it answers on its port, a failing migration leaves your previous version serving. Keep each migration backward compatible with the code that’s already running, and save long backfills for a separate job. Our guide to expand/contract migrations covers how.

How do you tell the platform how to start the app?

Python apps don’t have a universal start convention the way Node has npm start, so be explicit. On Candor Deploy, a .candorfile in your repo sets the start command, and it wins over anything detected:

services:
  web:
    start: sh scripts/start.sh
    database: postgres

The database key asks for a Postgres database and wires its connection string into your environment. If you’d rather create the database in the portal and attach it there, leave that line out. The file is optional; everything in it can be set elsewhere, but committing it means the config travels with your code.

What else should you check before the first deploy?

A short checklist:

  • Health endpoint. Add a cheap route such as @app.get("/healthz") that returns {"ok": True} without touching the database. It gives you something to curl after a deploy.
  • Secrets. Put SECRET_KEY, API keys and similar values on the app’s Environment tab, one KEY=value per line. Environment changes apply on the next deploy, so redeploy after saving.
  • Workers. If you use one, set --workers to fit your memory. Each worker is a separate Python process with its own memory, so more isn’t automatically better on a small plan.
  • CORS. If a separate frontend calls the API from another origin, configure CORSMiddleware with your real frontend origin rather than *.
  • Logs. Print to stdout. Uvicorn’s access log already does, and so does Python’s logging with a default stream handler.

What happens when something goes wrong?

Read the two logs in order. Build output, on the Deployments tab, tells you whether dependencies installed and the stack was detected. Runtime logs, on the Logs tab, show the uvicorn banner, your migration output and any traceback (troubleshooting docs).

The three failures you’ll see most:

  1. Stuck in Deploying. The app isn’t listening on 0.0.0.0:$PORT. Check your start command.
  2. ModuleNotFoundError: psycopg2. The URL scheme is plain postgresql://. Use the normalizer above.
  3. Connection refused right after creating the database. A fresh Postgres takes a moment to generate credentials. Redeploy once its Connect tab shows a connection URI.

What should you do next?

Add scripts/start.sh and a .candorfile to your FastAPI repo, run it once locally with PORT=8080 sh scripts/start.sh, then push. The Candor Deploy quickstart walks through connecting the repo and watching your first build.

Written with help from our AI assistant.