Deploying With Uvicorn and Nginx

Introduction

uvicorn --reload is for local development only. Production runs Uvicorn workers or Gunicorn with UvicornWorker behind Nginx for HTTPS, static files, and WebSocket upgrades. This chapter deploys a FastAPI app on Linux with 12-factor environment config.

Prerequisites

Development vs Production

uvicorn --reloadProduction Uvicorn/Gunicorn
PurposeLocal devServe real traffic
Auto reloadYesNo
WorkersOneMultiple processes
Bindlocalhost OKOften 127.0.0.1 behind Nginx

Warning

Never Use --reload in Production

Reload watches files and is not hardened for public exposure.

Install Production Server

bash
pip install "uvicorn[standard]" gunicorn

Add to requirements.txt.

Run Uvicorn Directly

bash
export ENVIRONMENT=production
export SECRET_KEY="your-production-secret"
export DATABASE_URL="mysql+pymysql://user:pass@127.0.0.1/app?charset=utf8mb4"
 
uvicorn app.main:app --host 127.0.0.1 --port 8000 --workers 4

Code explanation:

  • --workers 4 — multiple processes (often 2 × CPU + 1 starting point)
  • Bind 127.0.0.1 — Nginx faces the public internet

Gunicorn + UvicornWorker

Recommended by many teams for process management:

bash
gunicorn app.main:app \
  -k uvicorn.workers.UvicornWorker \
  -w 4 \
  -b 127.0.0.1:8000 \
  --timeout 120 \
  --access-logfile /var/log/fastapi-app/access.log \
  --error-logfile /var/log/fastapi-app/error.log

Code explanation:

  • -k uvicorn.workers.UvicornWorker runs ASGI app under Gunicorn master
  • --timeout aligns with long uploads—match Nginx proxy_read_timeout

Health Check Route

python
@app.get("/health")
def health():
    return {"status": "ok"}

Load balancers probe GET /health—no auth, minimal work.

systemd Service

/etc/systemd/system/fastapi-app.service:

Enable:

bash
sudo systemctl daemon-reload
sudo systemctl enable --now fastapi-app

Run migrations before start:

bash
alembic upgrade head

Nginx Reverse Proxy

/etc/nginx/sites-available/fastapi-app:

WebSocket locations add:

nginx
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

Details: Nginx reverse proxy, proxy Spring/Node pattern.

HTTPS: Let's Encrypt.

Trust Forwarded Headers

Behind proxy, configure if you need client IP in app:

python
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
app.add_middleware(ProxyHeadersMiddleware, trusted_hosts=["127.0.0.1"])

Only when you control the proxy.

Environment File

/etc/fastapi-app/env:

bash
ENVIRONMENT=production
SECRET_KEY=...
DATABASE_URL=...
CORS_ORIGINS=https://app.example.com

Deploy Checklist

  • No --reload
  • Secrets in env file
  • alembic upgrade head
  • Gunicorn/Uvicorn on localhost
  • Nginx proxy_set_header set
  • HTTPS enabled
  • Docs disabled in production if required

FAQ

502 Bad Gateway?

App not running or wrong proxy_pass port—systemctl status fastapi-app.

Workers vs async?

Each worker is a process—async helps within worker; scale workers for CPU cores.

Windows production?

Uvicorn directly; IIS or reverse proxy in front—Linux preferred for this track.

Static files?

Nginx alias faster than StaticFiles in Python.

Zero-downtime deploy?

Blue/green or rolling with two ports—advanced ops.

Log rotation?

logrotate on /var/log/fastapi-app/ or ship to centralized logging.