FastAPI is the modern Python web framework for building APIs, leveraging Python type hints for automatic validation, serialization, and OpenAPI documentation. Combined with Docker for deployment, it provides a robust, reproducible production environment. This guide covers building a complete FastAPI application and deploying it with Docker on a VPS.
Project Setup
mkdir fastapi-app && cd fastapi-app
# Create virtual environment
python3 -m venv venv
source venv/bin/activate
# Install dependencies
pip install fastapi uvicorn[standard] sqlalchemy[asyncio] asyncpg alembic pydantic-settings
pip freeze > requirements.txt
Application Code
# app/main.py
from fastapi import FastAPI, HTTPException, Depends
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
from .database import engine, get_db
from .models import Base
from .routers import users
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield
# Shutdown
await engine.dispose()
app = FastAPI(
title="My API",
version="1.0.0",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(users.router, prefix="/api/v1")
@app.get("/health")
async def health():
return {"status": "healthy"}
# app/database.py
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker, declarative_base
from .config import settings
engine = create_async_engine(settings.DATABASE_URL, echo=False, pool_size=20, max_overflow=10)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
Base = declarative_base()
async def get_db():
async with AsyncSessionLocal() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
# app/config.py
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
DATABASE_URL: str = "postgresql+asyncpg://user:pass@localhost/mydb"
SECRET_KEY: str = "change-me"
DEBUG: bool = False
class Config:
env_file = ".env"
settings = Settings()
# app/models.py
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.sql import func
from .database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(100), nullable=False)
email = Column(String(255), unique=True, nullable=False, index=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
# app/schemas.py
from pydantic import BaseModel, EmailStr
from datetime import datetime
class UserCreate(BaseModel):
name: str
email: EmailStr
class UserResponse(BaseModel):
id: int
name: str
email: str
created_at: datetime
class Config:
from_attributes = True
# app/routers/users.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from ..database import get_db
from ..models import User
from ..schemas import UserCreate, UserResponse
router = APIRouter(tags=["users"])
@router.get("/users", response_model=list[UserResponse])
async def list_users(skip: int = 0, limit: int = 20, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(User).offset(skip).limit(limit))
return result.scalars().all()
@router.post("/users", response_model=UserResponse, status_code=201)
async def create_user(user: UserCreate, db: AsyncSession = Depends(get_db)):
db_user = User(name=user.name, email=user.email)
db.add(db_user)
await db.flush()
await db.refresh(db_user)
return db_user
@router.get("/users/{user_id}", response_model=UserResponse)
async def get_user(user_id: int, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
Dockerfile
# Dockerfile
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
FROM python:3.12-slim
WORKDIR /app
# Copy installed packages
COPY --from=builder /install /usr/local
# Copy application
COPY app/ ./app/
# Create non-root user
RUN useradd -r -s /bin/false appuser
USER appuser
EXPOSE 8000
# Production server with Gunicorn + Uvicorn workers
CMD ["gunicorn", "app.main:app", \
"-w", "4", \
"-k", "uvicorn.workers.UvicornWorker", \
"-b", "0.0.0.0:8000", \
"--access-logfile", "-", \
"--error-logfile", "-", \
"--timeout", "30", \
"--graceful-timeout", "30", \
"--keep-alive", "5"]
Docker Compose
# docker-compose.yml
services:
api:
build: .
ports:
- "8000:8000"
env_file: .env
depends_on:
db:
condition: service_healthy
restart: unless-stopped
deploy:
resources:
limits:
memory: 512M
cpus: '2'
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: mydb
POSTGRES_USER: user
POSTGRES_PASSWORD: password
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
interval: 5s
timeout: 5s
retries: 5
volumes:
pgdata:
Production Deployment
# Build and start
docker compose up -d --build
# Check logs
docker compose logs -f api
# Scale workers
docker compose up -d --scale api=3
# Health check
curl http://localhost:8000/health
# Access auto-generated API docs
# http://localhost:8000/docs (Swagger UI)
# http://localhost:8000/redoc (ReDoc)
Gunicorn Configuration
# gunicorn.conf.py
import multiprocessing
bind = "0.0.0.0:8000"
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "uvicorn.workers.UvicornWorker"
worker_connections = 1000
timeout = 30
keepalive = 5
max_requests = 1000
max_requests_jitter = 50
accesslog = "-"
errorlog = "-"
loglevel = "info"
preload_app = True
Nginx Reverse Proxy
upstream fastapi {
server 127.0.0.1:8000;
keepalive 32;
}
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
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;
}
}
Summary
FastAPI with Docker provides a modern, performant Python API stack with automatic documentation, type validation, and async database support. The Gunicorn + Uvicorn worker combination handles production workloads efficiently, while Docker ensures consistent deployments. FastAPI's async support makes it 3-5x faster than Flask/Django for I/O-bound workloads, and the automatic OpenAPI docs eliminate the need for separate API documentation tools.