Back to Blog
FastAPI
Best Practices
Production
Python
DevOps
Performance

FastAPI Best Practices for Production: Complete 2026 Guide

Master FastAPI best practices for production deployment. Security, performance, testing, error handling, monitoring, and scalability patterns for 2026.

FastLaunchAPI Team
19 min read

FastAPI Best Practices for Production: Complete 2026 Guide

Building a FastAPI application is easy. Building a production-ready FastAPI application that scales, performs well, and stays secure requires following proven best practices.

This comprehensive guide covers everything you need to know to deploy FastAPI applications to production in 2026, based on real-world patterns implemented in the FastLaunchAPI template - a battle-tested starter that eliminates weeks of setup time.

Project Structure Best Practices

The FastLaunchAPI template follows this proven production structure:

fastlaunchapi-premium/
├── backend/
│   ├── app/
│   │   ├── routers/          # Feature-based routing
│   │   │   ├── auth/         # Authentication module
│   │   │   │   ├── auth.py   # Auth endpoints
│   │   │   │   ├── models.py # User models
│   │   │   │   ├── services.py  # Business logic
│   │   │   │   ├── tasks.py     # Celery tasks
│   │   │   │   └── oauth_providers.py  # Dynamic OAuth
│   │   │   ├── payments/     # Stripe integration
│   │   │   │   ├── payments.py
│   │   │   │   ├── models.py
│   │   │   │   ├── services.py
│   │   │   │   └── tasks.py
│   │   │   └── core/         # System routes
│   │   │       ├── core.py
│   │   │       └── services.py
│   │   │
│   │   ├── db/               # Database layer
│   │   │   └── database.py   # Dual session management
│   │   │
│   │   ├── email/            # Email templates
│   │   │   ├── emails.py
│   │   │   └── templates/
│   │   │       ├── verify_user_email.html
│   │   │       └── reset_password_email.html
│   │   │
│   │   └── config/           # Settings
│   │       └── settings.py   # Pydantic settings
│   │
│   ├── alembic/              # Database migrations
│   │   └── versions/
│   │
│   ├── celery_setup.py       # Celery config + Beat schedule
│   ├── main.py               # FastAPI app entry
│   ├── pyproject.toml        # uv dependencies
│   └── uv.lock               # Locked versions
│
├── docker-compose.yaml       # All services
├── Dockerfile                # App container
└── .env.example              # Environment template

Why this structure works:

💡 Pro Tip: This exact structure ships with FastLaunchAPI - no setup needed!

Configuration Management

Use Pydantic Settings with Dynamic Detection

The FastLaunchAPI template uses Pydantic Settings for type-safe, environment-based configuration:

# app/config/settings.py
from pydantic_settings import BaseSettings, SettingsConfigDict
from typing import Optional

class Settings(BaseSettings):
    # API Settings
    PROJECT_NAME: str = "FastLaunchAPI"
    API_V1_STR: str = "/api/v1"

    # Security
    SECRET_KEY: str  # JWT signing key (required)
    ALGORITHM: str = "HS256"
    ACCESS_TOKEN_EXPIRE_MINUTES: int = 30

    # Database
    DATABASE_URL: str  # postgresql+asyncpg://...

    # OAuth2 - Dynamic provider detection
    GOOGLE_CLIENT_ID: Optional[str] = None
    GOOGLE_CLIENT_SECRET: Optional[str] = None
    GITHUB_CLIENT_ID: Optional[str] = None
    GITHUB_CLIENT_SECRET: Optional[str] = None

    # Stripe
    STRIPE_SECRET_KEY: str
    STRIPE_WEBHOOK_SECRET: str
    STRIPE_PRICE_ID: str

    # SendGrid Email
    SENDGRID_API_KEY: str
    FROM_EMAIL: str = "[email protected]"

    # Celery + Redis
    CELERY_BROKER_URL: str = "redis://localhost:6379/0"
    CELERY_RESULT_BACKEND: str = "redis://localhost:6379/0"

    # CORS
    BACKEND_CORS_ORIGINS: list[str] = ["http://localhost:3000"]

    model_config = SettingsConfigDict(
        env_file=".env",
        case_sensitive=True,
        extra="ignore"  # Ignore unknown env vars
    )

    def oauth_providers_configured(self) -> list[str]:
        """Dynamically detect which OAuth providers are configured"""
        providers = []
        if self.GOOGLE_CLIENT_ID and self.GOOGLE_CLIENT_SECRET:
            providers.append("google")
        if self.GITHUB_CLIENT_ID and self.GITHUB_CLIENT_SECRET:
            providers.append("github")
        return providers

