Python Fastapi Dockerized Production Setup

Written by

in

Deploying a Python FastAPI application to production can feel daunting, especially when you want the reliability of containers, the scalability of Docker, and the performance guarantees of a well‑tuned stack. In this guide we’ll walk you through a complete, production‑ready setup: from structuring your FastAPI project, writing a Dockerfile that follows best practices, configuring docker‑compose for multi‑service orchestration, to adding health checks, logging, and security hardening. By the end you’ll have a repeatable, SEO‑friendly blueprint you can clone for any new FastAPI service.

Why Dockerize a FastAPI Application?

Docker brings consistency, isolation, and portability—three pillars essential for modern micro‑service architectures. When you containerize FastAPI you gain:

  • Environment parity: The same image runs locally, on staging, and in production.
  • Dependency isolation: No more “it works on my machine” surprises.
  • Scalable deployment: Containers can be orchestrated by Kubernetes, ECS, or Docker Swarm with minimal changes.
  • Fast rollbacks: Reverting to a previous image is a single command.

Project Structure for a Production‑Ready FastAPI Service

A clean directory layout makes it easier to maintain, test, and extend your API. Below is a recommended structure:

my_fastapi_app/
├── app/
│   ├── __init__.py
│   ├── main.py            # FastAPI instance
│   ├── routers/
│   │   └── users.py
│   ├── models/
│   │   └── user.py
│   ├── services/
│   │   └── auth.py
│   └── config.py
├── tests/
│   └── test_users.py
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
└── .dockerignore

Key Files Explained

  • main.py: Creates the FastAPI() object, includes routers, and configures middleware.
  • config.py: Centralizes environment variables using pydantic.BaseSettings.
  • requirements.txt: Pin exact versions for reproducible builds.
  • .dockerignore: Excludes virtual environments, caches, and test data from the image.

Writing a Production‑Grade Dockerfile

The Dockerfile should be small, secure, and multi‑stage to keep the final image lightweight. Below is a step‑by‑step example:

# ---- Build Stage ---------------------------------------------------------
FROM python:3.12-slim AS builder

# Install build dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc \
    libpq-dev \
    && rm -rf /var/lib/apt/lists/*

# Create a non‑root user
RUN useradd -m appuser

# Set work directory
WORKDIR /app

# Install Python dependencies
COPY requirements.txt .
RUN pip install --upgrade pip && \
    pip install --user -r requirements.txt

# ---- Runtime Stage -------------------------------------------------------
FROM python:3.12-slim

# Copy only the runtime essentials from the builder
COPY --from=builder /home/appuser/.local /home/appuser/.local
ENV PATH=/home/appuser/.local/bin:$PATH

# Create a non‑root user (same UID/GID as builder)
RUN useradd -m appuser
USER appuser

# Set work directory
WORKDIR /app

# Copy application code
COPY --chown=appuser:appuser app/ ./app/
COPY --chown=appuser:appuser main.py ./

# Expose the port FastAPI will run on
EXPOSE 8000

# Use Uvicorn with gunicorn workers for production
CMD ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", "main:app", "--bind", "0.0.0.0:8000", "--workers", "4", "--log-level", "info"]

Why This Dockerfile Works for Production

  • Multi‑stage build: The final image contains only runtime dependencies, reducing size to ~80 MB.
  • Non‑root user: Improves security by preventing privilege escalation inside the container.
  • Gunicorn + Uvicorn workers: Provides graceful process management, auto‑restarts, and better CPU utilization.
  • Explicit EXPOSE: Documents the intended network port for orchestration tools.

Docker Compose for Local Development and Staging

While a single container can run in production, most teams use docker‑compose to spin up supporting services (databases, caches, reverse proxies). Here’s a minimal docker-compose.yml that includes PostgreSQL and Redis:


services:
  api:
    build: .
    container_name: fastapi_app
    restart: unless-stopped
    env_file:
      - .env
    ports:
      - "8000:8000"
    depends_on:
      - db
      - redis
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 5s
      retries: 3

  db:
    image: postgres:16-alpine
    container_name: fastapi_db
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - db_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    container_name: fastapi_redis
    restart: unless-stopped
    volumes:
      - redis_data:/data

volumes:
  db_data:
  redis_data:

Important Compose Settings

  • restart: unless-stopped ensures containers recover after crashes but don’t restart on manual stop.
  • healthcheck provides Docker with a way to verify the API is ready before routing traffic.
  • env_file keeps secrets out of the compose file and works with .env for CI/CD pipelines.

Configuration Management with Pydantic Settings

FastAPI integrates seamlessly with pydantic.BaseSettings. Create a config.py that reads environment variables and provides defaults for local development:

from pydantic import BaseSettings, Field, PostgresDsn, RedisDsn

class Settings(BaseSettings):
    # Core FastAPI settings
    APP_NAME: str = "FastAPI Dockerized Service"
    DEBUG: bool = False

    # Database
    DATABASE_URL: PostgresDsn = Field(..., env="POSTGRES_URL")

    # Redis cache
    REDIS_URL: RedisDsn = Field(..., env="REDIS_URL")

    # Security
    SECRET_KEY: str = Field(..., env="SECRET_KEY")
    ALGORITHM: str = "HS256"
    ACCESS_TOKEN_EXPIRE_MINUTES: int = 30

    class Config:
        env_file = ".env"
        env_file_encoding = "utf-8"

settings = Settings()

Import settings wherever you need configuration; FastAPI will raise an error at startup if required variables are missing, preventing accidental misconfiguration in production.

Implementing Health Checks and Metrics

Robust services expose endpoints for health and observability. Add two simple routes in main.py:

from fastapi import FastAPI, Depends
from fastapi.responses import JSONResponse
from starlette.status import HTTP_200_OK
from .config import settings

app = FastAPI(title=settings.APP_NAME)

@app.get("/health", status_code=HTTP_200_OK)
def health_check():
    return {"status": "ok"}

@app.get("/metrics")
def metrics():
    # Example: expose Prometheus metrics (requires prometheus_client)
    from prometheus_client import generate_latest, CONTENT_TYPE_LATEST
    return JSONResponse(content=generate_latest(), media_type=CONTENT_TYPE_LATEST)

When paired with Docker’s healthcheck (see the compose file), orchestration platforms can automatically restart unhealthy containers.

Logging Best Practices

Production logging should be structured, level‑aware, and sent to stdout so Docker can capture it. Configure the standard library logger in main.py:

import logging
import sys

log_formatter = logging.Formatter(
    "%(asctime)s | %(levelname)s | %(name)s | %(message)s"
)

handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(log_formatter)

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

For more advanced scenarios, integrate structlog or forward logs to a centralized system like Loki or Elastic Stack.

Security Hardening Steps

  • Use a non‑root user: Already covered in the Dockerfile.
  • Limit container capabilities: In production orchestrators add readOnlyRootFilesystem: true and drop unnecessary Linux capabilities.
  • Enable HTTPS: Deploy behind a reverse proxy (NGINX, Traefik, or a cloud load balancer) that terminates TLS. Store certificates in a secret manager.
  • Set security headers: Use FastAPI middleware or the proxy to add Strict-Transport-Security, Content-Security-Policy, and X-Frame-Options.
  • Scan images: Run docker scan or integrate Trivy in CI to detect vulnerabilities before push.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *