How to Deploy a FastAPI Application on a VPS: Production Setup with Gunicorn, Nginx, and SSL
FastAPI is Python’s highest-performance web framework — ASGI-based, async-first, with automatic OpenAPI documentation. Production deployment on a VPS combines Gunicorn (the process manager) with Uvicorn workers (the async ASGI server), fronted by Nginx for SSL termination and request queuing. This guide sets up a production-ready FastAPI deployment with systemd, zero-downtime reloads, and environment-variable security.
Why Gunicorn + Uvicorn Workers
- Uvicorn: ASGI server that handles async Python requests efficiently — needed for FastAPI
- Gunicorn: Production process manager that spawns multiple Uvicorn workers (one per CPU core), restarts crashed workers, and handles graceful reloads
- Together: Multi-process async request handling with production-grade process supervision
Worker Count Formula
# Recommended: (CPU cores × 2) + 1
# 1 vCPU → 3 workers
# 2 vCPU → 5 workers
# 4 vCPU → 9 workers
python3 -c "import multiprocessing; print(multiprocessing.cpu_count() * 2 + 1)"
Step 1: Server Preparation
sudo apt update
sudo apt install -y python3.12 python3.12-venv nginx certbot python3-certbot-nginx
# Create dedicated user for the API process
sudo adduser --disabled-password --gecos "" apiuser
sudo mkdir -p /var/www/myapi
sudo chown apiuser:apiuser /var/www/myapi
Step 2: Deploy Application Code
# From your local machine:
rsync -avz --exclude='.venv' --exclude='__pycache__' \
./myapi/ deploy@YOUR_VPS_IP:/var/www/myapi/
# On the VPS:
cd /var/www/myapi
python3.12 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
pip install "gunicorn[setproctitle]" "uvicorn[standard]"
Step 3: Example FastAPI Application
# main.py
from fastapi import FastAPI, HTTPException
from contextlib import asynccontextmanager
from typing import AsyncGenerator
import os
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator:
# Startup: initialize DB connection pools, etc.
yield
# Shutdown: close connections
app = FastAPI(
title="My API",
version="1.0.0",
lifespan=lifespan,
# Hide docs in production
docs_url="/docs" if os.getenv("ENVIRONMENT") != "production" else None,
redoc_url=None if os.getenv("ENVIRONMENT") == "production" else "/redoc",
)
@app.get("/health")
async def health_check():
return {"status": "ok", "version": "1.0.0"}
@app.get("/api/items/{item_id}")
async def get_item(item_id: int):
if item_id not in range(1, 100):
raise HTTPException(status_code=404, detail="Item not found")
return {"id": item_id, "name": f"Item {item_id}"}
Step 4: Secure Environment Variables
sudo mkdir -p /etc/myapi
sudo nano /etc/myapi/production.env
DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/mydb
SECRET_KEY=your-secret-key-minimum-32-characters-long
ENVIRONMENT=production
LOG_LEVEL=info
ALLOWED_ORIGINS=https://yourdomain.com,https://www.yourdomain.com
sudo chmod 600 /etc/myapi/production.env
sudo chown root:apiuser /etc/myapi/production.env
Step 5: Gunicorn Configuration File
nano /var/www/myapi/gunicorn.conf.py
import multiprocessing
# Workers
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "uvicorn.workers.UvicornWorker"
worker_connections = 1000
# Server
bind = "127.0.0.1:8000"
backlog = 2048
# Timeouts
timeout = 120
keepalive = 5
graceful_timeout = 30 # Time to finish in-flight requests on reload
# Logging
accesslog = "/var/log/myapi/access.log"
errorlog = "/var/log/myapi/error.log"
loglevel = "info"
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s'
# Process naming
proc_name = "myapi"
# Restart workers periodically to prevent memory leaks
max_requests = 1000
max_requests_jitter = 50
Step 6: systemd Service
sudo mkdir -p /var/log/myapi
sudo chown apiuser:apiuser /var/log/myapi
sudo nano /etc/systemd/system/myapi.service
[Unit]
Description=FastAPI Application (myapi)
Documentation=https://fastapi.tiangolo.com
After=network.target postgresql.service
[Service]
User=apiuser
Group=apiuser
WorkingDirectory=/var/www/myapi
EnvironmentFile=/etc/myapi/production.env
ExecStart=/var/www/myapi/.venv/bin/gunicorn \
-c /var/www/myapi/gunicorn.conf.py \
main:app
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=30
Restart=on-failure
RestartSec=5s
# Security hardening
NoNewPrivileges=true
PrivateTmp=true
MemoryMax=1G
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now myapi
sudo systemctl status myapi
# Verify it responds
curl http://127.0.0.1:8000/health
Step 7: Nginx Configuration
sudo nano /etc/nginx/sites-available/myapi
upstream fastapi_backend {
server 127.0.0.1:8000;
keepalive 32;
}
server {
listen 80;
server_name api.yourdomain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name api.yourdomain.com;
ssl_protocols TLSv1.2 TLSv1.3;
client_max_body_size 50M;
# Rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/m;
limit_req zone=api burst=60 nodelay;
location / {
proxy_pass http://fastapi_backend;
proxy_http_version 1.1;
proxy_set_header Connection ""; # Enable keepalive to upstream
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_read_timeout 120s;
proxy_connect_timeout 10s;
}
}
sudo ln -s /etc/nginx/sites-available/myapi /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d api.yourdomain.com
Zero-Downtime Deployment
# Deploy script (run from local machine or CI/CD)
rsync -avz --exclude='.venv' --exclude='__pycache__' \
./myapi/ deploy@YOUR_VPS_IP:/var/www/myapi/
ssh deploy@YOUR_VPS_IP << 'EOF'
cd /var/www/myapi
source .venv/bin/activate
pip install -r requirements.txt --quiet
# Graceful reload: Gunicorn finishes in-flight requests before
# starting new workers with updated code — zero dropped requests
sudo systemctl reload myapi
EOF
Getting Started
FastAPI with 3 Uvicorn workers handles 1,000+ concurrent API requests on a 2 vCPU VPS. Worker count scales automatically with CPU count via the configuration formula. USA VPS plans at VPS.DO provide dedicated KVM vCPUs ensuring consistent Gunicorn multi-process performance without noisy-neighbor CPU contention.
Conclusion
FastAPI in production on a VPS uses Gunicorn as the process manager, Uvicorn workers for async request handling, Nginx as the SSL-terminating reverse proxy with rate limiting, and systemd for service management and auto-restart. Graceful reload via systemctl reload deploys new code without dropping a single in-flight request. The stack handles thousands of concurrent API requests with sub-10ms response times on modest VPS hardware.