Shipping a FastAPI app to production has a clear playbook in 2026: containerize it with a lean Docker image, run Gunicorn + Uvicorn workers for process supervision, and deploy to a platform that handles TLS and scaling for you. This guide walks every step.
What changed in 2026
- Python 3.12/3.13 are the production LTS targets; the official
python:3.13-slim base image is ~45 MB compressed.
uv (from Astral) is the dominant Python package manager in 2026 — installs 10–100× faster than pip, lockfile-first, drop-in replacement in Docker.
- Cloud Run, Fly.io, and Railway are the go-to managed platforms for teams that don't want to manage Kubernetes. Each accepts a Dockerfile directly.
- Healthcheck endpoints are table stakes — every load balancer and k8s liveness probe expects a
GET /healthz that returns 200.
The Dockerfile (multi-stage)
# Stage 1: build dependencies
FROM python:3.13-slim AS builder
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN pip install uv && uv sync --frozen --no-dev
# Stage 2: runtime image
FROM python:3.13-slim AS runtime
WORKDIR /app
# Non-root user
RUN adduser --disabled-password --gecos "" appuser
COPY --from=builder /app/.venv /app/.venv
COPY ./app ./app
ENV PATH="/app/.venv/bin:$PATH"
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
USER appuser
EXPOSE 8000
CMD ["gunicorn", "app.main:app", \
"-k", "uvicorn.workers.UvicornWorker", \
"--workers", "4", \
"--bind", "0.0.0.0:8000", \
"--timeout", "120", \
"--access-logfile", "-"]
Keep .dockerignore tight:
__pycache__/
*.pyc
.env
.env.*
tests/
.git/
Environment configuration
Use pydantic-settings to read from environment variables:
# app/core/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
DATABASE_URL: str
SECRET_KEY: str
DEBUG: bool = False
WORKERS: int = 4
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
settings = Settings()
In production, inject these via your cloud platform's secret/env UI — never bake them into the image.
Health check endpoint
# Add to app/main.py
from fastapi import FastAPI
from sqlalchemy import text
from app.db.session import AsyncSessionLocal
app = FastAPI()
@app.get("/healthz", include_in_schema=False)
async def health():
try:
async with AsyncSessionLocal() as db:
await db.execute(text("SELECT 1"))
return {"status": "ok"}
except Exception:
from fastapi import Response
return Response(status_code=503, content="db unavailable")
Deployment targets comparison
| Platform |
Best for |
Cold starts |
Managed DB |
| Cloud Run (GCP) |
Serverless, pay-per-request |
~1–2 s |
Cloud SQL |
| Fly.io |
Always-on, global PoPs |
Near zero |
Fly Postgres |
| Railway |
Simplest DX, hobby/startup |
Near zero |
Railway Postgres |
| ECS Fargate |
AWS-native, no k8s overhead |
~10 s |
RDS |
| Kubernetes |
Full control, team expertise |
Depends on HPA |
Any |
For most SaaS apps: Railway to ship fast, Cloud Run or Fly.io as you scale.
How to pick your deployment target
- Solo project or early startup — Railway or Fly.io. One
railway up or fly deploy.
- GCP-native stack — Cloud Run. Scales to zero, integrates with Cloud SQL and Secret Manager.
- AWS-native — ECS Fargate + ECR + RDS. More IAM to configure, but fits existing AWS accounts.
- Full control — Kubernetes (GKE, EKS, AKS). Overkill until you need it.
Deploying to Fly.io (fastest path)
# One-time setup
fly launch --dockerfile Dockerfile --no-deploy
fly secrets set DATABASE_URL="postgresql+asyncpg://..." SECRET_KEY="..."
# Deploy
fly deploy
# Scale workers (optional)
fly scale count 2
Common mistakes
Running with uvicorn app.main:app --reload in production. The --reload flag watches the filesystem and restarts constantly; it is a dev-only flag.
No health check. Load balancers send traffic to broken instances. A /healthz that checks the DB connection catches the most common failure mode.
Too many workers. Each Gunicorn worker is a full Python process. 4–8 workers on a 2-core instance exhausts RAM. Start at (2 × cores) + 1 and measure.
Not setting --timeout on Gunicorn. Without it, a slow DB query hangs the worker until the OS kills it; set a realistic timeout (30–120 s).
Exposing debug mode. DEBUG=True in production can leak stack traces. Gate it on an env var and default to False.
What to skip
- Running the app as root in the container — unnecessary privilege; add a non-root user.
- Alpine-based Python images — glibc compatibility issues with many Python packages make
slim images the safer choice.
- Storing logs in the container — write to stdout/stderr and let the platform collect them.
FAQ
How many Uvicorn workers should I use?
Start with (2 × CPU cores) + 1. For I/O-heavy apps (DB, HTTP calls), workers can be higher; for CPU-heavy (ML inference), match cores exactly.
Do I need Nginx in front of FastAPI?
On managed platforms (Cloud Run, Fly, Railway), no — they handle TLS termination and load balancing. On bare VMs, yes: Nginx handles TLS, static files, and buffering.
How do I handle database migrations on deploy?
Run alembic upgrade head as an init container or entrypoint script before the app starts. Do not run migrations from the Gunicorn CMD.
Is Docker Compose okay for production?
For a single-server deployment with low traffic, it works. For anything that needs scaling or zero-downtime deploys, use a proper platform.
Where to go next
See How to build an API in Python in 2026, How to write a database migration in 2026, and How to set up Postgres locally in 2026.