Docker Deployment

Difficulty: Advanced

Docker containers provide a consistent, reproducible environment for deploying FastAPI applications. A Docker image packages your application code, Python runtime, dependencies, and configuration into a single unit that runs identically on any machine. This eliminates the 'works on my machine' problem and simplifies deployment.

A Dockerfile defines how to build your Docker image. For FastAPI, the typical Dockerfile starts from a Python base image, copies your requirements, installs dependencies, copies your application code, and defines the command to run Uvicorn. Each instruction creates a layer, and Docker caches layers for faster rebuilds.

Multi-stage builds reduce the final image size by separating the build stage from the runtime stage. In the build stage, you install dependencies (which may require build tools). In the runtime stage, you copy only the installed packages and your code. This can reduce image size significantly.

Docker Compose simplifies multi-container setups. A typical FastAPI deployment includes the API container, a database container (PostgreSQL), and possibly a Redis container for caching. Docker Compose defines all services in a single YAML file and manages their networking and dependencies.

Best practices for Dockerizing FastAPI include: using a non-root user for security, using .dockerignore to exclude unnecessary files, pinning Python version and dependency versions, using health checks, and configuring logging to stdout/stderr for container log management.

Environment variables are the standard way to configure containers. Database URLs, secret keys, API keys, and feature flags should all be environment variables, not hardcoded. Docker Compose's env_file directive and Kubernetes ConfigMaps make this easy to manage.

Code examples

Basic Dockerfile

# Dockerfile
FROM python:3.12-slim

# Set working directory
WORKDIR /app

# Install dependencies first (cached layer)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy application code
COPY . .

# Create non-root user
RUN adduser --disabled-password --no-create-home appuser
USER appuser

# Expose port
EXPOSE 8000

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

# Build: docker build -t my-fastapi-app .
# Run:   docker run -p 8000:8000 my-fastapi-app

Start from python:3.12-slim (smaller than full image). Copy and install requirements first for layer caching. Copy code after dependencies so code changes do not re-install deps. Use a non-root user for security.

Multi-Stage Build

# Dockerfile with multi-stage build

# Stage 1: Build
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --target=/app/deps -r requirements.txt

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

# Copy only the installed packages
COPY --from=builder /app/deps /usr/local/lib/python3.12/site-packages/

# Copy application code
COPY . .

# Non-root user
RUN adduser --disabled-password --no-create-home appuser
USER appuser

EXPOSE 8000

# Health check
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
    CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]

Multi-stage builds use a builder stage for installing dependencies and a runtime stage with only the necessary files. The HEALTHCHECK ensures Docker knows if the container is healthy. Multiple Uvicorn workers handle concurrent requests.

Docker Compose for Full Stack

# docker-compose.yml
version: '3.8'

services:
  api:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://postgres:password@db:5432/myapp
      - SECRET_KEY=your-secret-key
      - ENVIRONMENT=production
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=myapp
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
    ports:
      - "5432:5432"

volumes:
  postgres_data:

# Commands:
# docker-compose up -d     # Start all services
# docker-compose logs api  # View API logs
# docker-compose down      # Stop all services

Docker Compose defines the API and database as linked services. The API waits for the database to be healthy before starting. Environment variables configure the connection. A named volume persists database data across restarts.

Key points

Concepts covered

Docker, Dockerfile, Multi-Stage Build, docker-compose, Container