settings = Settings()

Best practices:

.env File Structure

# .env.example (from FastLaunchAPI template)
# Database
DATABASE_URL=postgresql+asyncpg://user:pass@localhost:5432/dbname

# Security
SECRET_KEY=your-super-secret-key-change-in-production

# OAuth2 (optional - add only providers you want)
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-secret
# GITHUB_CLIENT_ID=  # Uncomment to enable GitHub login

# Stripe
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PRICE_ID=price_...

# SendGrid
SENDGRID_API_KEY=SG.xxx
[email protected]

# Celery
CELERY_BROKER_URL=redis://localhost:6379/0

💡 FastLaunchAPI ships with this exact pattern - all environment variables documented and validated!

Database Best Practices

Dual Session Management (Async + Sync)

Critical for production: FastLaunchAPI uses dual session management to handle both async FastAPI endpoints and sync Celery tasks:

# app/db/database.py
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
from app.config.settings import settings

# ASYNC ENGINE for FastAPI endpoints
async_engine = create_async_engine(
    settings.DATABASE_URL,
    echo=False,
    pool_size=10,
    max_overflow=20,
    pool_pre_ping=True,  # Verify connections before using
)

AsyncSessionLocal = async_sessionmaker(
    async_engine,
    class_=AsyncSession,
    expire_on_commit=False,
    autocommit=False,
    autoflush=False,
)

# SYNC ENGINE for Celery tasks
# Convert asyncpg URL to psycopg2 for sync operations
sync_db_url = settings.DATABASE_URL.replace(
    "postgresql+asyncpg://",
    "postgresql+psycopg2://"
)

sync_engine = create_engine(
    sync_db_url,
    echo=False,
    pool_size=5,
    max_overflow=10,
    pool_pre_ping=True,
)

SyncSessionLocal = sessionmaker(
    sync_engine,
    class_=Session,
    expire_on_commit=False,
    autocommit=False,
    autoflush=False,
)

# Async dependency for FastAPI routes
async def get_db() -> AsyncSession:
    async with AsyncSessionLocal() as session:
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()
            raise
        finally:
            await session.close()

# Sync context manager for Celery tasks
def get_sync_db() -> Session:
    db = SyncSessionLocal()
    try:
        yield db
        db.commit()
    except Exception:
        db.rollback()
        raise
    finally:
        db.close()

Why dual sessions?

Usage in Routes

# app/routers/auth/auth.py
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.database import get_db

router = APIRouter()

@router.post("/register")
async def register(
    email: str,
    db: AsyncSession = Depends(get_db)  # Async session
):
    # Use async SQLAlchemy queries
    result = await db.execute(
        select(User).where(User.email == email)
    )
    user = result.scalar_one_or_none()
    # ...

Usage in Celery Tasks

# app/routers/auth/tasks.py
from celery_setup import celery_app
from app.db.database import get_sync_db
from app.routers.auth.models import User
from sqlalchemy import select

@celery_app.task
def send_verification_email(user_id: int):
    """Celery task - uses SYNC session"""
    with next(get_sync_db()) as db:
        # Use sync SQLAlchemy - no await!
        user = db.execute(
            select(User).where(User.id == user_id)
        ).scalar_one()

        # Send email logic
        send_email(user.email, "Verify your account", ...)

        # Update user
        user.verification_email_sent = True
        db.commit()

