FastAPI Production Deployments
FastAPI Production Deployments: A Comprehensive Guide
Introduction
FastAPI has rapidly become one of the most popular frameworks for building high-performance APIs in Python. Built on top of Starlette for the web components and Pydantic for data validation, it delivers automatic OpenAPI documentation, async support, and type-safe request/response handling out of the box. However, building a great API is only half the battle — deploying it reliably to production requires careful attention to server configuration, containerization, orchestration, security, and monitoring.
This guide walks through everything you need to know about taking a FastAPI application from local development to a robust production environment.
1. Preparing Your Application for Production
Project Structure
A clean project structure is the foundation of a maintainable deployment:
my_fastapi_app/
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── config.py
│ ├── database.py
│ ├── dependencies.py
│ ├── routers/
│ │ ├── __init__.py
│ │ ├── users.py
│ │ └── items.py
│ ├── models/
│ │ ├── __init__.py
│ │ └── schemas.py
│ └── middleware/
│ ├── __init__.py
│ └── security.py
├── tests/
├── alembic/
├── Dockerfile
├── docker-compose.yml
├── pyproject.toml
└── requirements.txt
Environment Configuration
Never hard-code secrets. Use environment variables managed through Pydantic Settings:
# app/config.py
from pydantic_settings import BaseSettings
from functools import lru_cache
class Settings(BaseSettings):
app_name: str = "My FastAPI App"
debug: bool = False
database_url: str = ""
redis_url: str = "redis://localhost:6379"
secret_key: str = ""
allowed_origins: list[str] = ["*"]
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
@lru_cache()
def get_settings() -> Settings:
return Settings()
# app/main.py
from fastapi import FastAPI
from app.config import get_settings
settings = get_settings()
app = FastAPI(
title=settings.app_name,
docs_url="/docs",
redoc_url="/redoc",
)
Logging Configuration
# app/config.py (add to Settings)
import logging
import sys
class LogConfig:
"""Logging configuration for the application."""
BASE = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"format": "%(asctime)s | %(levelname)s | %(name)s | %(message)s",
},
"json": {
"format": "%(asctime)s | %(levelname)s | %(name)s | %(message)s",
"class": "pythonjsonlogger.jsonlogger.JsonFormatter",
},
},
"handlers": {
"default": {
"class": "logging.StreamHandler",
"formatter": "default",
"stream": "ext://sys.stderr",
},
},
"root": {"handlers": ["default"], "level": "INFO"},
}
2. Production Server Options
FastAPI runs on ASGI servers. The three primary choices for production are Uvicorn, Gunicorn + Uvicorn workers, and Hypercorn.
Option A: Uvicorn Standalone (Simple Deployments)
uvicorn app.main:app --host 0.0.0.0 --port 8000 --log-level info
Option B: Gunicorn with Uvicorn Workers (Recommended for Most Cases)
Gunicorn manages worker processes, while Uvicorn handles async traffic. This combination gives you process-level fault isolation.
gunicorn app.main:app \
-w 4 \
-k uvicorn.workers.UvicornWorker \
--bind 0.0.0.0:8000 \
--workers-per-node 4 \
--max-requests 1000 \
--max-requests-jitter 50 \
--timeout 120 \
--graceful-timeout 30 \
--access-logfile - \
--log-level info
Key parameters explained:
| Parameter | Purpose | |---|---| | -w 4 | Number of worker processes | | -k uvicorn.workers.UvicornWorker | ASGI worker class | | --max-requests | Restart worker after N requests (prevents memory leaks) | | --max-requests-jitter | Add randomness to restarts (prevents thundering herd) | | --timeout | Request timeout in seconds | | --graceful-timeout | Time for graceful shutdown |
Worker Count Calculation
A common rule of thumb:
workers = (2 * CPU_cores) + 1
For I/O-bound async workloads, you can safely go higher. Monitor your application and adjust accordingly.
3. Docker Deployment
Multi-Stage Dockerfile
# ---- Builder Stage ----
FROM python:3.12-slim AS builder
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies into a virtual environment
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY pyproject.toml .
RUN pip install --upgrade pip && \
pip install --no-cache-dir --upgrade -r requirements.txt
# ---- Production Stage ----
FROM python:3.12-slim AS production
WORKDIR /app
# Create non-root user for security
RUN groupadd -r appuser && useradd -r -g appuser -m appuser
# Copy virtual environment from builder
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Copy application code
COPY --chown=appuser:appuser app/ ./app/
COPY --chown=appuser:appuser alembic/ ./alembic/
COPY --chown=appuser:appuser pyproject.toml ./
USER appuser
# Health check
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD python -c "import urllib.request; \
urllib.request.urlopen('http://localhost:8000/health')" || exit 1
EXPOSE 8000
CMD ["gunicorn", "app.main:app", \
"-w", "4", \
"-k", "uvicorn.workers.UvicornWorker", \
"--bind", "0.0.0.0:8000", \
"--max-requests", "1000", \
"--max-requests-jitter", "50", \
"--timeout", "120"]
Docker Compose for Production
# docker-compose.yml
services:
web:
build:
context: .
dockerfile: Dockerfile
ports:
- "8000:8000"
environment:
- DATABASE_URL=${DATABASE_URL}
- REDIS_URL=${REDIS_URL}
- SECRET_KEY=${SECRET_KEY}
- DEBUG=false
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
networks:
- app_network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 5s
retries: 3
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- app_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
interval: 10s
timeout: 5