How to Deploy a FastAPI Application on a VPS: Production Setup with Gunicorn, Nginx, and SSL

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.

Fast • Reliable • Affordable VPS - DO It Now!

Get top VPS hosting with VPS.DO’s fast, low-cost plans. Try risk-free with our 7-day no-questions-asked refund and start today!