Critical insights:

💡 This dual session pattern ships with FastLaunchAPI - production-tested and ready to use!

Repository Pattern

# app/db/repositories/user_repository.py
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.models.user import User
from typing import Optional

class UserRepository:
    def __init__(self, session: AsyncSession):
        self.session = session

    async def get_by_id(self, user_id: int) -> Optional[User]:
        result = await self.session.execute(
            select(User).where(User.id == user_id)
        )
        return result.scalar_one_or_none()

    async def get_by_email(self, email: str) -> Optional[User]:
        result = await self.session.execute(
            select(User).where(User.email == email)
        )
        return result.scalar_one_or_none()

    async def create(self, user: User) -> User:
        self.session.add(user)
        await self.session.flush()
        await self.session.refresh(user)
        return user

    async def update(self, user: User) -> User:
        await self.session.merge(user)
        await self.session.flush()
        return user

Benefits:

Error Handling Best Practices

Custom Exception Handler

# app/core/exceptions.py
from fastapi import HTTPException, Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException

class AppException(Exception):
    """Base exception for application errors"""
    def __init__(self, message: str, status_code: int = 500):
        self.message = message
        self.status_code = status_code

class NotFoundException(AppException):
    def __init__(self, resource: str):
        super().__init__(f"{resource} not found", 404)

class UnauthorizedException(AppException):
    def __init__(self, message: str = "Unauthorized"):
        super().__init__(message, 401)

# Exception handlers
async def app_exception_handler(request: Request, exc: AppException):
    return JSONResponse(
        status_code=exc.status_code,
        content={
            "error": exc.message,
            "path": request.url.path,
            "method": request.method,
        }
    )

async def validation_exception_handler(request: Request, exc: RequestValidationError):
    return JSONResponse(
        status_code=422,
        content={
            "error": "Validation error",
            "details": exc.errors(),
        }
    )

# app/main.py
from app.core.exceptions import *

app = FastAPI()

app.add_exception_handler(AppException, app_exception_handler)
app.add_exception_handler(RequestValidationError, validation_exception_handler)

Structured Logging

# app/core/logging.py
import logging
import json
from datetime import datetime

class JSONFormatter(logging.Formatter):
    def format(self, record):
        log_data = {
            "timestamp": datetime.utcnow().isoformat(),
            "level": record.levelname,
            "message": record.getMessage(),
            "module": record.module,
            "function": record.funcName,
        }

        if record.exc_info:
            log_data["exception"] = self.formatException(record.exc_info)

        return json.dumps(log_data)

def setup_logging():
    handler = logging.StreamHandler()
    handler.setFormatter(JSONFormatter())

    logger = logging.getLogger("app")
    logger.addHandler(handler)
    logger.setLevel(logging.INFO)

    return logger

logger = setup_logging()

Security Best Practices

JWT Authentication + OAuth2

FastLaunchAPI implements industry-standard JWT + OAuth2 with dynamic provider detection:

# app/routers/auth/services.py
from datetime import datetime, timedelta
from jose import JWTError, jwt
from passlib.context import CryptContext
from app.config.settings import settings

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def verify_password(plain_password: str, hashed_password: str) -> bool:
    return pwd_context.verify(plain_password, hashed_password)

def get_password_hash(password: str) -> str:
    return pwd_context.hash(password)

def create_access_token(user_id: int, expires_delta: timedelta | None = None):
    """Create JWT access token"""
    to_encode = {"sub": str(user_id), "type": "access"}

    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(
            minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
        )

    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(
        to_encode,
        settings.SECRET_KEY,
        algorithm=settings.ALGORITHM
    )
    return encoded_jwt

def decode_token(token: str) -> dict:
    """Decode and validate JWT token"""
    try:
        payload = jwt.decode(
            token,
            settings.SECRET_KEY,
            algorithms=[settings.ALGORITHM]
        )
        return payload
    except JWTError:
        return None

OAuth2 with Dynamic Provider Detection:

