Docker and Docker Compose
Containerizing your FastAPI app ensures consistent behavior across development, staging, and production. A well-structured Dockerfile and docker-compose.yml let you spin up the entire stack — app, database, cache — with one command.
Learning Focus
By the end of this lesson you can: write a production-quality Dockerfile for FastAPI, orchestrate services with Docker Compose, manage secrets with environment files, and run database migrations as part of the startup.
Production Dockerfile
Dockerfile
# Stage 1: Build dependencies
FROM python:3.11-slim AS builder
WORKDIR /app
# Install build tools for compiled packages
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential libpq-dev \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# Stage 2: Runtime image (smaller)
FROM python:3.11-slim AS runtime
WORKDIR /app
# Copy installed packages from builder
COPY --from=builder /install /usr/local
# Copy application code
COPY app/ ./app/
COPY alembic/ ./alembic/
COPY alembic.ini .
# Non-root user for security
RUN useradd -m -u 1000 appuser
USER appuser
EXPOSE 8000
CMD ["gunicorn", "app.main:app", \
"--workers", "4", \
"--worker-class", "uvicorn.workers.UvicornWorker", \
"--bind", "0.0.0.0:8000", \
"--timeout", "60", \
"--access-logfile", "-"]
Docker Compose for Full Stack
docker-compose.yml
version: "3.9"
services:
app:
build: .
ports:
- "8000:8000"
env_file:
- .env
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
command: >
sh -c "alembic upgrade head && gunicorn app.main:app
--workers 4 --worker-class uvicorn.workers.UvicornWorker
--bind 0.0.0.0:8000"
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 5s
timeout: 5s
retries: 5
restart: unless-stopped
redis:
image: redis:7-alpine
command: redis-server --requirepass ${REDIS_PASSWORD}
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
postgres_data:
Environment File
.env
# Application
DATABASE_URL=postgresql+asyncpg://postgres:postgres@db:5432/app
SECRET_KEY=change-me-in-production-use-32-random-bytes
DEBUG=false
ALLOWED_ORIGINS=https://app.example.com
# Database
POSTGRES_USER=postgres
POSTGRES_PASSWORD=change-me-in-production
POSTGRES_DB=app
# Redis
REDIS_PASSWORD=change-me-redis
REDIS_URL=redis://:change-me-redis@redis:6379/0
Build and Run
deploy-commands.sh
# Build and start all services
docker compose up -d --build
# View logs
docker compose logs -f app
# Run migrations manually
docker compose exec app alembic upgrade head
# Scale app workers
docker compose up -d --scale app=3
# Stop everything
docker compose down
Health Check Endpoint
Always add a health check endpoint for Docker and load balancers:
app/routers/health.py
from fastapi import APIRouter
from sqlalchemy import text
from app.db.session import AsyncSessionFactory
router = APIRouter(tags=["health"])
@router.get("/health")
async def health() -> dict:
return {"status": "ok"}
@router.get("/health/db")
async def health_db() -> dict:
async with AsyncSessionFactory() as db:
await db.execute(text("SELECT 1"))
return {"status": "ok", "database": "connected"}
Common Pitfalls
| Pitfall | Cause / Symptom | Fix |
|---|---|---|
| App starts before DB is ready | No depends_on health check | Use condition: service_healthy with DB healthcheck |
| Migrations run on every startup | No idempotency check | alembic upgrade head is idempotent — safe to run every start |
| Large Docker image | Not using multi-stage build | Use builder → runtime two-stage pattern |
| Secrets in Dockerfile | ARG/ENV with secrets | Use .env files and Docker secrets, never bake secrets into image |
| Port 8000 exposed to internet | No Nginx reverse proxy | Always run Nginx in front of Gunicorn/Uvicorn |
Hands-On Practice
quickstart-docker.sh
# 1) Create a .env file
cp .env.example .env
# Edit .env with your values
# 2) Build and run
docker compose up -d --build
# 3) Check app is healthy
curl http://localhost:8000/health
# → {"status": "ok"}
curl http://localhost:8000/health/db
# → {"status": "ok", "database": "connected"}
# 4) View running containers
docker compose ps
# 5) Tail logs
docker compose logs -f --tail=50 app