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
| Pitfall | Cause / Symptom | Fix |
|---|---|---|
| WebSocket 502 | Missing Upgrade header | Add proxy_set_header Upgrade $http_upgrade |
| Real IP shows 127.0.0.1 | Missing X-Forwarded-For passthrough | Add all proxy header directives; trust headers in FastAPI |
| Large uploads rejected | client_max_body_size too small | Add client_max_body_size 50m; in Nginx config |
| SSL redirect loop | FastAPI redirecting http→https inside Nginx | Trust X-Forwarded-Proto and don't redirect in FastAPI |
| Docs blocked in production | /docs blocked by Nginx | Use 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