Complete Guide to Deploying FastAPI in Production: Reliable Operations with Uvicorn Multi-Workers, Docker, and a Reverse Proxy
Goal and Big Picture
This guide takes you one step beyond development mode (uvicorn app.main:app --reload) and organizes the “path you inevitably walk” when you expose a FastAPI app to the outside world:
- What to care about in production
- Building an ASGI server setup using Uvicorn multi-workers
- Deploying with Docker
- Putting Nginx (or similar) in front as a reverse proxy (HTTPS, headers, etc.)
FastAPI’s official docs also highlight production concepts like HTTPS, startup method, restarts, replication (process count), and memory. Here we’ll translate those ideas into concrete settings and examples.
Who Benefits (Concrete Personas)
Solo developers / learners (VPS or small cloud)
- You built a FastAPI app, but you’re not sure what “production startup” should look like.
- You want stable public access for yourself or a small number of users.
- You’ve touched Docker, but want the “FastAPI + Docker” shape.
→ You’ll get a minimal, practical setup with Uvicorn multi-workers + Docker, including commands and Dockerfiles.
Small-team backend engineers
- Your team chose FastAPI and you want a repeatable production deployment flow.
- You’re considering a reverse proxy (Nginx/Traefik) in front of one or more servers.
- You want a clear mental model for reload/restart, process count, health checks, and ops.
→ You’ll see the separation of responsibilities between Uvicorn, proxy, and orchestration.
SaaS teams / startups (future scaling)
- You expect to scale later with Kubernetes or managed container services.
- You want to start simple (single container) while keeping “replication later” in mind.
- You’d rather avoid complex Gunicorn setups unless necessary.
→ Modern Uvicorn includes built-in multi-process execution, so you can keep the base architecture simple and scale later.
Accessibility Notes (Readability & Understanding)
- Structure: “overview → Uvicorn production → Docker → reverse proxy → ops/monitoring → roadmap”
- Terms: ASGI server / worker / reverse proxy are explained briefly at first use and then reused consistently
- Code: Configs are split into small blocks with minimal comments to reduce cognitive load
- Assumed reader: You’ve finished the FastAPI tutorial once, but each section can be read independently
1. Production Deployment Concepts (What to Decide)
Before commands, clarify what you’re optimizing for.
1.1 Key points to consider
-
HTTPS
- Encrypt client communication.
- Often handled by a reverse proxy like Nginx/Traefik with certificate automation.
-
Startup & restart
- The app should come back after server reboot.
- If the process dies, something should restart it (systemd / container orchestration).
-
Replication (process count)
- Use multiple processes to utilize CPU cores.
- Either via Uvicorn
--workersor via “more containers/pods” at the cluster level.
-
Memory & resources
- Each worker consumes memory; the number of workers is bounded by RAM.
- Decide how many processes you can afford per host.
A good mental model is three layers:
FastAPI app (Uvicorn) / Reverse proxy (Nginx, etc.) / OS or Orchestrator
This keeps roles clear and prevents “mystery configs.”
2. Starting Uvicorn for Production: Multi-Workers and Options
In the past, “Gunicorn + UvicornWorker” was common. But recent Uvicorn versions (often cited around 0.30+) include a built-in multi-process supervisor, so --workers can be enough.
2.1 Minimal production command
Common development command:
uvicorn app.main:app --reload
A simple production-leaning example:
uvicorn app.main:app \
--host 0.0.0.0 \
--port 8000 \
--workers 4
Key differences:
- Remove
--reload(file watching is unnecessary and risky in production) - Use
--host 0.0.0.0so external clients (or containers) can reach it - Set
--workersbased on CPU cores (often “cores” to “2× cores” as a starting point)
2.2 Additional useful options
-
--limit-concurrency- Caps concurrent requests to prevent runaway CPU/memory usage.
-
--limit-max-requests- Recycles workers after a number of requests to mitigate memory leaks over time.
-
--proxy-headers- Needed behind a reverse proxy so the app understands client IP and original scheme.
Example:
uvicorn app.main:app \
--host 0.0.0.0 \
--port 8000 \
--workers 4 \
--proxy-headers \
--limit-max-requests 10000
3. Containerizing FastAPI with Docker
Many production deployments run FastAPI inside Docker. FastAPI docs also provide Docker templates (“FastAPI in Containers – Docker”).
3.1 A minimal Dockerfile (FastAPI CLI style)
A simple example inspired by the docs, using FastAPI CLI:
FROM python:3.11-slim
WORKDIR /code
COPY ./requirements.txt /code/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
COPY ./app /code/app
CMD ["fastapi", "run", "app/main.py", "--port", "80"]
Example requirements.txt:
fastapi[standard]>=0.113.0,<0.114.0
uvicorn>=0.30.0,<0.31.0
sqlalchemy>=2.0.0,<3.0.0
Build & run:
docker build -t my-fastapi-app .
docker run -d --name my-fastapi -p 8000:80 my-fastapi-app
Now you can access the app at http://localhost:8000.
3.2 Switching the container to start Uvicorn directly
You can also start Uvicorn as the container command:
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80", "--workers", "4"]
A common operational principle is “one container = one process,” while scaling is done by running multiple containers. In practice, many teams still run a few workers inside a container via --workers, and scale containers/pods later with Kubernetes/ECS as needed.
4. Pairing with a Reverse Proxy (Nginx / Traefik)
In many deployments, FastAPI (Uvicorn) runs as an HTTP backend, with Nginx/Traefik in front to handle HTTPS termination and routing.
4.1 Why put a reverse proxy in front?
-
HTTPS certificate management
- Centralize Let’s Encrypt renewals and TLS settings.
-
Static file delivery
- Serve images/CSS/JS efficiently via Nginx.
-
Load balancing
- Distribute traffic across multiple FastAPI containers/hosts.
-
Security and limits
- IP allowlists, rate limits, basic auth, etc. at the edge.
4.2 Simple Nginx config (single host)
Assuming Uvicorn listens on 127.0.0.1:8000:
server {
listen 80;
server_name example.com;
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;
}
}
Run Uvicorn with --proxy-headers so it interprets these forwarded headers properly.
4.3 Nginx + FastAPI in containers
With Docker Compose, you usually run Nginx and FastAPI as separate containers. Nginx can forward to http://fastapi:8000 using Docker’s service name resolution.
5. Environment Variables and Configuration Management
Production setups live or die by configuration hygiene.
5.1 Things that vary by environment
- Database URL
- API keys/secrets
- Debug flag
- Log level
Keep them out of code—load from environment variables.
5.2 Using pydantic-settings
# app/core/settings.py
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
app_name: str = "My FastAPI App"
environment: str = "dev"
database_url: str
secret_key: str
log_level: str = "info"
class Config:
env_file = ".env"
settings = Settings()
Typical pattern:
- Production: real environment variables (no
.envfile in the image) - Development:
.envfor convenience
Example container run:
docker run -d \
-e ENVIRONMENT=prod \
-e DATABASE_URL=postgresql+psycopg://... \
-e SECRET_KEY=... \
-p 8000:80 my-fastapi-app
6. Operations: Monitoring and Restart Strategy
After “how to start it,” the next question is “what happens when it breaks?”
6.1 Running directly on a VPS: systemd
If you run Uvicorn on the host (no Docker), systemd is a common approach:
[Unit]
Description=My FastAPI application
After=network.target
[Service]
User=www-data
WorkingDirectory=/opt/myapp
ExecStart=/usr/local/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4 --proxy-headers
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
Enable and start:
systemctl enable my-fastapi
systemctl start my-fastapi
6.2 Orchestrators: restart, scaling, health checks
In Docker Compose / Kubernetes / ECS, you configure:
- Auto-restart on failure
- Replica count
- Health checks (e.g.,
/healthendpoint)
A simple health check endpoint:
# app/api/health.py
from fastapi import APIRouter
router = APIRouter()
@router.get("/health", tags=["health"])
def health_check():
return {"status": "ok"}
7. Mini Reference Setup: Docker + Uvicorn + Nginx
A small example that combines everything.
7.1 Directory layout
project/
app/
__init__.py
main.py
api/
health.py
...
requirements.txt
docker-compose.yml
Dockerfile
nginx/
default.conf
7.2 Dockerfile (FastAPI container)
FROM python:3.11-slim
WORKDIR /code
COPY ./requirements.txt /code/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
COPY ./app /code/app
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4", "--proxy-headers"]
7.3 Nginx config
server {
listen 80;
server_name _;
location / {
proxy_pass http://fastapi: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;
}
}
7.4 docker-compose.yml
version: "3.9"
services:
fastapi:
build: .
container_name: fastapi-app
environment:
- ENVIRONMENT=prod
- DATABASE_URL=sqlite:///./app.db
expose:
- "8000"
nginx:
image: nginx:1.27
container_name: nginx-proxy
ports:
- "80:80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- fastapi
Run:
docker-compose up -d --build
Traffic flow:
http://localhost/→ Nginx- Nginx →
fastapi:8000 - FastAPI(Uvicorn) handles the request
8. Common Pitfalls and Fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Client IP always shows as 127.0.0.1 | Proxy headers aren’t interpreted | Ensure Nginx sets headers and start Uvicorn with --proxy-headers |
| Mixed HTTP/HTTPS links | Proxy and backend disagree about scheme | Set X-Forwarded-Proto in proxy and ensure the app respects it |
| Errors only after deploy | Env vars or DB settings differ | Centralize config (Settings + ENV) and run locally in a “prod-like” mode |
| Crashes under load | Too many/few workers, RAM pressure | Watch CPU/RAM/latency metrics; tune --workers and/or replicas gradually |
| “Which architecture is correct?” | Too many options | Start with the minimal shape: Uvicorn multi-workers + Docker + simple reverse proxy |
9. A Gradual Roadmap (Move Toward Production Step by Step)
-
Local development
- Use
--reload, build features, add tests/logging.
- Use
-
Try production-like Uvicorn locally
- Remove
--reload, test--workersand--limit-max-requests.
- Remove
-
Containerize with Docker
- Build a reproducible image, try Compose with DB if needed.
-
Add a reverse proxy
- Start with HTTP routing; add HTTPS termination and static files later.
-
Deploy to production
- VPS or cloud (ECS/GKE/etc.), add health checks, logs, metrics, load testing.
-
Scale later
- Move to Kubernetes when needed, keeping “simple container, scale at the platform layer” as the guiding principle.
Summary
- Production deployment is mostly about HTTPS, startup/restart behavior, replication, and memory.
- Modern Uvicorn supports multi-worker operation via
--workers, enabling a simpler “no Gunicorn required” baseline in many cases. - Docker improves reproducibility and reduces environment drift; a reverse proxy (Nginx/Traefik) makes it easier to handle HTTPS, routing, static files, and edge-level security.
- You don’t need to start with a perfect architecture. Begin with the minimal stable shape—Uvicorn multi-workers + Docker—and add proxy/orchestration as your needs grow.