# app/routers/auth/oauth_providers.py
from app.config.settings import settings

def get_oauth_config(provider: str) -> dict:
    """Dynamically configure OAuth providers from env vars"""
    providers = {
        "google": {
            "client_id": settings.GOOGLE_CLIENT_ID,
            "client_secret": settings.GOOGLE_CLIENT_SECRET,
            "authorize_url": "https://accounts.google.com/o/oauth2/auth",
            "token_url": "https://oauth2.googleapis.com/token",
            "userinfo_url": "https://www.googleapis.com/oauth2/v1/userinfo",
        },
        "github": {
            "client_id": settings.GITHUB_CLIENT_ID,
            "client_secret": settings.GITHUB_CLIENT_SECRET,
            "authorize_url": "https://github.com/login/oauth/authorize",
            "token_url": "https://github.com/login/oauth/access_token",
            "userinfo_url": "https://api.github.com/user",
        },
    }

    # Only return configured providers
    if provider in providers:
        config = providers[provider]
        if config["client_id"] and config["client_secret"]:
            return config

    return None

@router.get("/oauth/{provider}")
async def oauth_login(provider: str):
    """Initiate OAuth flow - only for configured providers"""
    oauth_config = get_oauth_config(provider)
    if not oauth_config:
        raise HTTPException(400, f"Provider {provider} not configured")

    # Generate OAuth URL
    return {"authorization_url": oauth_config["authorize_url"]}

Protected Routes:

# app/routers/auth/auth.py
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from app.routers.auth.services import decode_token

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")

async def get_current_user(
    token: str = Depends(oauth2_scheme),
    db: AsyncSession = Depends(get_db)
):
    """Dependency to get authenticated user"""
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )

    payload = decode_token(token)
    if not payload:
        raise credentials_exception

    user_id: str = payload.get("sub")
    if user_id is None:
        raise credentials_exception

    # Get user from database
    result = await db.execute(
        select(User).where(User.id == int(user_id))
    )
    user = result.scalar_one_or_none()

    if user is None:
        raise credentials_exception

    return user

# Protected endpoint example
@router.get("/me")
async def get_me(current_user: User = Depends(get_current_user)):
    """Get current authenticated user"""
    return current_user

Key security features:

💡 FastLaunchAPI includes complete auth - JWT, OAuth2 (Google/GitHub), email verification, password reset!

CORS Configuration

# app/main.py
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.allowed_origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
    expose_headers=["X-Total-Count"],  # Custom headers
)

Performance Optimization

Caching with Redis

# app/core/cache.py
from redis.asyncio import Redis
from typing import Optional
import json

class CacheService:
    def __init__(self, redis_url: str):
        self.redis = Redis.from_url(redis_url, decode_responses=True)

    async def get(self, key: str) -> Optional[dict]:
        data = await self.redis.get(key)
        return json.loads(data) if data else None

    async def set(self, key: str, value: dict, expire: int = 3600):
        await self.redis.set(key, json.dumps(value), ex=expire)

    async def delete(self, key: str):
        await self.redis.delete(key)

    async def clear_pattern(self, pattern: str):
        keys = await self.redis.keys(pattern)
        if keys:
            await self.redis.delete(*keys)

cache = CacheService(settings.redis_url)

# Usage in routes
@app.get("/users/{user_id}")
async def get_user(user_id: int):
    # Check cache
    cached = await cache.get(f"user:{user_id}")
    if cached:
        return cached

    # Get from database
    user = await user_repository.get_by_id(user_id)

    # Cache result
    await cache.set(f"user:{user_id}", user.dict(), expire=300)

    return user

Background Tasks with Celery

FastLaunchAPI uses Celery + Redis for robust background task processing:

# celery_setup.py
from celery import Celery
from celery.schedules import crontab
from app.config.settings import settings

celery_app = Celery(
    "fastlaunchapi",
    broker=settings.CELERY_BROKER_URL,
    backend=settings.CELERY_RESULT_BACKEND,
)

