DEV Community

Python-T Point
Python-T Point

Posted on • Originally published at pythontpoint.in

🚀 Building a scalable Python API with FastAPI and Docker

Running a single‑threaded FastAPI process inside a Docker container can increase overall throughput. The container isolates the Python interpreter and lets the host kernel schedule multiple worker processes across CPU cores, allowing each worker to run its own event loop and avoid the GIL bottleneck.

📑 Table of Contents

  • 🚀 Architecture Overview — Why It Matters
  • 🐍 FastAPI Service — How to Structure
  • 📦 Dockerfile — Building the Image
  • 📡 Container Orchestration — Scaling Stateless
  • 🔧 Load Balancing — Using NGINX
  • 🗄️ Data Layer — Externalizing State
  • 🛡️ Connection Pool — Managing Connections
  • 🔧 Monitoring & Observability — Adding Metrics
  • ⚖️ Performance Comparison — FastAPI vs Flask
  • 🟩 Final Thoughts
  • ❓ Frequently Asked Questions
  • How many Uvicorn workers should I run per container?
  • Can I replace PostgreSQL with an in‑memory store for faster reads?
  • Is Docker Compose sufficient for production, or should I use Kubernetes?
  • 📚 References & Further Reading

🚀 Architecture Overview — Why It Matters

Understanding the end‑to‑end flow of a FastAPI service wrapped in Docker is essential for building a system that can handle thousands of concurrent requests.

  • FastAPI is an ASGI framework that runs on a non‑blocking event loop; paired with an async server such as uvicorn, it can multiplex I/O for many connections per process.
  • Docker adds cgroup‑based CPU and memory isolation, a reproducible runtime, and straightforward replication across hosts.
  • The load balancer (NGINX in the example) terminates client connections, performs TLS off‑loading, and forwards traffic to a pool of identical containers.
  • External state stores (PostgreSQL, Redis) keep the API stateless, so containers can be added or removed without losing session data.

What this does: the diagram below (conceptual) shows request flow from the client, through the reverse proxy, into a Kubernetes Service (or Docker Compose network), and finally into the FastAPI application which talks to a separate database.

Key point: Decoupling request handling, transport termination, and data persistence enables horizontal scaling without code changes.


🐍 FastAPI Service — How to Structure

A minimal FastAPI application demonstrates the core patterns needed for a production‑ready service.

# app/main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker app = FastAPI(title="User Service") DATABASE_URL = "postgresql+psycopg2://user:password@db:5432/users"
engine = create_engine(DATABASE_URL, pool_pre_ping=True)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) class UserCreate(BaseModel): username: str email: str @app.post("/users")
def create_user(payload: UserCreate): with SessionLocal() as db: result = db.execute( text("INSERT INTO users (username, email) VALUES (:u,:e) RETURNING id"), {"u": payload.username, "e": payload.email}, ) user_id = result.fetchone()[0] return {"id": user_id, "username": payload.username}
Enter fullscreen mode Exit fullscreen mode

What this does: (More onPythonTPoint tutorials)

  • FastAPI automatically generates OpenAPI docs and validates UserCreate against the Pydantic model.
  • SQLAlchemy engine with pool_pre_ping ensures dead connections are detected before use.
  • The endpoint opens a short‑lived session, runs a single INSERT, and returns the new identifier, keeping the request stateless.

📦 Dockerfile — Building the Image

# Dockerfile
FROM python:3.12-slim # Install system dependencies required by psycopg2
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
WORKDIR /app
COPY requirements.txt .
RUN pip install -no-cache-dir -r requirements.txt COPY ./app ./app
USER appuser EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
Enter fullscreen mode Exit fullscreen mode

What this does:

  • python:3.12-slim provides a minimal runtime, reducing attack surface.
  • System packages gcc and libpq-dev are needed to compile psycopg2 native bindings.
  • Running as appuser prevents privilege escalation inside the container.
  • The --workers 4 flag starts four Uvicorn worker processes, each with its own event loop, bypassing the GIL limitation.

