Back to Blog
FastAPI
Docker
Deployment
DevOps
Production

FastAPI Docker Deployment: Production Guide with Multi-Stage Builds

Complete guide to deploying FastAPI with Docker in production. Learn multi-stage builds, Docker Compose, environment management, and optimization techniques for scalable deployments.

FastLaunchAPI Team
8 min read

FastAPI Docker Deployment: Production Guide with Multi-Stage Builds

Deploying FastAPI to production requires more than just docker run. You need optimized images, proper environment management, health checks, and a deployment strategy that scales.

This guide covers everything from basic Dockerization to production-ready multi-stage builds.

Why Docker for FastAPI?

Docker solves the "it works on my machine" problem and provides:

Basic FastAPI Dockerfile

Let's start with a simple Dockerfile:

FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

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

Problems with this approach:

Production Multi-Stage Dockerfile

Here's a production-ready multi-stage build:

# Stage 1: Build stage
FROM python:3.12-slim as builder

WORKDIR /app

# Install build dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc \
    postgresql-dev \
    && rm -rf /var/lib/apt/lists/*

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

# Stage 2: Runtime stage
FROM python:3.12-slim

WORKDIR /app

# Create non-root user
RUN useradd -m -u 1000 appuser && \
    chown -R appuser:appuser /app

# Install runtime dependencies only
RUN apt-get update && apt-get install -y --no-install-recommends \
    postgresql-client \
    curl \
    && rm -rf /var/lib/apt/lists/*

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

# Copy application code
COPY --chown=appuser:appuser . .

# Switch to non-root user
USER appuser

# Add local bin to PATH
ENV PATH=/home/appuser/.local/bin:$PATH

# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:8000/health || exit 1

# Expose port
EXPOSE 8000

# Run with production settings
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]

Benefits:

Docker Compose for Development

Create a docker-compose.yml for local development:

version: "3.8"

services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://user:password@db:5432/fastapi_db
      - REDIS_URL=redis://redis:6379/0
      - ENVIRONMENT=development
    volumes:
      - ./app:/app/app # Hot reload
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload

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

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    command: redis-server --appendonly yes

  celery_worker:
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      - DATABASE_URL=postgresql://user:password@db:5432/fastapi_db
      - REDIS_URL=redis://redis:6379/0
    depends_on:
      - redis
      - db
    command: celery -A app.celery_app worker --loglevel=info

volumes:
  postgres_data:
  redis_data:

Production Docker Compose

For production, use environment files:

# docker-compose.prod.yml
version: "3.8"

services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
      target: runtime
    restart: always
    ports:
      - "8000:8000"
    env_file:
      - .env.production
    depends_on:
      - db
      - redis
    networks:
      - fastapi_network
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: "1"
          memory: 512M
        reservations:
          cpus: "0.5"
          memory: 256M

  db:
    image: postgres:15-alpine
    restart: always
    env_file:
      - .env.production
    volumes:
      - postgres_prod:/var/lib/postgresql/data
    networks:
      - fastapi_network
    deploy:
      resources:
        limits:
          cpus: "2"
          memory: 2G

  redis:
    image: redis:7-alpine
    restart: always
    volumes:
      - redis_prod:/data
    networks:
      - fastapi_network

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - api
    networks:
      - fastapi_network

volumes:
  postgres_prod:
  redis_prod:

networks:
  fastapi_network:
    driver: bridge

Environment Management

Create a .env.example file:

# Database
DATABASE_URL=postgresql://user:password@db:5432/fastapi_db
DATABASE_POOL_SIZE=20
DATABASE_MAX_OVERFLOW=0

# Redis
REDIS_URL=redis://redis:6379/0

# Application
SECRET_KEY=your-secret-key-here
ENVIRONMENT=production
DEBUG=false
ALLOWED_HOSTS=["yourdomain.com"]

# Stripe
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...

# Email
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
[email protected]
SMTP_PASSWORD=your-app-password

# Sentry
SENTRY_DSN=https://[email protected]/...

# Monitoring
LOG_LEVEL=INFO

Optimization Techniques

1. Use .dockerignore

Create a .dockerignore file:

# Python
__pycache__
*.pyc
*.pyo
*.pyd
.Python
*.so
*.egg
*.egg-info
dist
build

# Virtual environments
venv/
env/
.venv

# IDE
.vscode/
.idea/
*.swp
*.swo

# Testing
.pytest_cache/
.coverage
htmlcov/

# Git
.git/
.gitignore

# Docker
Dockerfile*
docker-compose*.yml
.dockerignore

# Documentation
README.md
docs/

# CI/CD
.github/
.gitlab-ci.yml

2. Layer Caching

Order Dockerfile commands from least to most frequently changed:

# ✅ Good: Dependencies change less often than code
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

# ❌ Bad: Rebuilds dependencies every time
COPY . .
RUN pip install -r requirements.txt

3. Use Smaller Base Images

# ❌ Large: 1.1GB
FROM python:3.12

# ✅ Medium: 500MB
FROM python:3.12-slim

# ✅ Smallest: 120MB (Alpine)
FROM python:3.12-alpine

Note: Alpine can have compatibility issues with some Python packages.

4. Multi-Platform Builds

Build for multiple architectures:

docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest .

Deployment Strategies

AWS ECS

{
  "family": "fastapi-task",
  "containerDefinitions": [
    {
      "name": "fastapi",
      "image": "your-registry/fastapi:latest",
      "memory": 512,
      "cpu": 256,
      "essential": true,
      "portMappings": [
        {
          "containerPort": 8000,
          "protocol": "tcp"
        }
      ],
      "environment": [
        {
          "name": "ENVIRONMENT",
          "value": "production"
        }
      ],
      "secrets": [
        {
          "name": "DATABASE_URL",
          "valueFrom": "arn:aws:secretsmanager:..."
        }
      ],
      "healthCheck": {
        "command": [
          "CMD-SHELL",
          "curl -f http://localhost:8000/health || exit 1"
        ],
        "interval": 30,
        "timeout": 5,
        "retries": 3
      }
    }
  ]
}

Google Cloud Run

# Build and push
docker build -t gcr.io/PROJECT_ID/fastapi:latest .
docker push gcr.io/PROJECT_ID/fastapi:latest

# Deploy
gcloud run deploy fastapi \
  --image gcr.io/PROJECT_ID/fastapi:latest \
  --platform managed \
  --region us-central1 \
  --allow-unauthenticated \
  --set-env-vars "ENVIRONMENT=production" \
  --max-instances 10

DigitalOcean App Platform

Create an app.yaml:

name: fastapi-app
services:
  - name: api
    dockerfile_path: Dockerfile
    github:
      repo: username/fastapi-app
      branch: main
      deploy_on_push: true
    http_port: 8000
    instance_count: 2
    instance_size_slug: basic-xxs
    envs:
      - key: DATABASE_URL
        scope: RUN_TIME
        type: SECRET
      - key: ENVIRONMENT
        value: production
    health_check:
      http_path: /health

databases:
  - name: db
    engine: PG
    version: "15"

Health Checks

Add a health endpoint to your FastAPI app:

from fastapi import FastAPI, status
from fastapi.responses import JSONResponse
import asyncpg
import redis.asyncio as redis

app = FastAPI()

@app.get("/health", status_code=status.HTTP_200_OK)
async def health_check():
    health_status = {
        "status": "healthy",
        "database": "unknown",
        "redis": "unknown"
    }

    # Check database
    try:
        conn = await asyncpg.connect(DATABASE_URL)
        await conn.execute("SELECT 1")
        await conn.close()
        health_status["database"] = "healthy"
    except Exception as e:
        health_status["database"] = f"unhealthy: {str(e)}"
        health_status["status"] = "degraded"

    # Check Redis
    try:
        r = redis.from_url(REDIS_URL)
        await r.ping()
        await r.close()
        health_status["redis"] = "healthy"
    except Exception as e:
        health_status["redis"] = f"unhealthy: {str(e)}"
        health_status["status"] = "degraded"

    if health_status["status"] == "degraded":
        return JSONResponse(
            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
            content=health_status
        )

    return health_status

Security Best Practices

1. Non-Root User

Always run containers as non-root:

RUN useradd -m -u 1000 appuser
USER appuser

2. Secret Management

Never hardcode secrets:

# ❌ Bad
ENV DATABASE_URL=postgresql://user:password@db/mydb

# ✅ Good: Use secrets
docker run --env-file .env.production myapp

3. Scan for Vulnerabilities

# Scan image
docker scan myapp:latest

# Use Trivy
trivy image myapp:latest

4. Minimal Base Images

Use distroless images for maximum security:

FROM gcr.io/distroless/python3-debian11
COPY --from=builder /app /app
CMD ["python", "main.py"]

Monitoring and Logging

Add Structured Logging

import logging
import json
from datetime import datetime

class JSONFormatter(logging.Formatter):
    def format(self, record):
        log_data = {
            "timestamp": datetime.utcnow().isoformat(),
            "level": record.levelname,
            "message": record.getMessage(),
            "module": record.module,
            "function": record.funcName,
        }
        return json.dumps(log_data)

# Configure logging
handler = logging.StreamHandler()
handler.setFormatter(JSONFormatter())
logging.basicConfig(handlers=[handler], level=logging.INFO)

Export Logs

# Docker logs to file
docker-compose logs -f api > logs/api.log

# Send to CloudWatch
docker run \
  --log-driver=awslogs \
  --log-opt awslogs-group=fastapi-logs \
  myapp:latest

Common Issues and Solutions

Issue 1: Large Image Size

Solution: Use multi-stage builds and .dockerignore

FROM python:3.12-slim as builder
# Install dependencies
FROM python:3.12-slim as runtime
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages

Issue 2: Slow Builds

Solution: Optimize layer caching

# Cache dependencies separately
COPY requirements.txt .
RUN pip install -r requirements.txt
# Code changes don't rebuild dependencies
COPY . .

Issue 3: Database Connection Failures

Solution: Add retry logic and health checks

from tenacity import retry, stop_after_attempt, wait_exponential

@retry(
    stop=stop_after_attempt(5),
    wait=wait_exponential(multiplier=1, min=4, max=10)
)
async def connect_db():
    return await asyncpg.connect(DATABASE_URL)

Quick Reference Commands

# Build image
docker build -t fastapi-app:latest .

# Run container
docker run -p 8000:8000 fastapi-app:latest

# Build with compose
docker-compose build

# Start services
docker-compose up -d

# View logs
docker-compose logs -f api

# Stop services
docker-compose down

# Remove volumes
docker-compose down -v

# Rebuild without cache
docker-compose build --no-cache

# Scale service
docker-compose up -d --scale api=3

# Execute command in container
docker-compose exec api bash

# Check resource usage
docker stats

Conclusion

Deploying FastAPI with Docker requires careful consideration of image size, security, performance, and maintainability. Multi-stage builds, proper environment management, and health checks are essential for production deployments.

Key Takeaways:


Want a production-ready FastAPI template with Docker already configured? Get our template with multi-stage Dockerfile, Docker Compose, and deployment scripts included. See what's included →

Related Articles