Cheat SheetsFastAPIDeployment

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 history
3

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