Deploy FastAPI applications with Docker Compose and production optimization

Intermediate 45 min Apr 29, 2026 263 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Learn to containerize FastAPI applications with Docker Compose, integrate PostgreSQL and Redis services, configure Nginx reverse proxy with SSL, and implement production monitoring for scalable microservices deployment.

Prerequisites

  • Basic Docker knowledge
  • Understanding of Python web applications
  • Familiarity with reverse proxies
  • Basic SQL database concepts

What this solves

FastAPI applications require proper containerization and orchestration for production deployment. This tutorial shows you how to create a complete Docker Compose stack with FastAPI, PostgreSQL database, Redis cache, and Nginx reverse proxy with SSL certificates for a production-ready microservices architecture.

Step-by-step installation

Install Docker and Docker Compose

First, install Docker Engine and Docker Compose on your system.

sudo apt update
sudo apt install -y apt-transport-https ca-certificates curl gnupg lsb-release
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo usermod -aG docker $USER
newgrp docker
sudo dnf update -y
sudo dnf install -y dnf-utils
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
newgrp docker

Create project structure

Set up the directory structure for your FastAPI application with all necessary files.

mkdir fastapi-docker && cd fastapi-docker
mkdir app nginx
touch docker-compose.yml .env
touch app/main.py app/requirements.txt app/Dockerfile
touch nginx/nginx.conf

Create FastAPI application

Build a sample FastAPI application with database and Redis connections.

from fastapi import FastAPI, HTTPException, Depends
from sqlalchemy import create_engine, Column, Integer, String, DateTime
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
from datetime import datetime
import redis
import os
import json
from typing import Optional

app = FastAPI(title="FastAPI Docker Demo", version="1.0.0")

Database configuration

DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://fastapi:password@postgres:5432/fastapi_db") engine = create_engine(DATABASE_URL) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base()

Redis configuration

redis_client = redis.Redis( host=os.getenv("REDIS_HOST", "redis"), port=int(os.getenv("REDIS_PORT", "6379")), decode_responses=True )

Database model

class Item(Base): __tablename__ = "items" id = Column(Integer, primary_key=True, index=True) name = Column(String, index=True) description = Column(String) created_at = Column(DateTime, default=datetime.utcnow) Base.metadata.create_all(bind=engine)

Dependency

def get_db(): db = SessionLocal() try: yield db finally: db.close() @app.get("/") async def root(): return {"message": "FastAPI with Docker Compose", "status": "healthy"} @app.get("/health") async def health_check(): # Test database connection try: db = next(get_db()) db.execute("SELECT 1") db_status = "healthy" except Exception as e: db_status = f"unhealthy: {str(e)}" # Test Redis connection try: redis_client.ping() redis_status = "healthy" except Exception as e: redis_status = f"unhealthy: {str(e)}" return { "status": "healthy", "database": db_status, "redis": redis_status, "timestamp": datetime.utcnow().isoformat() } @app.post("/items") async def create_item(name: str, description: str, db: Session = Depends(get_db)): item = Item(name=name, description=description) db.add(item) db.commit() db.refresh(item) # Cache the item redis_client.setex( f"item:{item.id}", 3600, json.dumps({ "id": item.id, "name": item.name, "description": item.description, "created_at": item.created_at.isoformat() }) ) return {"id": item.id, "name": item.name, "description": item.description} @app.get("/items/{item_id}") async def get_item(item_id: int, db: Session = Depends(get_db)): # Try cache first cached_item = redis_client.get(f"item:{item_id}") if cached_item: return {"source": "cache", "item": json.loads(cached_item)} # Fallback to database item = db.query(Item).filter(Item.id == item_id).first() if not item: raise HTTPException(status_code=404, detail="Item not found") item_data = { "id": item.id, "name": item.name, "description": item.description, "created_at": item.created_at.isoformat() } # Cache for future requests redis_client.setex(f"item:{item_id}", 3600, json.dumps(item_data)) return {"source": "database", "item": item_data}

Define Python dependencies

Create requirements file with all necessary packages for the FastAPI application.

fastapi==0.104.1
uvicorn[standard]==0.24.0
psycopg2-binary==2.9.9
sqlalchemy==2.0.23
redis==5.0.1
python-multipart==0.0.6

Create FastAPI Dockerfile

Build a production-optimized Docker image for the FastAPI application.

FROM python:3.11-slim

Set environment variables

ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 ENV PATH="/home/appuser/.local/bin:${PATH}"

Create non-root user

RUN groupadd -r appuser && useradd -r -g appuser appuser

