Deployment — Cheat Sheet
FastAPI · 3 topics. Download the PDF or the Instagram carousel and share it.
Cheat Sheet · AiCanCode.org
Deployment
FastAPI3 topicsQuick revision reference
1
Servers & Workers — Running FastAPI in Production
Production = multiple Uvicorn worker processes behind a reverse proxy: workers ≈ cores for async apps, --proxy-headers so the app sees real client IPs and HTTPS, and graceful shutdown so deploys drop zero requests.
- ✓Production = Uvicorn --workers N (≈ cores for async apps) behind a reverse proxy
- ✓Everything per-worker multiplies: lifespan, memory, caches, WS managers
- ✓--proxy-headers + --forwarded-allow-ips restore real client IP and https scheme
- ✓Graceful shutdown (drain → teardown → exit) + readiness probes = zero-drop deploys
Fork workers ≈ cores; everything per-process multiplies by N
# Development:
fastapi dev main.py # 1 process, auto-reload — dev ONLY
# Production:
uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4
# What --workers 4 actually does:
# master process (PID 1) supervises 4 forked workers
# each worker: own interpreter, own GIL, own event loop, own lifespan run
# the OS load-balances the port's connections across them
# one worker crashes → master replaces it; the other 3 keep serving
# Sizing:
# async-heavy app (I/O-bound): workers = cores (loop saturates a core)
# lots of blocking def routes: more workers help, but fix the code first
# memory check: 4 workers × (app + model + pools) must fit RAM —
# the 2GB ML model from the lifespan chapter × 4 = 8GB. Plan for it.
# then MEASURE under representative load and adjust. No formula survives
# contact with a real workload.
# Per-worker consequences you already met:
# lifespan runs 4× (chapter: Lifespan) · in-process caches drift 4 ways
# (chapter: Caching) · WS ConnectionManagers are per-worker too —
# even ONE machine with 4 workers needs the Redis pub/sub bridge
# (chapter: WebSockets)
# Gunicorn as the process manager (the classic recipe, still common):
gunicorn app.main:app -k uvicorn.workers.UvicornWorker -w 4 \
--graceful-timeout 30 --bind 0.0.0.0:8000
# Uvicorn's own --workers is fine today; gunicorn adds mature process
# management (worker recycling via max-requests, detailed timeouts).2
Dockerizing FastAPI — Images Done Right
A slim Python base, requirements installed before code (layer-cache order), a non-root user, and uvicorn as CMD — plus docker-compose wiring Postgres/Redis for a one-command dev environment.
- ✓python:3.12-slim; skip alpine (musl vs manylinux wheels)
- ✓COPY requirements → pip install → COPY code: cached deps = seconds-fast rebuilds
- ✓Non-root USER; exec-form CMD so SIGTERM reaches uvicorn (graceful shutdown)
- ✓compose healthchecks gate startup; same image every env, config via environment
slim base, cached deps layer, non-root, exec-form CMD
# ── Dockerfile ──────────────────────────────
FROM python:3.12-slim
# slim: ~150MB vs ~1GB full. alpine: musl breaks compiled wheels — avoid.
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# no .pyc litter; logs flush immediately (or docker logs shows nothing on crash)
WORKDIR /app
# deps BEFORE code — the layer-cache money shot:
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# code changes daily; requirements change weekly. This order means the
# 90-second pip layer is CACHED for every code-only rebuild (→ ~2s builds).
COPY app/ ./app
COPY alembic/ ./alembic
COPY alembic.ini .
# non-root: a compromised app process shouldn't own the container
RUN useradd --create-home appuser
USER appuser
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]
# EXEC form (JSON array) — uvicorn IS PID 1 and receives SIGTERM directly.
# Shell form (CMD uvicorn ...) wraps it in /bin/sh, which EATS the signal:
# graceful shutdown never runs, K8s waits, then SIGKILLs. Classic.
# ── .dockerignore (keeps the build context and image clean) ──
# .venv/ .git/ __pycache__/ tests/ .env *.md
# .env especially: baking secrets into image layers = leaked via docker history3
Production Checklist — Logging, Monitoring & Hardening
The launch gate: structured JSON logs with request IDs, error tracking (Sentry), metrics + alerts on the RED trio, security headers and rate limits, timeouts on everything external — and the checklist that ties all 40 chapters together.
- ✓JSON logs to stdout with request_id — incidents become log queries
- ✓Sentry for grouped, alerting errors; users get ref ids, trackers get tracebacks
- ✓Watch RED (rate, errors, duration p99) and alert on user-felt symptoms
- ✓The launch checklist assembles the whole track: config, auth, data, resilience, observability
JSON logs + Sentry + RED metrics: incidents become queries
# ── 1. Structured JSON logging ──
import json, logging, sys
class JsonFormatter(logging.Formatter):
def format(self, record):
entry = {"ts": self.formatTime(record), "level": record.levelname,
"logger": record.name, "msg": record.getMessage()}
for key in ("request_id", "method", "path", "status", "ms", "user_id"):
if hasattr(record, key):
entry[key] = getattr(record, key)
return json.dumps(entry)
handler = logging.StreamHandler(sys.stdout) # stdout — the platform collects
handler.setFormatter(JsonFormatter())
logging.basicConfig(level=logging.INFO, handlers=[handler])
# access middleware logs with extra= (marries the Middleware chapter):
# log.info("request", extra={"request_id": rid, "method": m,
# "path": p, "status": s, "ms": elapsed})
# → {"ts":"...","level":"INFO","msg":"request","request_id":"7f3a",
# "method":"GET","path":"/orders/42","status":200,"ms":3.2}
# Incident flow: user reports error ref 7f3a → filter request_id=7f3a →
# the request's whole story. NEVER log: passwords, tokens, OTPs, full cards.
# ── 2. Error tracking (Sentry) ──
import sentry_sdk
sentry_sdk.init(dsn=settings.sentry_dsn, environment=settings.env,
traces_sample_rate=0.1) # + FastAPI auto-integration
# unhandled exceptions → grouped issues, stack + request context, alerts.
# Your catch-all handler (Errors chapter) and Sentry coexist: user gets
# the bland 500 + ref id; Sentry gets the truth.
# ── 3. Metrics: the RED trio ──
# pip install prometheus-fastapi-instrumentator
from prometheus_fastapi_instrumentator import Instrumentator
Instrumentator().instrument(app).expose(app) # → GET /metrics
# Rate, Errors, Duration (p50/p95/p99 histograms) per route.
# Alert on what users feel: 5xx ratio > 1% (5 min) · p99 > 2s
# · readiness failing · DB pool exhausted. Not on CPU%.Learn this free with Aria, your AI tutor → AiCanCode.org/learn/fastapi