celery_app.conf.update(
    task_serializer="json",
    accept_content=["json"],
    result_serializer="json",
    timezone="UTC",
    enable_utc=True,
    task_track_started=True,
    task_time_limit=300,  # 5 minutes max
)

# Celery Beat Schedule (periodic tasks)
celery_app.conf.beat_schedule = {
    'cleanup-expired-tokens': {
        'task': 'app.routers.auth.tasks.cleanup_expired_tokens',
        'schedule': crontab(hour=2, minute=0),  # 2 AM daily
    },
    'send-payment-reminders': {
        'task': 'app.routers.payments.tasks.send_payment_reminders',
        'schedule': crontab(hour=10, minute=0),  # 10 AM daily
    },
}

Using Celery Tasks:

# app/routers/auth/tasks.py
from celery_setup import celery_app
from app.email.emails import send_email
from app.db.database import get_sync_db  # Use sync session!
from app.routers.auth.models import User
from sqlalchemy import select

@celery_app.task(bind=True, max_retries=3)
def send_verification_email(self, user_id: int, verification_token: str):
    """Send email verification - retries on failure"""
    try:
        with next(get_sync_db()) as db:
            user = db.execute(
                select(User).where(User.id == user_id)
            ).scalar_one()

            send_email(
                to_email=user.email,
                subject="Verify your account",
                html_content=f"Token: {verification_token}",
            )

            user.verification_email_sent = True
            db.commit()

    except Exception as exc:
        # Retry with exponential backoff
        raise self.retry(exc=exc, countdown=60 * (2 ** self.request.retries))


@celery_app.task
def cleanup_expired_tokens():
    """Periodic task - runs daily at 2 AM"""
    with next(get_sync_db()) as db:
        deleted = db.execute(
            delete(User).where(
                User.verification_token_expires < datetime.utcnow(),
                User.is_verified == False
            )
        )
        db.commit()
        return f"Deleted {deleted.rowcount} expired tokens"

Triggering from FastAPI routes:

# app/routers/auth/auth.py
from app.routers.auth.tasks import send_verification_email

@router.post("/register")
async def register(user: UserCreate, db: AsyncSession = Depends(get_db)):
    # Create user in database
    new_user = User(email=user.email, ...)
    db.add(new_user)
    await db.commit()

    # Trigger Celery task (non-blocking!)
    send_verification_email.delay(new_user.id, new_user.verification_token)

    return {"message": "Check your email"}

Running Celery + Beat + Flower:

# docker-compose.yaml (from FastLaunchAPI template)
services:
  celery_worker:
    build: .
    command: celery -A celery_setup worker --loglevel=info
    depends_on:
      - redis
      - db

  celery_beat:
    build: .
    command: celery -A celery_setup beat --loglevel=info
    depends_on:
      - redis

  flower:
    build: .
    command: celery -A celery_setup flower --port=5555
    ports:
      - "5555:5555"
    depends_on:
      - redis

Why Celery over BackgroundTasks?

💡 FastLaunchAPI includes this complete Celery setup with Redis, Beat, and Flower pre-configured!

Database Query Optimization

# Use select_related equivalent
from sqlalchemy.orm import selectinload

async def get_user_with_posts(user_id: int):
    result = await session.execute(
        select(User)
        .options(selectinload(User.posts))
        .where(User.id == user_id)
    )
    return result.scalar_one_or_none()

Testing Best Practices

Test Setup

# tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from app.main import app
from app.db.session import get_db

# Test database
TEST_DATABASE_URL = "postgresql+asyncpg://test:test@localhost/test_db"

@pytest.fixture
async def test_db():
    engine = create_async_engine(TEST_DATABASE_URL)
    TestSessionLocal = sessionmaker(
        engine, class_=AsyncSession, expire_on_commit=False
    )

    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

    async with TestSessionLocal() as session:
        yield session

    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.drop_all)