Install system dependencies

RUN apt-get update \ && apt-get install -y --no-install-recommends \ gcc \ libpq-dev \ && rm -rf /var/lib/apt/lists/*

Set work directory

WORKDIR /app

Install Python dependencies

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

Copy application code

COPY . .

Change ownership to non-root user

RUN chown -R appuser:appuser /app USER appuser

Health check

HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ CMD python -c "import requests; requests.get('http://localhost:8000/health', timeout=10)"

Run the application

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

Configure environment variables

Set up environment variables for database credentials and application configuration.

# Database configuration
POSTGRES_DB=fastapi_db
POSTGRES_USER=fastapi
POSTGRES_PASSWORD=secure_password_123
DATABASE_URL=postgresql://fastapi:secure_password_123@postgres:5432/fastapi_db

Redis configuration

REDIS_HOST=redis REDIS_PORT=6379

Application configuration

FASTAPI_ENV=production WORKERS=4
Security: Change the default password to a strong, unique password before deploying to production. Store sensitive credentials in a proper secrets management system.

Configure Nginx reverse proxy

Set up Nginx as a reverse proxy with SSL support and security headers.

events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    
    # Security headers
    add_header X-Frame-Options DENY always;
    add_header X-Content-Type-Options nosniff always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    
    # Gzip compression
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
    
    # Rate limiting
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
    
    # Upstream backend
    upstream fastapi_backend {
        least_conn;
        server app:8000 max_fails=3 fail_timeout=30s;
    }
    
    server {
        listen 80;
        server_name example.com www.example.com;
        
        # Health check endpoint
        location /nginx-health {
            access_log off;
            return 200 "healthy";
            add_header Content-Type text/plain;
        }
        
        # API endpoints with rate limiting
        location /api/ {
            limit_req zone=api burst=20 nodelay;
            
            proxy_pass http://fastapi_backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            
            # Timeouts
            proxy_connect_timeout 30s;
            proxy_send_timeout 30s;
            proxy_read_timeout 30s;
        }
        
        # All other requests
        location / {
            proxy_pass http://fastapi_backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            
            # Timeouts
            proxy_connect_timeout 30s;
            proxy_send_timeout 30s;
            proxy_read_timeout 30s;
        }
    }
}

Create Docker Compose configuration

Define the complete stack with FastAPI, PostgreSQL, Redis, and Nginx services.

version: '3.8'

services:
  app:
    build: ./app
    container_name: fastapi-app
    restart: unless-stopped
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - REDIS_HOST=${REDIS_HOST}
      - REDIS_PORT=${REDIS_PORT}
    depends_on:
      - postgres
      - redis
    networks:
      - app-network
    healthcheck:
      test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:8000/health', timeout=10)"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  postgres:
    image: postgres:16-alpine
    container_name: fastapi-postgres
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - app-network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 30s
      timeout: 10s
      retries: 3

  redis:
    image: redis:7-alpine
    container_name: fastapi-redis
    restart: unless-stopped
    command: redis-server --appendonly yes --requirepass redis_password_123
    volumes:
      - redis_data:/data
    networks:
      - app-network
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "redis_password_123", "ping"]
      interval: 30s
      timeout: 10s
      retries: 3

  nginx:
    image: nginx:1.25-alpine
    container_name: fastapi-nginx
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
    depends_on:
      - app
    networks:
      - app-network
    healthcheck:
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost/nginx-health"]
      interval: 30s
      timeout: 10s
      retries: 3

networks:
  app-network:
    driver: bridge

volumes:
  postgres_data:
    driver: local
  redis_data:
    driver: local

Deploy the application stack

Start all services using Docker Compose and verify they're running correctly.

docker compose up -d
docker compose ps
docker compose logs -f

Configure production resource limits

Add resource constraints to prevent any single service from overwhelming the system. This builds on container resource management techniques.

version: '3.8'

services:
  app:
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 1G
        reservations:
          cpus: '0.5'
          memory: 512M
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  postgres:
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  redis:
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 256M
        reservations:
          cpus: '0.1'
          memory: 128M
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  nginx:
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 256M
        reservations:
          cpus: '0.1'
          memory: 128M
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

Set up SSL certificates

Generate SSL certificates for HTTPS support in production environments.

mkdir -p nginx/ssl

For development - self-signed certificate

openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout nginx/ssl/server.key \ -out nginx/ssl/server.crt \ -subj "/C=US/ST=State/L=City/O=Organization/CN=example.com"

Set proper permissions

chmod 600 nginx/ssl/server.key chmod 644 nginx/ssl/server.crt
Never use chmod 777. SSL private keys must be readable only by the owner. Use chmod 600 for private keys and proper container user mapping for production deployments.

Update Nginx for SSL

Add SSL configuration to the Nginx reverse proxy for secure connections.

events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    
    # Rate limiting
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
    
    # Upstream backend
    upstream fastapi_backend {
        least_conn;
        server app:8000 max_fails=3 fail_timeout=30s;
    }
    
    # Redirect HTTP to HTTPS
    server {
        listen 80;
        server_name example.com www.example.com;
        return 301 https://$server_name$request_uri;
    }
    
    # HTTPS server
    server {
        listen 443 ssl http2;
        server_name example.com www.example.com;
        
        # SSL configuration
        ssl_certificate /etc/nginx/ssl/server.crt;
        ssl_certificate_key /etc/nginx/ssl/server.key;
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
        ssl_prefer_server_ciphers off;
        
        # Security headers
        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
        add_header X-Frame-Options DENY always;
        add_header X-Content-Type-Options nosniff always;
        
        # API endpoints
        location /api/ {
            limit_req zone=api burst=20 nodelay;
            proxy_pass http://fastapi_backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
        
        # All other requests
        location / {
            proxy_pass http://fastapi_backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}

Add monitoring and health checks

Implement comprehensive monitoring for all services in the stack.

version: '3.8'

services:
  prometheus:
    image: prom/prometheus:v2.45.0
    container_name: fastapi-prometheus
    restart: unless-stopped
    ports:
      - "9090:9090"
    volumes:
      - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--web.console.libraries=/etc/prometheus/console_libraries'
      - '--web.console.templates=/etc/prometheus/consoles'
      - '--storage.tsdb.retention.time=200h'
      - '--web.enable-lifecycle'
    networks:
      - app-network

  grafana:
    image: grafana/grafana:10.1.0
    container_name: fastapi-grafana
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin_password_123
    volumes:
      - grafana_data:/var/lib/grafana
      - ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards
      - ./monitoring/grafana/provisioning:/etc/grafana/provisioning
    networks:
      - app-network

volumes:
  prometheus_data:
    driver: local
  grafana_data:
    driver: local

Configure Prometheus monitoring

Set up Prometheus to collect metrics from all services in the stack.

mkdir -p monitoring/grafana/{dashboards,provisioning}
touch monitoring/prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

  - job_name: 'fastapi-app'
    static_configs:
      - targets: ['app:8000']
    metrics_path: '/metrics'
    scrape_interval: 10s

  - job_name: 'nginx'
    static_configs:
      - targets: ['nginx:80']
    metrics_path: '/nginx-health'
    scrape_interval: 15s

  - job_name: 'postgres'
    static_configs:
      - targets: ['postgres:5432']
    scrape_interval: 30s

  - job_name: 'redis'
    static_configs:
      - targets: ['redis:6379']
    scrape_interval: 30s

Verify your setup

Test all components of your FastAPI Docker Compose stack to ensure everything is working correctly.

# Check all containers are running
docker compose ps

Test the FastAPI application

curl http://localhost/ curl http://localhost/health

Test database functionality

curl -X POST "http://localhost/items?name=test&description=Test item" curl http://localhost/items/1

Check container logs

docker compose logs app docker compose logs postgres docker compose logs redis docker compose logs nginx

Monitor resource usage

docker stats

Test SSL if configured

curl -k https://localhost/health

Production deployment optimization

For production deployments, you'll want to implement additional security and performance optimizations. Consider using database connection pooling for better PostgreSQL performance and container security profiles for enhanced security.

Common issues

Symptom Cause Fix
App container fails to start Database connection timeout Add health checks and depends_on with condition
502 Bad Gateway from Nginx FastAPI container not ready Check docker compose logs app for startup errors
Permission denied on SSL files Incorrect file permissions Use chmod 600 for private keys, 644 for certificates
Database connection refused PostgreSQL not ready Wait for health checks or use connection retry logic
Redis connection errors Authentication or network issues Check Redis password configuration and network connectivity
High memory usage No resource limits configured Add deploy.resources limits in docker-compose.override.yml

Next steps

Running this in production?

Want this handled for you? Setting up Docker Compose once is straightforward. Keeping it patched, monitored, backed up and tuned across environments is the harder part. See how we run infrastructure like this for European SaaS and e-commerce teams.

Automated install script

Run this to automate the entire setup

Need help?

Don't want to manage this yourself?

We handle managed devops services for businesses that depend on uptime. From initial setup to ongoing operations.