Key point: The Dockerfile creates a reproducible image that launches multiple workers, turning a single‑process Python app into a multi‑process scalable service. (Also read: ⚙️ Building a Jenkins Docker CI CD pipeline tutorial made easy)


📡 Container Orchestration — Scaling Stateless

Deploying the FastAPI image with Docker Compose demonstrates how to replicate containers and expose them behind a reverse proxy.

# docker-compose.yaml
services: api: build: . ports: - "8000" environment: - DATABASE_URL=postgresql+psycopg2://user:password@db:5432/users deploy: replicas: 3 resources: limits: cpus: "0.5" memory: 256M db: image: postgres:15 environment: POSTGRES_USER: user POSTGRES_PASSWORD: password POSTGRES_DB: users volumes: - db_data:/var/lib/postgresql/data nginx: image: nginx:alpine ports: - "80:80" volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro depends_on: - api volumes: db_data:
Enter fullscreen mode Exit fullscreen mode

What this does:

  • api service builds the image defined earlier and runs three replicas, each limited to half a CPU core and 256 MiB RAM.
  • db runs a PostgreSQL container with persistent storage.
  • nginx acts as the entry point, forwarding HTTP traffic to the api service.

Replicating the API isolates failures, distributes load evenly, and enables the orchestrator to replace unhealthy instances automatically.

🔧 Load Balancing — Using NGINX

# nginx.conf
events { worker_connections 1024; } http { upstream fastapi_backend { server api:8000; server api:8000; server api:8000; } server { listen 80; location / { proxy_pass http://fastapi_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }
}
Enter fullscreen mode Exit fullscreen mode

What this does:

  • upstream fastapi_backend defines a round‑robin pool of the three API containers.
  • NGINX forwards each incoming request to the next container, achieving L4 load distribution.
  • Headers preserve the original client IP for logging and rate‑limiting downstream.

Key point: A lightweight reverse proxy provides deterministic request routing without requiring each FastAPI instance to manage its own concurrency limits.


🗄️ Data Layer — Externalizing State

Keeping the API stateless requires moving all persistence to external services such as PostgreSQL and Redis.

# app/database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker engine = create_engine( "postgresql+psycopg2://user:password@db:5432/users", pool_size=20, max_overflow=10, pool_timeout=30, pool_pre_ping=True,
) SessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False)
Enter fullscreen mode Exit fullscreen mode

What this does:

  • pool_size=20 creates a fixed pool of 20 connections per container, reducing connection churn.
  • max_overflow=10 allows temporary spikes beyond the pool size.
  • Pre‑ping validates connections before use, preventing “connection already closed” errors.

External databases survive container restarts, enable horizontal scaling, and provide ACID guarantees that an in‑process SQLite file cannot match. (Also read: ⚙️ Building a CI/CD pipeline with Git, Jenkins, and Docker made easy)

🛡️ Connection Pool — Managing Connections

$ docker exec -it $(docker ps -qf "name=db") psql -U user -d users -c "\
SELECT count(*) FROM pg_stat_activity;" count ------ 3
(1 row)
Enter fullscreen mode Exit fullscreen mode

This output shows three active connections from the three FastAPI workers, confirming that the pool is being reused rather than recreated per request.

Key point: Proper connection pooling reduces latency and CPU overhead, which is critical when many containers share the same database instance. (Also read: 🔧 Debug Docker OOM kills with Python)


🔧 Monitoring & Observability — Adding Metrics

Instrumenting the service with Prometheus exporters allows the system to be observed and autoscaled based on real‑time load.

