Skip to main content

Deployment

Docker Deployment

Dockerfile

FROM python:3.9-slim

WORKDIR /app

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

COPY . .

EXPOSE 8000

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

Docker Compose

version: '3.8'

services:
web:
build: .
ports:
- '8000:8000'
environment:
- DATABASE_URL=postgresql://user:password@db:5432/dbname
depends_on:
- db
volumes:
- ./app:/app
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload

db:
image: postgres:13
environment:
- POSTGRES_DB=dbname
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- '5432:5432'

volumes:
postgres_data:

Production Servers

Gunicorn with Uvicorn Workers

# Install gunicorn
pip install gunicorn

# Run with gunicorn
gunicorn app.main:app -w 4 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000

Gunicorn Configuration

# gunicorn.conf.py
bind = "0.0.0.0:8000"
workers = 4
worker_class = "uvicorn.workers.UvicornWorker"
worker_connections = 1000
max_requests = 1000
max_requests_jitter = 50
preload_app = True
timeout = 30
keepalive = 2

# Logging
accesslog = "-"
errorlog = "-"
loglevel = "info"

Systemd Service

# /etc/systemd/system/fastapi.service
[Unit]
Description=FastAPI app
After=network.target

[Service]
Type=exec
User=www-data
Group=www-data
WorkingDirectory=/var/www/fastapi
Environment=PATH=/var/www/fastapi/venv/bin
ExecStart=/var/www/fastapi/venv/bin/gunicorn --config gunicorn.conf.py app.main:app
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=5
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Environment Configuration

Environment Variables

# config.py
import os
from pydantic import BaseSettings

class Settings(BaseSettings):
app_name: str = "FastAPI App"
debug: bool = False
database_url: str
secret_key: str
algorithm: str = "HS256"
access_token_expire_minutes: int = 30

class Config:
env_file = ".env"

settings = Settings()

Environment Files

# .env
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
SECRET_KEY=your-secret-key-here
DEBUG=False
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30

Load Balancing

Nginx Configuration

# /etc/nginx/sites-available/fastapi
upstream fastapi {
server 127.0.0.1:8000;
server 127.0.0.1:8001;
server 127.0.0.1:8002;
server 127.0.0.1:8003;
}

server {
listen 80;
server_name example.com;

location / {
proxy_pass http://fastapi;
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;
}

location /static/ {
alias /var/www/fastapi/static/;
}
}

SSL Configuration

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

ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

location / {
proxy_pass http://fastapi;
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;
}
}

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

Cloud Deployment

AWS Lambda

# serverless.yml
service: fastapi-lambda

provider:
name: aws
runtime: python3.9
region: us-east-1

functions:
app:
handler: app.main.handler
events:
- http:
path: /{proxy+}
method: any
- http:
path: /
method: any

plugins:
- serverless-python-requirements

AWS Lambda Handler

# app/main.py
from fastapi import FastAPI
from mangum import Mangum

app = FastAPI()

@app.get("/")
async def root():
return {"message": "Hello World"}

handler = Mangum(app)

Heroku Deployment

# Procfile
web: gunicorn app.main:app -w 4 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:$PORT
# runtime.txt
python-3.9.7

Railway Deployment

# railway.json
{
"build": {
"builder": "NIXPACKS"
},
"deploy": {
"startCommand": "uvicorn app.main:app --host 0.0.0.0 --port $PORT"
}
}

Database Setup

Production Database

# Database connection with connection pooling
from sqlalchemy import create_engine
from sqlalchemy.pool import QueuePool

engine = create_engine(
DATABASE_URL,
poolclass=QueuePool,
pool_size=10,
max_overflow=20,
pool_pre_ping=True,
pool_recycle=3600,
echo=False
)

Database Migration

# Run migrations in production
alembic upgrade head

# Backup database before migration
pg_dump dbname > backup.sql

# Restore if needed
psql dbname < backup.sql

Monitoring & Logging

Logging Configuration

# logging_config.py
import logging.config

LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
},
},
"handlers": {
"default": {
"formatter": "default",
"class": "logging.StreamHandler",
"stream": "ext://sys.stdout",
},
"file": {
"formatter": "default",
"class": "logging.FileHandler",
"filename": "app.log",
},
},
"root": {
"level": "INFO",
"handlers": ["default", "file"],
},
}

logging.config.dictConfig(LOGGING_CONFIG)

Health Checks

@app.get("/health")
async def health_check():
return {
"status": "healthy",
"timestamp": datetime.utcnow().isoformat()
}

@app.get("/health/db")
async def db_health_check(db: Session = Depends(get_db)):
try:
db.execute("SELECT 1")
return {"status": "healthy", "database": "connected"}
except Exception as e:
raise HTTPException(status_code=503, detail="Database unavailable")

Security in Production

Security Headers

@app.middleware("http")
async def add_security_headers(request: Request, call_next):
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["X-XSS-Protection"] = "1; mode=block"
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
return response

Rate Limiting

from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address

limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter

@app.get("/api/data")
@limiter.limit("10/minute")
async def get_data(request: Request):
return {"data": "sensitive information"}