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.
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:
- Consistency: Same environment in dev, staging, and production
- Isolation: Dependencies don't conflict with system packages
- Scalability: Easy horizontal scaling with orchestrators
- Portability: Deploy anywhere that runs Docker
- Reproducibility: Infrastructure as code
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:
- ❌ Large image size (~1GB+)
- ❌ Installs dev dependencies
- ❌ No optimization
- ❌ Security vulnerabilities
- ❌ No health checks
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:
- ✅ 60% smaller image size
- ✅ Runs as non-root user (security)
- ✅ No build tools in final image
- ✅ Health checks included
- ✅ Production-optimized
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:
- ✅ Use multi-stage builds to reduce image size by 60%+
- ✅ Always run as non-root user for security
- ✅ Implement health checks for monitoring
- ✅ Use Docker Compose for local development
- ✅ Layer caching speeds up builds significantly
- ✅ Proper logging and monitoring are critical
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
FastAPI vs Flask for SaaS: Which Framework Wins in 2026?
Comprehensive comparison of FastAPI and Flask for building SaaS applications. Performance benchmarks, feature analysis, and real-world insights to help you choose the right Python framework.
FastAPI vs Django REST Framework: Complete 2026 Comparison Guide
Detailed comparison of FastAPI vs Django REST Framework for building APIs in 2026. Performance benchmarks, features, use cases, and migration guide included.
FastAPI Template with Stripe: Build a Payment-Ready API in 10 Minutes
Learn how to integrate Stripe payments into your FastAPI application using a production-ready template. Complete guide with authentication, webhooks, and subscription management.