
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Running a Python API locally with uvicorn main:app --reload is fine for development, but that command will fail under real traffic or crash silently without process management. To safely deploy FastAPI to production, you must wrap the ASGI application in an industrial-grade server manager, containerize it for consistency, and place a reverse proxy in front for TLS and buffering. This guide walks through the exact stack I use for client projects: Dockerized multi-stage builds, Gunicorn-managed Uvicorn workers, Nginx as the edge, and systemd for host-level resilience.
How do you containerize FastAPI for production deployments?
The most common mistake when teams first deploy FastAPI to production is shipping the entire development environment—debug tools, hot reloaders, test dependencies—into the runtime image. This bloats attack surface and slows cold starts. Use a multi-stage Docker build to separate build-time dependencies from the slim runtime.
Multi-stage Dockerfile for FastAPI
This Dockerfile installs dependencies in a builder stage, then copies only the compiled packages and application code into a minimal python-slim runtime. It runs as a non-root user, which is mandatory for any audit-ready infrastructure (SOC 2, ISO 27001).
# Stage 1: Builder
FROM python:3.12-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# Stage 2: Runtime
FROM python:3.12-slim
RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /sbin/nologin appuser
COPY --from=builder /install /usr/local
WORKDIR /app
COPY ./app ./app
USER appuser
EXPOSE 8000
CMD ["gunicorn", "app.main:app", "-k", "uvicorn.workers.UvicornWorker", "-w", "4", "--bind", "0.0.0.0:8000"] - Non-root execution: The
appuserprevents privilege escalation if the container is compromised. If you need to write logs or temp files, create explicit directories with correct ownership during the build. - No dev dependencies: Your
requirements.txtshould be generated viapip-compileorpoetry exportwith only production packages. Never copyrequirements-dev.txtinto the runtime stage. - Health checks: Add a
/healthendpoint in FastAPI and configure Docker’sHEALTHCHECKinstruction or your orchestrator’s liveness probe to hit it every 30 seconds.
If you are new to securing Ubuntu hosts that run these containers, review the Ubuntu security hardening guide before opening ports. Host-level misconfigurations undermine even perfectly secured containers.
Why use Gunicorn with Uvicorn workers instead of standalone Uvicorn?
Uvicorn alone is an excellent ASGI server, but it is designed as a single-process worker. In production, you need process management: automatic worker restarts on crashes, graceful shutdowns during deploys, and concurrency across CPU cores. Gunicorn provides this supervision layer while delegating actual request handling to Uvicorn workers.
Recommended Gunicorn configuration
Create a gunicorn.conf.py rather than passing flags inline. This makes tuning reproducible and version-controlled:
# gunicorn.conf.py
workers = 4 # Rule of thumb: (2 x CPU cores) + 1 for CPU-bound; adjust for async I/O
worker_class = "uvicorn.workers.UvicornWorker"
bind = "0.0.0.0:8000"
keepalive = 65
graceful_timeout = 30
timeout = 120
accesslog = "-"
errorlog = "-"
loglevel = "info" A common mistake is setting workers too high for async workloads. FastAPI with Uvicorn workers is asynchronous; four workers can handle hundreds of concurrent connections if your endpoints are I/O-bound. Only increase workers if CPU profiling shows saturation. For guidance on right-sizing resources in orchestrated environments, see Kubernetes resource limits and requests.
How do you configure Nginx as a reverse proxy for FastAPI?
Nginx sits between the internet and your Gunicorn container because it handles concerns that Python servers should not: TLS termination, HTTP/2, request buffering (protecting against slowloris attacks), static file serving, and rate limiting. Never expose Gunicorn directly to the public internet.
Nginx configuration for FastAPI
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;
client_max_body_size 10m;
location / {
proxy_pass http://127.0.0.1:8000;
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;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_read_timeout 120s;
}
location /static/ {
alias /var/www/fastapi/static/;
expires 30d;
add_header Cache-Control "public, immutable";
}
}
server {
listen 80;
server_name api.example.com;
return 301 https://$host$request_uri;
} - Proxy headers: Without
X-Forwarded-ForandX-Forwarded-Proto, FastAPI sees all requests as coming from localhost over HTTP. This breaks IP-based rate limiting, logging, and secure cookie flags. - Connection header: Setting
Connection ""enables keep-alive between Nginx and Gunicorn, reducing TCP handshake overhead on every request. - TLS setup: Use Certbot for free, automated certificates. Follow the steps in setting up free SSL with Let's Encrypt for a hardened configuration.
How do you manage FastAPI services with systemd or Docker Compose?
Containers need a supervisor. On a single VPS, systemd manages the Docker container lifecycle. In orchestrated environments, Kubernetes handles this—but many Nepal-based startups and SMEs still run single-server deployments where systemd is the right tool.
Systemd unit for Dockerized FastAPI
[Unit]
Description=FastAPI Production Container
Requires=docker.service
After=docker.service
[Service]
Restart=always
RestartSec=5
ExecStartPre=-/usr/bin/docker stop fastapi-prod
ExecStartPre=-/usr/bin/docker rm fastapi-prod
ExecStart=/usr/bin/docker run \
--name fastapi-prod \
--network host \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
-e DATABASE_URL=postgresql://user:pass@db:5432/app \
myregistry/fastapi-app:2026.08.20
ExecStop=/usr/bin/docker stop fastapi-prod
[Install]
WantedBy=multi-user.target | Approach | Best For | Pros | Cons |
|---|---|---|---|
| Systemd + Docker | Single VPS, low budget | Simple, no orchestrator overhead, native logging | No auto-scaling, manual rolling deploys |
| Docker Compose | Local staging, small teams | Multi-container networking, easy config | Not production-grade for HA, limited secrets mgmt |
| Kubernetes (EKS/GKE/AKS) | Scale, compliance, multi-service | Auto-healing, HPA, GitOps, audit trails | Operational complexity, cost floor ~$70+/mo |
For teams ready to move beyond single-server, blue-green and canary deploys on Kubernetes eliminate downtime during FastAPI releases. But do not adopt K8s prematurely; systemd with Docker is production-viable for thousands of RPM.
What monitoring and logging practices are essential for production FastAPI?
Deploying without observability means you cannot distinguish between a healthy idle API and a silently failing one. FastAPI exposes metrics-friendly hooks, but you must wire them explicitly.
- Structured logging: Configure
structlogorpython-json-loggerso every log line is parseable JSON. Includerequest_id,user_id, andduration_msfields. See structured logging best practices for field naming conventions. - Prometheus metrics: Use
prometheus-fastapi-instrumentatorto auto-export request latency histograms, error rates, and active connections. Expose/metricson a private port or protect it with basic auth. - Health endpoints: Implement
/health/live(process alive) and/health/ready(DB reachable, cache connected). Orchestrators use these differently; conflating them causes cascading failures. - Tracing: Instrument with OpenTelemetry. Even if you do not run Jaeger yet, having trace context propagation in place now avoids costly retrofitting later.
In my experience helping Nepal-based fintech companies prepare for compliance audits, structured logs and metric baselines are the first evidence requested. Build this in before your first production incident, not after.
Deploy FastAPI to Production With Confidence
The stack outlined here—multi-stage Docker, Gunicorn-managed Uvicorn workers, Nginx reverse proxy, systemd or Kubernetes orchestration, and structured observability—is battle-tested across dozens of production APIs. Do not skip layers: each solves a specific failure mode that uvicorn --reload simply does not address. Start with the Dockerfile and Gunicorn config from this guide, validate with load testing using k6, and iterate based on real metrics. If your team needs help designing a production-grade deployment pipeline or preparing infrastructure for compliance audits, reach out to discuss your architecture.