@pytest.fixture
def client(test_db):
    def override_get_db():
        yield test_db

    app.dependency_overrides[get_db] = override_get_db
    with TestClient(app) as c:
        yield c
    app.dependency_overrides.clear()

@pytest.fixture
async def test_user(test_db):
    user = User(email="[email protected]", hashed_password="...")
    test_db.add(user)
    await test_db.commit()
    return user

API Tests

# tests/test_api/test_users.py
import pytest

def test_get_user(client, test_user):
    response = client.get(f"/api/v1/users/{test_user.id}")
    assert response.status_code == 200
    assert response.json()["email"] == test_user.email

def test_create_user(client):
    response = client.post(
        "/api/v1/users",
        json={"email": "[email protected]", "password": "secure123"}
    )
    assert response.status_code == 201
    assert "id" in response.json()

@pytest.mark.asyncio
async def test_user_service(test_db):
    service = UserService(test_db)
    user = await service.create_user("[email protected]", "password")
    assert user.id is not None

Monitoring & Observability

Prometheus Metrics

# app/core/metrics.py
from prometheus_client import Counter, Histogram, generate_latest
from fastapi import Request
import time

REQUEST_COUNT = Counter(
    'http_requests_total',
    'Total HTTP requests',
    ['method', 'endpoint', 'status']
)

REQUEST_DURATION = Histogram(
    'http_request_duration_seconds',
    'HTTP request duration',
    ['method', 'endpoint']
)

@app.middleware("http")
async def monitor_requests(request: Request, call_next):
    start_time = time.time()

    response = await call_next(request)

    duration = time.time() - start_time

    REQUEST_COUNT.labels(
        method=request.method,
        endpoint=request.url.path,
        status=response.status_code
    ).inc()

    REQUEST_DURATION.labels(
        method=request.method,
        endpoint=request.url.path
    ).observe(duration)

    return response

@app.get("/metrics")
async def metrics():
    return Response(generate_latest(), media_type="text/plain")

Health Checks

@app.get("/health")
async def health_check(db: AsyncSession = Depends(get_db)):
    try:
        # Check database
        await db.execute("SELECT 1")

        # Check Redis
        await cache.redis.ping()

        return {
            "status": "healthy",
            "database": "up",
            "cache": "up",
            "timestamp": datetime.utcnow()
        }
    except Exception as e:
        return JSONResponse(
            status_code=503,
            content={"status": "unhealthy", "error": str(e)}
        )

Deployment Best Practices

UV Package Manager (10-100x Faster)

FastLaunchAPI uses UV - the modern Rust-based Python package manager:

# pyproject.toml
[project]
name = "fastlaunchapi"
version = "1.0.0"
requires-python = ">=3.11"
dependencies = [
    "fastapi>=0.115.0",
    "uvicorn[standard]>=0.32.0",
    "sqlalchemy>=2.0.0",
    "alembic>=1.13.0",
    "asyncpg>=0.29.0",
    "psycopg2-binary>=2.9.0",  # For Celery sync sessions
    "pydantic>=2.10.0",
    "pydantic-settings>=2.6.0",
    "python-jose[cryptography]>=3.3.0",
    "passlib[bcrypt]>=1.7.4",
    "python-multipart>=0.0.6",
    "celery>=5.4.0",
    "redis>=5.0.0",
    "sendgrid>=6.11.0",
    "stripe>=11.0.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=8.0.0",
    "pytest-asyncio>=0.23.0",
    "httpx>=0.27.0",
    "ruff>=0.8.0",
]

Installing with UV:

# Install UV (one-time setup)
curl -LsSf https://astral.sh/uv/install.sh | sh

# Install dependencies (10-100x faster than pip!)
uv pip install -r pyproject.toml

# Add new package
uv add stripe

# Development dependencies
uv add --dev pytest

Docker Configuration with Multi-Stage Build

# Dockerfile (from FastLaunchAPI template)
FROM python:3.11-slim as base

# Install UV
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv

ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    UV_COMPILE_BYTECODE=1 \
    UV_LINK_MODE=copy

