Technology Encyclopedia Home >How to Deploy a FastAPI Application on a Cloud Server — Uvicorn, Gunicorn, and Nginx

How to Deploy a FastAPI Application on a Cloud Server — Uvicorn, Gunicorn, and Nginx

I switched a REST API project from Flask to FastAPI about a year ago, mostly for the automatic documentation and native async support. The development experience was immediately better.

Deployment, though, is slightly different from Flask. FastAPI uses an ASGI server (Uvicorn) instead of WSGI (Gunicorn with sync workers). The first time I deployed it, I just ran Uvicorn directly and called it done. That worked, but a single Uvicorn process doesn't use multiple CPU cores. Running Uvicorn workers under Gunicorn's process management is the right production setup — one Gunicorn process manages multiple Uvicorn workers, and all your CPU cores actually contribute.

This guide covers the complete setup, plus the async/sync gotcha I hit that wasted an afternoon.

I run this on Tencent Cloud Lighthouse with Ubuntu 22.04. FastAPI with Uvicorn workers is notably efficient — the 2 GB RAM plan handles production API workloads comfortably. Lighthouse's predictable monthly pricing (fixed cost regardless of request volume) works well for API hosting where you can't always predict traffic patterns. As your API grows, you can upgrade the instance spec from the control panel without re-provisioning — the same IP, same configuration, just more CPU and RAM. The OrcaTerm browser terminal also makes it easy to monitor Gunicorn worker processes and tail logs from anywhere.


Table of Contents

  1. The FastAPI Production Stack
  2. Prerequisites
  3. Part 1 — Server Setup
  4. Part 2 — Install Python and Create Virtual Environment
  5. Part 3 — Deploy the FastAPI Application
  6. Part 4 — Configure Gunicorn with Uvicorn Workers
  7. Part 5 — Create a systemd Service
  8. Part 6 — Configure Nginx
  9. Part 7 — Enable HTTPS
  10. Part 8 — Interactive API Docs in Production
  11. The Gotcha: Async vs Sync Workers
  12. Common Commands

Key Takeaways

  • FastAPI requires an ASGI server (Uvicorn) — not a WSGI server like standard Gunicorn
  • Production: Gunicorn manages multiple Uvicorn workers for multi-core utilization
  • Sync database operations in async FastAPI must use run_in_executor to avoid blocking
  • FastAPI auto-generates /docs — protect or disable in production
  • Workers formula: (2 × CPU cores) + 1 as starting point

