709 lines
16 KiB
Markdown
709 lines
16 KiB
Markdown
# Production Deployment
|
|
|
|
This guide covers deploying the FastAPI boilerplate to production with proper performance, security, and reliability configurations.
|
|
|
|
## Production Architecture
|
|
|
|
The recommended production setup uses:
|
|
|
|
- **Gunicorn** - WSGI server managing Uvicorn workers
|
|
- **Uvicorn Workers** - ASGI server handling FastAPI requests
|
|
- **NGINX** - Reverse proxy and load balancer
|
|
- **PostgreSQL** - Production database
|
|
- **Redis** - Caching and background tasks
|
|
- **Docker** - Containerization
|
|
|
|
## Environment Configuration
|
|
|
|
### Production Environment Variables
|
|
|
|
Update your `.env` file for production:
|
|
|
|
```bash
|
|
# ------------- environment -------------
|
|
ENVIRONMENT="production"
|
|
|
|
# ------------- app settings -------------
|
|
APP_NAME="Your Production App"
|
|
DEBUG=false
|
|
|
|
# ------------- database -------------
|
|
POSTGRES_USER="prod_user"
|
|
POSTGRES_PASSWORD="secure_production_password"
|
|
POSTGRES_SERVER="db" # or your database host
|
|
POSTGRES_PORT=5432
|
|
POSTGRES_DB="prod_database"
|
|
|
|
# ------------- redis -------------
|
|
REDIS_CACHE_HOST="redis"
|
|
REDIS_CACHE_PORT=6379
|
|
REDIS_QUEUE_HOST="redis"
|
|
REDIS_QUEUE_PORT=6379
|
|
REDIS_RATE_LIMIT_HOST="redis"
|
|
REDIS_RATE_LIMIT_PORT=6379
|
|
|
|
# ------------- security -------------
|
|
SECRET_KEY="your-super-secure-secret-key-generate-with-openssl"
|
|
ALGORITHM="HS256"
|
|
ACCESS_TOKEN_EXPIRE_MINUTES=30
|
|
REFRESH_TOKEN_EXPIRE_DAYS=7
|
|
|
|
# ------------- logging -------------
|
|
LOG_LEVEL="INFO"
|
|
```
|
|
|
|
### Docker Configuration
|
|
|
|
#### Production Dockerfile
|
|
|
|
```dockerfile
|
|
FROM python:3.11-slim
|
|
|
|
WORKDIR /code
|
|
|
|
# Install system dependencies
|
|
RUN apt-get update && apt-get install -y \
|
|
gcc \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# Install UV
|
|
RUN pip install uv
|
|
|
|
# Copy dependency files
|
|
COPY pyproject.toml uv.lock ./
|
|
|
|
# Install dependencies
|
|
RUN uv sync --frozen --no-dev
|
|
|
|
# Copy application code
|
|
COPY src/ ./src/
|
|
|
|
# Create non-root user
|
|
RUN useradd --create-home --shell /bin/bash app \
|
|
&& chown -R app:app /code
|
|
USER app
|
|
|
|
# Production command with Gunicorn
|
|
CMD ["uv", "run", "gunicorn", "src.app.main:app", "-w", "4", "-k", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:8000"]
|
|
```
|
|
|
|
#### Production Docker Compose
|
|
|
|
```yaml
|
|
version: '3.8'
|
|
|
|
services:
|
|
web:
|
|
build: .
|
|
ports:
|
|
- "8000:8000"
|
|
env_file:
|
|
- ./src/.env
|
|
depends_on:
|
|
- db
|
|
- redis
|
|
restart: unless-stopped
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
memory: 1G
|
|
reservations:
|
|
memory: 512M
|
|
|
|
worker:
|
|
build: .
|
|
command: uv run arq src.app.core.worker.settings.WorkerSettings
|
|
env_file:
|
|
- ./src/.env
|
|
depends_on:
|
|
- db
|
|
- redis
|
|
restart: unless-stopped
|
|
deploy:
|
|
replicas: 2
|
|
|
|
db:
|
|
image: postgres:15
|
|
volumes:
|
|
- postgres_data:/var/lib/postgresql/data/
|
|
environment:
|
|
- POSTGRES_USER=${POSTGRES_USER}
|
|
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
|
- POSTGRES_DB=${POSTGRES_DB}
|
|
restart: unless-stopped
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
memory: 2G
|
|
reservations:
|
|
memory: 1G
|
|
|
|
redis:
|
|
image: redis:7-alpine
|
|
restart: unless-stopped
|
|
volumes:
|
|
- redis_data:/data
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
memory: 512M
|
|
reservations:
|
|
memory: 256M
|
|
|
|
nginx:
|
|
image: nginx:alpine
|
|
ports:
|
|
- "80:80"
|
|
- "443:443"
|
|
volumes:
|
|
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
|
|
- ./nginx/ssl:/etc/nginx/ssl
|
|
depends_on:
|
|
- web
|
|
restart: unless-stopped
|
|
|
|
volumes:
|
|
postgres_data:
|
|
redis_data:
|
|
```
|
|
|
|
## Gunicorn Configuration
|
|
|
|
### Basic Gunicorn Setup
|
|
|
|
Create `gunicorn.conf.py`:
|
|
|
|
```python
|
|
import multiprocessing
|
|
|
|
# Server socket
|
|
bind = "0.0.0.0:8000"
|
|
backlog = 2048
|
|
|
|
# Worker processes
|
|
workers = multiprocessing.cpu_count() * 2 + 1
|
|
worker_class = "uvicorn.workers.UvicornWorker"
|
|
worker_connections = 1000
|
|
max_requests = 1000
|
|
max_requests_jitter = 50
|
|
|
|
# Restart workers after this many requests, with up to 50 jitter
|
|
preload_app = True
|
|
|
|
# Logging
|
|
accesslog = "-"
|
|
errorlog = "-"
|
|
loglevel = "info"
|
|
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s'
|
|
|
|
# Process naming
|
|
proc_name = "fastapi-boilerplate"
|
|
|
|
# Server mechanics
|
|
daemon = False
|
|
pidfile = "/tmp/gunicorn.pid"
|
|
user = None
|
|
group = None
|
|
tmp_upload_dir = None
|
|
|
|
# SSL (if terminating SSL at application level)
|
|
# keyfile = "/path/to/keyfile"
|
|
# certfile = "/path/to/certfile"
|
|
|
|
# Worker timeout
|
|
timeout = 30
|
|
keepalive = 2
|
|
|
|
# Memory management
|
|
max_requests = 1000
|
|
max_requests_jitter = 50
|
|
preload_app = True
|
|
```
|
|
|
|
### Running with Gunicorn
|
|
|
|
```bash
|
|
# Basic command
|
|
uv run gunicorn src.app.main:app -w 4 -k uvicorn.workers.UvicornWorker
|
|
|
|
# With configuration file
|
|
uv run gunicorn src.app.main:app -c gunicorn.conf.py
|
|
|
|
# With specific bind address
|
|
uv run gunicorn src.app.main:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000
|
|
```
|
|
|
|
## NGINX Configuration
|
|
|
|
### Single Server Setup
|
|
|
|
Create `nginx/nginx.conf`:
|
|
|
|
```nginx
|
|
events {
|
|
worker_connections 1024;
|
|
}
|
|
|
|
http {
|
|
upstream fastapi_backend {
|
|
server web:8000;
|
|
}
|
|
|
|
server {
|
|
listen 80;
|
|
server_name your-domain.com;
|
|
|
|
# Redirect HTTP to HTTPS
|
|
return 301 https://$server_name$request_uri;
|
|
}
|
|
|
|
server {
|
|
listen 443 ssl http2;
|
|
server_name your-domain.com;
|
|
|
|
# SSL Configuration
|
|
ssl_certificate /etc/nginx/ssl/cert.pem;
|
|
ssl_certificate_key /etc/nginx/ssl/key.pem;
|
|
ssl_protocols TLSv1.2 TLSv1.3;
|
|
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384;
|
|
ssl_prefer_server_ciphers off;
|
|
|
|
# Security headers
|
|
add_header X-Frame-Options DENY;
|
|
add_header X-Content-Type-Options nosniff;
|
|
add_header X-XSS-Protection "1; mode=block";
|
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
|
|
|
# Gzip compression
|
|
gzip on;
|
|
gzip_vary on;
|
|
gzip_min_length 10240;
|
|
gzip_proxied expired no-cache no-store private must-revalidate auth;
|
|
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;
|
|
|
|
location / {
|
|
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 60s;
|
|
proxy_send_timeout 60s;
|
|
proxy_read_timeout 60s;
|
|
|
|
# Buffer settings
|
|
proxy_buffering on;
|
|
proxy_buffer_size 8k;
|
|
proxy_buffers 8 8k;
|
|
}
|
|
|
|
# Health check endpoint (no rate limiting)
|
|
location /health {
|
|
proxy_pass http://fastapi_backend;
|
|
proxy_set_header Host $host;
|
|
access_log off;
|
|
}
|
|
|
|
# Static files (if any)
|
|
location /static/ {
|
|
alias /code/static/;
|
|
expires 1y;
|
|
add_header Cache-Control "public, immutable";
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Simple Single Server (default.conf)
|
|
|
|
For basic production setup, create `default.conf`:
|
|
|
|
```nginx
|
|
# ---------------- Running With One Server ----------------
|
|
server {
|
|
listen 80;
|
|
|
|
location / {
|
|
proxy_pass http://web:8000;
|
|
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;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Load Balancing Multiple Servers
|
|
|
|
For horizontal scaling with multiple FastAPI instances:
|
|
|
|
```nginx
|
|
# ---------------- To Run with Multiple Servers ----------------
|
|
upstream fastapi_app {
|
|
server fastapi1:8000; # Replace with actual server names
|
|
server fastapi2:8000;
|
|
# Add more servers as needed
|
|
}
|
|
|
|
server {
|
|
listen 80;
|
|
|
|
location / {
|
|
proxy_pass http://fastapi_app;
|
|
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;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Advanced Load Balancing
|
|
|
|
For production with advanced features:
|
|
|
|
```nginx
|
|
upstream fastapi_backend {
|
|
least_conn;
|
|
server web1:8000 weight=3;
|
|
server web2:8000 weight=2;
|
|
server web3:8000 weight=1;
|
|
|
|
# Health checks
|
|
keepalive 32;
|
|
}
|
|
|
|
server {
|
|
listen 443 ssl http2;
|
|
server_name your-domain.com;
|
|
|
|
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;
|
|
|
|
# Connection settings for load balancing
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Connection "";
|
|
}
|
|
}
|
|
```
|
|
|
|
### SSL Certificate Setup
|
|
|
|
#### Using Let's Encrypt (Certbot)
|
|
|
|
```bash
|
|
# Install certbot
|
|
sudo apt-get update
|
|
sudo apt-get install certbot python3-certbot-nginx
|
|
|
|
# Obtain certificate
|
|
sudo certbot --nginx -d your-domain.com
|
|
|
|
# Auto-renewal (add to crontab)
|
|
0 2 * * 1 /usr/bin/certbot renew --quiet
|
|
```
|
|
|
|
#### Manual SSL Setup
|
|
|
|
```bash
|
|
# Generate self-signed certificate (development only)
|
|
mkdir -p nginx/ssl
|
|
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
|
-keyout nginx/ssl/key.pem \
|
|
-out nginx/ssl/cert.pem
|
|
```
|
|
|
|
## Production Best Practices
|
|
|
|
### Database Optimization
|
|
|
|
#### PostgreSQL Configuration
|
|
|
|
```sql
|
|
-- Optimize PostgreSQL for production
|
|
ALTER SYSTEM SET shared_buffers = '256MB';
|
|
ALTER SYSTEM SET effective_cache_size = '1GB';
|
|
ALTER SYSTEM SET random_page_cost = 1.1;
|
|
ALTER SYSTEM SET effective_io_concurrency = 200;
|
|
SELECT pg_reload_conf();
|
|
```
|
|
|
|
#### Connection Pooling
|
|
|
|
```python
|
|
# src/app/core/db/database.py
|
|
from sqlalchemy.ext.asyncio import create_async_engine
|
|
|
|
# Production database settings
|
|
engine = create_async_engine(
|
|
DATABASE_URL,
|
|
echo=False, # Disable in production
|
|
pool_size=20,
|
|
max_overflow=0,
|
|
pool_pre_ping=True,
|
|
pool_recycle=3600,
|
|
)
|
|
```
|
|
|
|
### Redis Configuration
|
|
|
|
#### Redis Production Settings
|
|
|
|
```bash
|
|
# redis.conf adjustments
|
|
maxmemory 512mb
|
|
maxmemory-policy allkeys-lru
|
|
save 900 1
|
|
save 300 10
|
|
save 60 10000
|
|
```
|
|
|
|
### Application Optimization
|
|
|
|
#### Logging Configuration
|
|
|
|
```python
|
|
# src/app/core/config.py
|
|
import logging
|
|
from pythonjsonlogger import jsonlogger
|
|
|
|
def setup_production_logging():
|
|
logHandler = logging.StreamHandler()
|
|
formatter = jsonlogger.JsonFormatter(
|
|
"%(asctime)s %(name)s %(levelname)s %(message)s"
|
|
)
|
|
logHandler.setFormatter(formatter)
|
|
|
|
logger = logging.getLogger()
|
|
logger.addHandler(logHandler)
|
|
logger.setLevel(logging.INFO)
|
|
|
|
# Reduce noise from third-party libraries
|
|
logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
|
|
logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)
|
|
```
|
|
|
|
#### Performance Monitoring
|
|
|
|
```python
|
|
# src/app/middleware/monitoring.py
|
|
import time
|
|
from fastapi import Request
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
|
|
class MonitoringMiddleware(BaseHTTPMiddleware):
|
|
async def dispatch(self, request: Request, call_next):
|
|
start_time = time.time()
|
|
|
|
response = await call_next(request)
|
|
|
|
process_time = time.time() - start_time
|
|
response.headers["X-Process-Time"] = str(process_time)
|
|
|
|
# Log slow requests
|
|
if process_time > 1.0:
|
|
logger.warning(f"Slow request: {request.method} {request.url} - {process_time:.2f}s")
|
|
|
|
return response
|
|
```
|
|
|
|
### Security Configuration
|
|
|
|
#### Environment Security
|
|
|
|
```python
|
|
# src/app/core/config.py
|
|
class ProductionSettings(Settings):
|
|
# Hide docs in production
|
|
ENVIRONMENT: str = "production"
|
|
|
|
# Security settings
|
|
SECRET_KEY: str = Field(..., min_length=32)
|
|
ALLOWED_HOSTS: list[str] = ["your-domain.com", "api.your-domain.com"]
|
|
|
|
# Database security
|
|
POSTGRES_PASSWORD: str = Field(..., min_length=16)
|
|
|
|
class Config:
|
|
case_sensitive = True
|
|
```
|
|
|
|
#### Rate Limiting
|
|
|
|
```python
|
|
# Adjust rate limits for production
|
|
DEFAULT_RATE_LIMIT_LIMIT = 100 # requests per period
|
|
DEFAULT_RATE_LIMIT_PERIOD = 3600 # 1 hour
|
|
```
|
|
|
|
### Health Checks
|
|
|
|
#### Application Health Check
|
|
|
|
```python
|
|
# src/app/api/v1/health.py
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from ...core.db.database import async_get_db
|
|
from ...core.utils.cache import redis_client
|
|
|
|
router = APIRouter()
|
|
|
|
@router.get("/health")
|
|
async def health_check():
|
|
return {"status": "healthy", "timestamp": datetime.utcnow()}
|
|
|
|
@router.get("/health/detailed")
|
|
async def detailed_health_check(db: AsyncSession = Depends(async_get_db)):
|
|
health_status = {"status": "healthy", "services": {}}
|
|
|
|
# Check database
|
|
try:
|
|
await db.execute("SELECT 1")
|
|
health_status["services"]["database"] = "healthy"
|
|
except Exception:
|
|
health_status["services"]["database"] = "unhealthy"
|
|
health_status["status"] = "unhealthy"
|
|
|
|
# Check Redis
|
|
try:
|
|
await redis_client.ping()
|
|
health_status["services"]["redis"] = "healthy"
|
|
except Exception:
|
|
health_status["services"]["redis"] = "unhealthy"
|
|
health_status["status"] = "unhealthy"
|
|
|
|
if health_status["status"] == "unhealthy":
|
|
raise HTTPException(status_code=503, detail=health_status)
|
|
|
|
return health_status
|
|
```
|
|
|
|
### Deployment Process
|
|
|
|
#### CI/CD Pipeline (GitHub Actions)
|
|
|
|
```yaml
|
|
# .github/workflows/deploy.yml
|
|
name: Deploy to Production
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
|
|
jobs:
|
|
deploy:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v3
|
|
|
|
- name: Build and push Docker image
|
|
env:
|
|
DOCKER_REGISTRY: your-registry.com
|
|
run: |
|
|
docker build -t $DOCKER_REGISTRY/fastapi-app:latest .
|
|
docker push $DOCKER_REGISTRY/fastapi-app:latest
|
|
|
|
- name: Deploy to production
|
|
run: |
|
|
# Your deployment commands
|
|
ssh production-server "docker compose pull && docker compose up -d"
|
|
```
|
|
|
|
#### Zero-Downtime Deployment
|
|
|
|
```bash
|
|
#!/bin/bash
|
|
# deploy.sh - Zero-downtime deployment script
|
|
|
|
# Pull new images
|
|
docker compose pull
|
|
|
|
# Start new containers
|
|
docker compose up -d --no-deps --scale web=2 web
|
|
|
|
# Wait for health check
|
|
sleep 30
|
|
|
|
# Stop old containers
|
|
docker compose up -d --no-deps --scale web=1 web
|
|
|
|
# Clean up
|
|
docker system prune -f
|
|
```
|
|
|
|
### Monitoring and Alerting
|
|
|
|
#### Basic Monitoring Setup
|
|
|
|
```python
|
|
# Basic metrics collection
|
|
import psutil
|
|
from fastapi import APIRouter
|
|
|
|
router = APIRouter()
|
|
|
|
@router.get("/metrics")
|
|
async def get_metrics():
|
|
return {
|
|
"cpu_percent": psutil.cpu_percent(),
|
|
"memory_percent": psutil.virtual_memory().percent,
|
|
"disk_usage": psutil.disk_usage('/').percent
|
|
}
|
|
```
|
|
|
|
### Backup Strategy
|
|
|
|
#### Database Backup
|
|
|
|
```bash
|
|
#!/bin/bash
|
|
# backup-db.sh
|
|
BACKUP_DIR="/backups"
|
|
DATE=$(date +%Y%m%d_%H%M%S)
|
|
|
|
pg_dump -h localhost -U $POSTGRES_USER $POSTGRES_DB | gzip > $BACKUP_DIR/backup_$DATE.sql.gz
|
|
|
|
# Keep only last 7 days of backups
|
|
find $BACKUP_DIR -name "backup_*.sql.gz" -mtime +7 -delete
|
|
```
|
|
|
|
## Troubleshooting
|
|
|
|
### Common Production Issues
|
|
|
|
**High Memory Usage**: Check for memory leaks, optimize database queries, adjust worker counts
|
|
|
|
**Slow Response Times**: Enable query logging, check database indexes, optimize N+1 queries
|
|
|
|
**Connection Timeouts**: Adjust proxy timeouts, check database connection pool settings
|
|
|
|
**SSL Certificate Issues**: Verify certificate paths, check renewal process
|
|
|
|
### Performance Tuning
|
|
|
|
- Monitor database query performance
|
|
- Implement proper caching strategies
|
|
- Use connection pooling
|
|
- Optimize Docker image layers
|
|
- Configure proper resource limits
|
|
|
|
This production guide provides a solid foundation for deploying the FastAPI boilerplate to production environments with proper performance, security, and reliability configurations. |