WORKDIR /app

# Install dependencies
COPY pyproject.toml uv.lock ./
RUN uv pip install --system --no-cache .

# Copy application
COPY ./app ./app
COPY ./celery_setup.py ./

# Production stage
FROM base as production

# Create non-root user
RUN useradd -m -u 1000 appuser && \
    chown -R appuser:appuser /app

USER appuser

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

Docker Compose - Complete Production Stack

# docker-compose.yaml (from FastLaunchAPI template)
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: fastlaunchapi
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

  backend:
    build: .
    command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
    volumes:
      - ./app:/app/app
    ports:
      - "8000:8000"
    env_file:
      - .env
    depends_on:
      - db
      - redis

  celery_worker:
    build: .
    command: celery -A celery_setup worker --loglevel=info
    volumes:
      - ./app:/app/app
    env_file:
      - .env
    depends_on:
      - db
      - redis

  celery_beat:
    build: .
    command: celery -A celery_setup beat --loglevel=info
    volumes:
      - ./app:/app/app
    env_file:
      - .env
    depends_on:
      - redis

  flower:
    build: .
    command: celery -A celery_setup flower --port=5555
    ports:
      - "5555:5555"
    env_file:
      - .env
    depends_on:
      - redis

  pgadmin:
    image: dpage/pgadmin4
    environment:
      PGADMIN_DEFAULT_EMAIL: [email protected]
      PGADMIN_DEFAULT_PASSWORD: admin
    ports:
      - "5050:80"

volumes:
  postgres_data:

Running the stack:

# Start all services
docker-compose up -d

# Run migrations
docker-compose exec backend alembic upgrade head

# View logs
docker-compose logs -f backend

# Monitor Celery tasks
open http://localhost:5555  # Flower UI

Production deployment:

💡 This exact Docker Compose setup ships with FastLaunchAPI - just docker-compose up!

API Versioning

# app/api/v1/router.py
from fastapi import APIRouter

api_router = APIRouter()
api_router.include_router(users.router, prefix="/users", tags=["users"])
api_router.include_router(items.router, prefix="/items", tags=["items"])

# app/main.py
app.include_router(api_router, prefix="/api/v1")

Documentation Best Practices

Custom OpenAPI Documentation

app = FastAPI(
    title="FastAPI Application",
    description="Production-ready FastAPI app",
    version="1.0.0",
    docs_url="/api/docs",
    redoc_url="/api/redoc",
    openapi_url="/api/openapi.json",
    openapi_tags=[
        {"name": "users", "description": "User management"},
        {"name": "auth", "description": "Authentication"},
    ]
)

Request/Response Examples

@app.post(
    "/users",
    response_model=UserResponse,
    status_code=201,
    responses={
        201: {
            "description": "User created successfully",
            "content": {
                "application/json": {
                    "example": {"id": 1, "email": "[email protected]"}
                }
            }
        },
        400: {"description": "Invalid input"},
    }
)
async def create_user(user: UserCreate):
    """
    Create a new user with the following information:

    - **email**: Valid email address
    - **password**: Minimum 8 characters
    """
    return await user_service.create(user)

Production Checklist

Before deploying to production, ensure these FastLaunchAPI features are configured:

Authentication & Security

Database & Infrastructure

Background Tasks & Email

Payments & Business Logic

DevOps & Deployment

Code Quality

Get FastLaunchAPI - Skip the Setup

All these production best practices are pre-implemented in FastLaunchAPI. Why spend weeks configuring when you can start building features today?

What You Get

FastLaunchAPI Premium → includes everything from this guide:

Backend (FastAPI)

Frontend (Next.js 15)

Developer Experience

Pricing

Why FastLaunchAPI?

Time savings:

Total: Save 4-6 weeks and launch your SaaS faster.

Get FastLaunchAPI Now → | View Documentation


Last updated: January 2026

This blog post is based on real production patterns from FastLaunchAPI - a battle-tested FastAPI template used by 100+ developers.

Related Articles