Skip to main content

Nginx Reverse Proxy

Running FastAPI directly on port 80/443 is not recommended in production. Nginx in front of Gunicorn/Uvicorn handles SSL termination, connection management, static file serving, and provides a mature layer for rate limiting and caching.

Learning Focus

By the end of this lesson you can: configure Nginx as a reverse proxy for FastAPI, set up SSL with Let's Encrypt, serve static files from Nginx, and pass proxy headers correctly.

Basic Nginx Configuration

/etc/nginx/sites-available/api.example.com
upstream fastapi {
server 127.0.0.1:8000;
keepalive 64;
}

server {
listen 80;
server_name api.example.com;
return 301 https://$host$request_uri;
}

server {
listen 443 ssl http2;
server_name api.example.com;

# SSL certificates (managed by Certbot)
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;

# Security headers
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";

# Proxy to FastAPI
location / {
proxy_pass http://fastapi;
proxy_http_version 1.1;
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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 90s;
proxy_buffering off;
}

# Serve static files directly from Nginx (faster than Python)
location /static/ {
alias /var/app/static/;
expires 7d;
add_header Cache-Control "public, immutable";
}

# API docs only in staging/dev
location ~ ^/(docs|redoc|openapi.json) {
proxy_pass http://fastapi;
proxy_set_header Host $host;
}
}

Load Balancing Multiple Workers

/etc/nginx/sites-available/api-lb.conf
upstream fastapi_workers {
least_conn; # Route to least-busy worker
server 127.0.0.1:8001;
server 127.0.0.1:8002;
server 127.0.0.1:8003;
server 127.0.0.1:8004;
keepalive 32;
}

Rate Limiting with Nginx

/etc/nginx/nginx.conf
http {
# Define rate limit zone: 10 requests/second per IP, 10MB state storage
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

server {
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
limit_req_status 429;
proxy_pass http://fastapi;
}
}
}

Nginx + Docker Compose

docker-compose.yml (with nginx)
services:
nginx:
image: nginx:1.25-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
- static_files:/var/app/static:ro
depends_on:
- app
restart: unless-stopped

app:
build: .
expose:
- "8000" # Only expose to internal Docker network
restart: unless-stopped

Let's Encrypt with Certbot

setup-ssl.sh
# Install Certbot
sudo apt install certbot python3-certbot-nginx

# Get certificate
sudo certbot --nginx -d api.example.com

# Auto-renewal
sudo crontab -e
# Add: 0 12 * * * /usr/bin/certbot renew --quiet

Common Pitfalls

PitfallCause / SymptomFix
WebSocket 502Missing Upgrade headerAdd proxy_set_header Upgrade $http_upgrade
Real IP shows 127.0.0.1Missing X-Forwarded-For passthroughAdd all proxy header directives; trust headers in FastAPI
Large uploads rejectedclient_max_body_size too smallAdd client_max_body_size 50m; in Nginx config
SSL redirect loopFastAPI redirecting http→https inside NginxTrust X-Forwarded-Proto and don't redirect in FastAPI
Docs blocked in production/docs blocked by NginxUse IP allowlisting for docs in production

Hands-On Practice

test-nginx.sh
# Test Nginx config
nginx -t

# Reload without downtime
nginx -s reload

# Test proxy headers
curl -v https://api.example.com/health 2>&1 | grep -E "< |X-"

# Test SSL grade
# Visit: https://www.ssllabs.com/ssltest/analyze.html?d=api.example.com

What's Next