FastAPI is an ASGI framework, so it needs an ASGI server — Uvicorn — rather than a WSGI server like plain Gunicorn. In production you run several Uvicorn worker processes under systemd, with Nginx in front for HTTPS. Here’s the full setup on Ubuntu.
1. Project and virtualenv
sudo apt update
sudo apt install -y python3-venv nginx git
sudo adduser --system --group --home /srv/api api
sudo -u api git clone https://github.com/you/api.git /srv/api/app
sudo -u api python3 -m venv /srv/api/venv
sudo -u api /srv/api/venv/bin/pip install -r /srv/api/app/requirements.txt "uvicorn[standard]"
uvicorn[standard] adds uvloop and httptools for better performance, plus WebSocket support.
Assume main.py contains:
from fastapi import FastAPI
app = FastAPI()
@app.get("/health")
def health():
return {"status": "ok"}
Test:
cd /srv/api/app
sudo -u api /srv/api/venv/bin/uvicorn main:app --host 127.0.0.1 --port 8000
curl http://127.0.0.1:8000/health
2. How many workers?
Uvicorn can run multiple worker processes itself:
uvicorn main:app --host 127.0.0.1 --port 8000 --workers 4
Alternatively, Gunicorn can manage Uvicorn workers, adding features like graceful reloads on HUP and worker timeouts:
gunicorn main:app -k uvicorn.workers.UvicornWorker -w 4 -b 127.0.0.1:8000
(Newer setups may use the separate uvicorn-worker package’s worker class.) Either approach works; start with one worker per CPU core and adjust. Async endpoints handle many concurrent I/O-bound requests per worker, but blocking code inside async def stalls the whole worker — use def endpoints (run in a thread pool) or async libraries.
3. systemd service
/etc/systemd/system/api.service:
[Unit]
Description=FastAPI app
After=network.target
[Service]
User=api
Group=api
WorkingDirectory=/srv/api/app
EnvironmentFile=/srv/api/.env
ExecStart=/srv/api/venv/bin/uvicorn main:app \
--host 127.0.0.1 --port 8000 \
--workers 4 \
--proxy-headers --forwarded-allow-ips=127.0.0.1
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
--proxy-headers with --forwarded-allow-ips makes FastAPI trust X-Forwarded-For and X-Forwarded-Proto from Nginx — so request.client.host is the real visitor IP and generated URLs use https.
sudo systemctl daemon-reload
sudo systemctl enable --now api
journalctl -u api -f
4. Nginx
upstream api_backend {
server 127.0.0.1:8000;
keepalive 16;
}
server {
listen 80;
server_name api.example.com;
client_max_body_size 10M;
location / {
proxy_pass http://api_backend;
proxy_http_version 1.1;
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_set_header Connection "";
}
location /ws/ {
proxy_pass http://api_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 1h;
}
}
The /ws/ block is only needed if you use WebSockets.
sudo ln -s /etc/nginx/sites-available/api /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d api.example.com
5. Docs in production
FastAPI serves interactive docs at /docs and /redoc. For a public API that’s often fine; for internal APIs, disable them or protect them:
app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)
6. Background work
BackgroundTasks runs small tasks after the response, in the same worker. For heavy or reliable jobs, use a real queue (Celery, RQ, arq) with Redis, running as its own systemd service. See Celery with Redis workers.
7. Deploy updates
cd /srv/api/app
sudo -u api git pull --ff-only
sudo -u api /srv/api/venv/bin/pip install -r requirements.txt
sudo systemctl restart api
A restart briefly drops in-flight requests. For graceful reloads, use Gunicorn with Uvicorn workers and systemctl reload (HUP), or run two instances behind the Nginx upstream and restart them one at a time.
Or run it in Docker
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt "uvicorn[standard]"
COPY . .
USER nobody
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4", "--proxy-headers", "--forwarded-allow-ips=*"]
Publish on localhost (-p 127.0.0.1:8000:8000) and keep Nginx on the host. --forwarded-allow-ips=* is acceptable only because the container is reachable solely through Nginx.
Troubleshooting
- 502 from Nginx — the service isn’t running or listens elsewhere;
systemctl status api. - Client IP is always 127.0.0.1 — missing
--proxy-headers/--forwarded-allow-ips. - Redirects to
http://— same cause; FastAPI doesn’t know the original scheme. - Throughput collapses under load — blocking calls in
async defendpoints; use async drivers or plaindef. - WebSocket closes after 60 s — raise
proxy_read_timeoutfor the WebSocket location.
Related: Gunicorn vs Uvicorn, Flask with Gunicorn and Python virtual environments. For APIs that call AI services from Asia, our Tokyo VPS is in a region supported by the major AI APIs.