Frequently Asked Questions {#faq}

What is Uvicorn and why use it with FastAPI?
Uvicorn is an ASGI server — the async equivalent of Gunicorn (WSGI). FastAPI is an ASGI framework that requires an ASGI server to run. Uvicorn handles async I/O efficiently, which is key to FastAPI's performance.

Should I use Uvicorn directly or Gunicorn with Uvicorn workers?
For production, use Gunicorn managing multiple Uvicorn workers. Gunicorn provides multi-process management; Uvicorn workers handle async requests. This uses all CPU cores while maintaining process management.

How many workers should I run for a FastAPI app?
Start with (2 × CPU cores) + 1. For CPU-bound work (ML inference, data processing), match workers to CPU count. For I/O-bound work (database calls, API calls), more workers can be beneficial.

Does FastAPI run synchronous database operations with async correctly?
If you use synchronous database drivers (like standard SQLAlchemy), run them in a thread pool executor. Use run_in_executor or asyncio.to_thread() to avoid blocking the event loop.

How does FastAPI's automatic documentation work in production?
FastAPI generates /docs (Swagger UI) and /redoc endpoints automatically. In production, you may want to disable them or protect them with HTTP Basic Auth to avoid exposing your API structure.

The FastAPI Production Stack {#stack}

Internet → Nginx → Gunicorn (UvicornWorker) → FastAPI app
Component Role
Nginx SSL termination, static files, reverse proxy
Gunicorn Multi-process manager
UvicornWorker ASGI server per worker (handles async)
FastAPI Your application

FastAPI is an ASGI framework (Asynchronous Server Gateway Interface), not WSGI. This means you need an ASGI-compatible server — Uvicorn — rather than a WSGI server like the standard Gunicorn workers. The recommended production setup is Gunicorn managing multiple Uvicorn worker processes.


Prerequisites {#prerequisites}

Requirement Notes
Cloud server Tencent Cloud Lighthouse Ubuntu 22.04
Python 3.9+ FastAPI requires Python 3.7+; 3.11+ for best performance
A FastAPI app We'll create a minimal example

Part 1 — Server Setup {#part-1}

ssh ubuntu@YOUR_SERVER_IP
sudo apt update && sudo apt upgrade -y
sudo apt install -y python3 python3-pip python3-venv git nginx

sudo ufw allow ssh
sudo ufw allow 'Nginx Full'
sudo ufw enable

Part 2 — Install Python and Create Virtual Environment {#part-2}

mkdir -p ~/apps/myapi && cd ~/apps/myapi

python3 -m venv venv
source venv/bin/activate

# Install FastAPI, Uvicorn, and Gunicorn
pip install fastapi uvicorn[standard] gunicorn

# If you have a requirements.txt:
# pip install -r requirements.txt

Part 3 — Deploy the FastAPI Application {#part-3}

Option A: Clone from GitHub

git clone https://github.com/your-username/your-fastapi-app.git .
source venv/bin/activate
pip install -r requirements.txt

Option B: Create a minimal example

cat > ~/apps/myapi/main.py << 'EOF'
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional

app = FastAPI(
    title="My API",
    description="Running on Tencent Cloud Lighthouse",
    version="1.0.0"
)

class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float

@app.get("/")
async def root():
    return {"message": "API is running", "status": "ok"}

@app.get("/health")
async def health():
    return {"status": "healthy"}

@app.post("/items/")
async def create_item(item: Item):
    return {"item": item, "message": "Item created"}

@app.get("/items/{item_id}")
async def read_item(item_id: int):
    return {"item_id": item_id}
EOF

Test it locally:

source venv/bin/activate
uvicorn main:app --host 0.0.0.0 --port 8000

# Visit http://YOUR_SERVER_IP:8000 and http://YOUR_SERVER_IP:8000/docs
# Ctrl+C to stop

Part 4 — Configure Gunicorn with Uvicorn Workers {#part-4}

Create a Gunicorn config file:

cat > ~/apps/myapi/gunicorn_conf.py << 'EOF'
from multiprocessing import cpu_count

# Server socket
bind = "unix:/run/myapi.sock"
umask = 0o007

# Workers
# Use UvicornWorker for ASGI (FastAPI) apps
worker_class = "uvicorn.workers.UvicornWorker"
workers = (cpu_count() * 2) + 1

# Timeouts
timeout = 120
keepalive = 5

# Logging
errorlog = "/var/log/myapi/error.log"
accesslog = "/var/log/myapi/access.log"
loglevel = "info"

# Process naming
proc_name = "myapi"
EOF

Test Gunicorn manually:

source venv/bin/activate
gunicorn main:app -c gunicorn_conf.py

# Ctrl+C to stop

Part 5 — Create a systemd Service {#part-5}

sudo mkdir -p /var/log/myapi
sudo chown ubuntu:ubuntu /var/log/myapi

sudo nano /etc/systemd/system/myapi.service
[Unit]
Description=Gunicorn + Uvicorn for FastAPI myapi
After=network.target

[Service]
User=ubuntu
Group=www-data
WorkingDirectory=/home/ubuntu/apps/myapi
Environment="PATH=/home/ubuntu/apps/myapi/venv/bin"

# Load environment variables from file
# EnvironmentFile=/home/ubuntu/apps/myapi/.env

ExecStart=/home/ubuntu/apps/myapi/venv/bin/gunicorn \
          main:app \
          -c /home/ubuntu/apps/myapi/gunicorn_conf.py \
          -k uvicorn.workers.UvicornWorker

Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl start myapi
sudo systemctl enable myapi
sudo systemctl status myapi

# Verify Unix socket was created
ls /run/myapi.sock

Part 6 — Configure Nginx {#part-6}

sudo nano /etc/nginx/sites-available/myapi
server {
    listen 80;
    server_name api.yourdomain.com;

    access_log /var/log/nginx/myapi_access.log;
    error_log  /var/log/nginx/myapi_error.log;

    location / {
        proxy_pass http://unix:/run/myapi.sock;
        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;

        # For streaming responses (FastAPI StreamingResponse)
        proxy_buffering off;

        proxy_connect_timeout 60s;
        proxy_send_timeout    120s;
        proxy_read_timeout    120s;
    }
}
sudo ln -s /etc/nginx/sites-available/myapi /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Part 7 — Enable HTTPS {#part-7}

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d api.yourdomain.com

Part 8 — Interactive API Docs in Production {#part-8}

FastAPI automatically generates OpenAPI documentation at /docs (Swagger UI) and /redoc. In production you may want to restrict access or disable it:

Restrict docs to specific IPs (via Nginx)

location /docs {
    # Allow only your IP
    allow YOUR_IP;
    deny all;

    proxy_pass http://unix:/run/myapi.sock;
}

location /redoc {
    allow YOUR_IP;
    deny all;
    proxy_pass http://unix:/run/myapi.sock;
}

Disable docs entirely in the app

app = FastAPI(
    docs_url=None,     # Disables /docs
    redoc_url=None,    # Disables /redoc
)

Keep docs but add HTTP Basic Auth

location /docs {
    auth_basic "API Documentation";
    auth_basic_user_file /etc/nginx/.htpasswd;
    proxy_pass http://unix:/run/myapi.sock;
}

The Gotcha: Async vs Sync Workers {#gotcha}

FastAPI supports both async (async def) and sync (def) endpoint functions. In production with Gunicorn + UvicornWorker, there's a performance consideration:

  • async def endpoints run in the event loop — great for I/O-bound work (database queries, HTTP calls)
  • def endpoints run in a thread pool — fine for CPU-bound or blocking operations

The mistake: using def for endpoints that do I/O (like database queries), which blocks the event loop. If you're using an async ORM (like Tortoise ORM or SQLAlchemy async), make sure all database calls use await.

Also, if you see workers crashing with timeout errors, check the timeout value in gunicorn_conf.py. Long-running operations may need a higher timeout, or should be moved to a background task:

from fastapi import BackgroundTasks

@app.post("/send-email/")
async def send_email(background_tasks: BackgroundTasks):
    background_tasks.add_task(send_email_function, email, message)
    return {"message": "Email will be sent in the background"}

Common Commands {#commands}

# Restart after code changes
sudo systemctl restart myapi

# View logs
sudo journalctl -u myapi -f
tail -f /var/log/myapi/error.log

# Check running workers
ps aux | grep gunicorn

# Test API health
curl http://localhost/health
curl https://api.yourdomain.com/health

# Interactive docs
open https://api.yourdomain.com/docs

Troubleshooting {#troubleshooting}

Issue Likely Cause Fix
Connection refused Service not running or wrong port Check systemctl status SERVICE and verify firewall rules
Permission denied Wrong file ownership or permissions Check file ownership with ls -la and use chown/chmod to fix
502 Bad Gateway Backend service not running Restart the backend service; check logs with journalctl -u SERVICE
SSL certificate error Certificate expired or domain mismatch Run sudo certbot renew and verify domain DNS points to server IP
Service not starting Config error or missing dependency Check logs with journalctl -u SERVICE -n 50 for specific error
Out of disk space Logs or data accumulation Run df -h to identify usage; clean logs or attach CBS storage
High memory usage Too many processes or memory leak Check with htop; consider upgrading instance plan if consistently high
Firewall blocking traffic Port not open in UFW or Lighthouse console Open port in Lighthouse console firewall AND sudo ufw allow PORT

Deploy your FastAPI app today:
👉 Tencent Cloud Lighthouse — Python-ready Ubuntu VPS
👉 View current pricing and promotions
👉 Explore all active deals and offers