Production Architecture Guide

Production FastAPI Docker Compose with PostgreSQL & Caddy (Auto-SSL)

Updated September 2026 Python 3.11 / FastAPI / Uvicorn / PostgreSQL 16 / Caddy 2

Running FastAPI in production requires more than a simple development server. A robust architecture needs a multi-stage Docker build that eliminates build dependencies from the final image, a dedicated non-root user for container security, database connection healthchecks, and an automated reverse proxy with Let's Encrypt TLS certificates.

Build Your Custom FastAPI Stack Visually

Use DevOpsForge to generate this exact stack or customize databases, ports, and proxies with zero configuration errors.

1. Production `docker-compose.yml`

This Compose file mounts Caddy as an auto-SSL gateway in front of a multi-worker FastAPI Uvicorn service, connected to a hardened PostgreSQL 16 database with automated container healthchecks and resource limits.

docker-compose.yml
version: '3.8'

services:
  caddy:
    image: caddy:2-alpine
    container_name: caddy_proxy
    restart: always
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp" # HTTP/3 QUIC
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      backend:
        condition: service_healthy

  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    container_name: fastapi_app
    restart: always
    expose:
      - "8080"
    environment:
      - ENVIRONMENT=production
      - DB_HOST=database
      - DB_PORT=5432
      - DB_NAME=${DB_NAME:-fastapi_db}
      - DB_USER=${DB_USER:-fastapi_user}
      - DB_PASSWORD=${DB_PASSWORD:-fastapi_secret_pass}
      - REDIS_HOST=redis
      - REDIS_PORT=6379
    deploy:
      resources:
        limits:
          cpus: '1.5'
          memory: 1024M
    depends_on:
      database:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8080/healthz')\""]
      interval: 15s
      timeout: 5s
      retries: 3
      start_period: 10s

  database:
    image: postgres:16-alpine
    container_name: fastapi_postgres
    restart: always
    environment:
      - POSTGRES_DB=${DB_NAME:-fastapi_db}
      - POSTGRES_USER=${DB_USER:-fastapi_user}
      - POSTGRES_PASSWORD=${DB_PASSWORD:-fastapi_secret_pass}
    volumes:
      - pg_data:/var/lib/postgresql/data
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 1024M
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-fastapi_user} -d ${DB_NAME:-fastapi_db}"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    container_name: fastapi_redis
    restart: always
    command: redis-server --appendonly yes
    volumes:
      - redis_data:/data
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3

volumes:
  caddy_data:
  caddy_config:
  pg_data:
  redis_data:

2. Hardened Multi-Stage Python `Dockerfile`

This Dockerfile uses a 2-stage build: Stage 1 builds binary pip wheels without polluting the runtime container with GCC or development libraries. Stage 2 copies the pre-built wheels and runs the application under a non-root system user.

backend/Dockerfile
# --- Stage 1: Build Wheels ---
FROM python:3.11-slim AS builder
WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt ./
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /app/wheels -r requirements.txt

# --- Stage 2: Hardened Runtime ---
FROM python:3.11-slim
WORKDIR /app

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

# Security: Dedicated non-root user
RUN addgroup --system --gid 1001 appgroup && \
    adduser --system --uid 1001 --gid 1001 appuser

COPY --from=builder /app/wheels /wheels
COPY --from=builder /app/requirements.txt ./
RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels

COPY . .
RUN chown -R appuser:appgroup /app

USER appuser

EXPOSE 8080
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080", "--workers", "2"]

3. Production `Caddyfile` with Automatic HTTPS

Caddy automatically provisions, verifies, and renews Let's Encrypt / ZeroSSL TLS certificates with zero manual cron configuration. It also enforces modern security headers and HTTP/3 support.

Caddyfile
api.yourdomain.com {
    encode gzip zstd

    # Proxy to FastAPI Uvicorn upstream
    reverse_proxy backend:8080

    # Security Headers
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        Referrer-Policy "strict-origin-when-cross-origin"
    }
}

4. Production `main.py` Healthcheck Endpoint

Ensure your FastAPI application provides a lightweight /healthz endpoint so Docker and Caddy can verify container readiness before directing traffic.

backend/main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI(
    title="Production FastAPI App",
    docs_url="/docs",
    redoc_url=None
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://yourdomain.com"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

@app.get("/healthz", tags=["Health"])
async def health_check():
    return {"status": "healthy", "service": "fastapi-backend"}

@app.get("/")
async def root():
    return {"message": "FastAPI Production Backend is Running"}