Skip to main content

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

PitfallCause / SymptomFix
App starts before DB is readyNo depends_on health checkUse condition: service_healthy with DB healthcheck
Migrations run on every startupNo idempotency checkalembic upgrade head is idempotent — safe to run every start
Large Docker imageNot using multi-stage buildUse builder → runtime two-stage pattern
Secrets in DockerfileARG/ENV with secretsUse .env files and Docker secrets, never bake secrets into image
Port 8000 exposed to internetNo Nginx reverse proxyAlways 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

What's Next