# app/metrics.py
from prometheus_client import Counter, Histogram, start_http_server REQUEST_COUNT = Counter( "fastapi_requests_total", "Total HTTP requests", ["method", "endpoint"]
)
REQUEST_LATENCY = Histogram( "fastapi_request_latency_seconds", "Request latency", ["method", "endpoint"]
) def start_metrics_server(port: int = 8001): start_http_server(port) def record_request(method: str, endpoint: str, latency: float): REQUEST_COUNT.labels(method=method, endpoint=endpoint).inc() REQUEST_LATENCY.labels(method=method, endpoint=endpoint).observe(latency)
Enter fullscreen mode Exit fullscreen mode

Integrate the collector in the main app:

# app/main.py (add at top)
from .metrics import start_metrics_server, record_request
import time start_metrics_server() @app.middleware("http")
async def metrics_middleware(request, call_next): start = time.time() response = await call_next(request) latency = time.time() - start record_request(request.method, request.url.path, latency) return response
Enter fullscreen mode Exit fullscreen mode

Deploy a Prometheus server (simplified) to scrape the metrics endpoint:

# prometheus.yaml
global: scrape_interval: 15s scrape_configs: - job_name: "fastapi" static_configs: - targets: ["api:8001"]
Enter fullscreen mode Exit fullscreen mode

What this does:

  • The FastAPI process exposes metrics on port 8001.
  • Prometheus pulls those metrics every 15 seconds.
  • Grafana can visualize fastapi_request_latency_seconds to trigger horizontal pod autoscaling.

According to the official Prometheus documentation, this pull‑based model reduces overhead on the application compared to push‑based telemetry.

Key point: Exporting standard metrics enables automated scaling decisions without modifying application code.


⚖️ Performance Comparison — FastAPI vs Flask

Choosing the right framework influences the scalability ceiling. The table below summarizes key differences relevant to containerized workloads.

Attribute FastAPI Flask
Protocol ASGI (async native) WSGI (sync)
Concurrency model Event loop + multiple workers Threaded or process per request
Typical throughput (req/s) ≈ 12 k on 4‑core (wrk –duration=30s –threads=8 –connections=200) ≈ 3 k on 4‑core (same benchmark)
OpenAPI generation Automatic via Pydantic Manual or via extensions
Dependency injection Built‑in, type‑hinted Requires third‑party libraries

FastAPI provides high performance out of the box while keeping the developer experience simple, which aligns with interview expectations for a scalable design.


🟩 Final Thoughts

Designing a scalable Python API with FastAPI Docker hinges on three principles: stateless request handling, process isolation via containers, and externalized state for persistence. By combining multiple Uvicorn workers, a reverse‑proxy load balancer, and robust connection pooling, the service can grow linearly with added containers. Instrumentation through Prometheus completes the loop, giving visibility that drives automated scaling policies.


❓ Frequently Asked Questions

How many Uvicorn workers should I run per container?

Match the number of workers to the number of CPU cores allocated to the container; a common rule is one worker per core, but load testing under realistic traffic will reveal the optimal balance.

Can I replace PostgreSQL with an in‑memory store for faster reads?

In‑memory caches like Redis are ideal for frequently accessed data, but they do not provide durability or complex query capabilities, so they complement rather than replace a relational database.

Is Docker Compose sufficient for production, or should I use Kubernetes?

Docker Compose is suitable for small deployments and proof‑of‑concepts. For larger, multi‑region services, Kubernetes adds automated health checks, rolling updates, and native autoscaling, which are essential for true production scalability.


💡 Want to practise this hands-on? DigitalOcean gives new accounts $200 free credit for 60 days — enough to spin up a full Linux/Docker/Kubernetes environment at no cost.

📚 Recommended reading: Best DevOps & cloud books on Amazon — from Linux fundamentals to Kubernetes in production, curated for working engineers.

📚 References & Further Reading

  • Official FastAPI documentation — comprehensive guide to async endpoints and dependency injection: fastapi.tiangolo.com
  • Docker Engine reference manual — details on image building and container runtime: docs.docker.com
  • Prometheus official docs — explains scrape configuration and metric exposition: prometheus.io

Top comments (0)