How to Dockerize a FastAPI Application with Uvicorn

Prerequisites

  • FastAPI application with a requirements.txt or pyproject.toml
  • Docker installed
  • Python 3.11+

Step 1: Create the Dockerfile

# Build stage
FROM python:3.12-slim AS builder

WORKDIR /app

# Install build dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt

# Runtime stage
FROM python:3.12-slim

WORKDIR /app

# Copy installed packages from builder
COPY --from=builder /root/.local /root/.local

# Copy application code
COPY . .

# Ensure local bin is in PATH
ENV PATH=/root/.local/bin:$PATH

# Expose FastAPI default port
EXPOSE 8000

# Run Uvicorn
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Why multi-stage? The builder stage installs dependencies with full tooling (gcc, headers). The runtime stage is slim — no compilers, no build tools. This reduces image size significantly (~150 MB → ~80 MB for a typical FastAPI app).

Step 2: Configure Uvicorn properly

Don’t hardcode Uvicorn settings in Dockerfile. Use environment variables instead:

# main.py
import os
import uvicorn
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def root():
    return {"status": "ok"}

if __name__ == "__main__":
    uvicorn.run(
        "main:app",
        host=os.getenv("HOST", "0.0.0.0"),
        port=int(os.getenv("PORT", 8000)),
        workers=int(os.getenv("WORKERS", 1)),
        reload=os.getenv("RELOAD", "false").lower() == "true"
    )

Step 3: Create docker-compose.yml

version: '3.8'

services:
  api:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql+asyncpg://user:pass@db:5432/mydb
      - REDIS_URL=redis://redis:6379
      - ENVIRONMENT=development
      - LOG_LEVEL=debug
    volumes:
      - .:/app
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: mydb
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
      interval: 5s
      timeout: 3s
      retries: 5

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

volumes:
  pgdata:
  redis_data:

Note depends_on with condition: service_healthy — this waits for PostgreSQL to actually accept connections, not just start the container.

Step 4: Add a health check endpoint

from fastapi import FastAPI

app = FastAPI()

@app.get("/health")
def health_check():
    return {"status": "healthy"}

The health check in docker-compose above uses this endpoint.

Step 5: .dockerignore

__pycache__/
*.py[cod]
.env
.venv
venv/
.git
.gitignore
*.md
Dockerfile
docker-compose.yml
.pytest_cache/
.mypy_cache/

Step 6: Environment variables with Pydantic

Use Pydantic for validated config:

# config.py
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    database_url: str = "postgresql+asyncpg://user:pass@localhost:5432/mydb"
    redis_url: str = "redis://localhost:6379"
    environment: str = "development"
    log_level: str = "info"

    class Config:
        env_file = ".env"

settings = Settings()

Production hardening

# Use a non-root user
RUN addgroup --system app && adduser --system --group app
USER app

# Set Python optimizations
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PYTHONOPTIMIZE=2

# Use multiple workers (one per CPU core typically)
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]

Verification

docker-compose up --build

# Test endpoint
curl http://localhost:8000/
# {"status":"ok"}

# Test health check
curl http://localhost:8000/health
# {"status":"healthy"}

# Check container health status
docker ps --filter "name=api"
# Should show "(healthy)" in the STATUS column

FAQ

Q: Should I use --workers with Uvicorn? Yes in production, but use only 1 worker in development (for hot reload). For production, set workers to 2 * CPU_CORES + 1.

Q: Gunicorn or Uvicorn? Uvicorn alone is fine for most deployments. Use Gunicorn + Uvicorn workers if you need process management features like graceful restarts.

Q: How do I handle async database drivers? Use asyncpg for PostgreSQL and aioredis for Redis. They require psycopg[binary] and redis[hiredis] in requirements.

References


Advertisement