FastAPI Production Deployment on DomainIndia VPS (Gunicorn + Uvicorn + nginx + PostgreSQL)
Why FastAPI
- Async-first (handles 10K+ concurrent connections)
- Pydantic for type-safe request/response models
- Auto-generated OpenAPI/Swagger docs at
/docs - 3-5× faster than Flask, same-league as Node.js Express
- Massive ecosystem (SQLAlchemy 2, Alembic, Celery, APScheduler)
Good fit: REST APIs, WebSocket services, ML inference endpoints. Less ideal for server-rendered HTML apps — use Django or Flask for those.
Stack we're deploying
Client → nginx (443) → Gunicorn → 4× Uvicorn workers → FastAPI app → PostgreSQL
↑
Redis (cache/queue)Step 1 — Prepare VPS
# AlmaLinux 9
sudo dnf install -y python3.12 python3.12-devel python3-pip nginx postgresql-server postgresql-contrib redis git certbot python3-certbot-nginx
# Ubuntu 22.04+
sudo apt install -y python3.12 python3.12-venv python3-pip nginx postgresql redis git certbot python3-certbot-nginx
# Create app user
sudo useradd -r -m -s /bin/bash fastapi
sudo su - fastapiStep 2 — Sample FastAPI app
~fastapi/app/main.py:
from contextlib import asynccontextmanager
from fastapi import FastAPI, Depends, HTTPException
from fastapi.responses import JSONResponse
from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import BaseModel, EmailStr
import os
from .db import get_db, engine
from .models import User
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup
print("Starting up")
yield
# Shutdown
await engine.dispose()
print("Shutting down")
app = FastAPI(
title="MyAPI",
version="1.0.0",
lifespan=lifespan,
)
class UserCreate(BaseModel):
email: EmailStr
name: str
class UserOut(BaseModel):
id: str
email: str
name: str
class Config: from_attributes = True
@app.get("/health")
async def health():
return {"status": "ok", "version": "1.0.0"}
@app.post("/users", response_model=UserOut)
async def create_user(payload: UserCreate, db: AsyncSession = Depends(get_db)):
user = User(email=payload.email, name=payload.name)
db.add(user)
await db.commit()
await db.refresh(user)
return user
@app.get("/users/{user_id}", response_model=UserOut)
async def get_user(user_id: str, db: AsyncSession = Depends(get_db)):
user = await db.get(User, user_id)
if not user:
raise HTTPException(404, "Not found")
return user~fastapi/app/db.py:
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
import os
DATABASE_URL = os.getenv("DATABASE_URL") # postgresql+asyncpg://...
engine = create_async_engine(DATABASE_URL, pool_size=10, max_overflow=5)
SessionLocal = async_sessionmaker(engine, expire_on_commit=False)
async def get_db():
async with SessionLocal() as session:
yield session~fastapi/app/models.py:
import uuid
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import String
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
email: Mapped[str] = mapped_column(String(255), unique=True)
name: Mapped[str] = mapped_column(String(100))~fastapi/requirements.txt:
fastapi==0.114.0
uvicorn[standard]==0.30.6
gunicorn==22.0.0
sqlalchemy==2.0.35
asyncpg==0.29.0
alembic==1.13.2
pydantic[email]==2.8.2
python-dotenv==1.0.1Step 3 — Install + setup venv
cd ~fastapi
python3.12 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtStep 4 — PostgreSQL
# As root
sudo postgresql-setup --initdb
sudo systemctl enable --now postgresql
sudo -u postgres psql <<EOF
CREATE DATABASE fastapi_prod;
CREATE USER fastapi WITH ENCRYPTED PASSWORD 'changeme';
GRANT ALL PRIVILEGES ON DATABASE fastapi_prod TO fastapi;
\c fastapi_prod
GRANT ALL ON SCHEMA public TO fastapi;
EOFStep 5 — Alembic migrations
cd ~fastapi
alembic init -t async alembicEdit alembic/env.py to import your models + use DATABASE_URL from env.
from app.models import Base
from app.db import DATABASE_URL
config.set_main_option("sqlalchemy.url", DATABASE_URL)
target_metadata = Base.metadataCreate migration:
alembic revision --autogenerate -m "create users"
alembic upgrade headStep 6 — Gunicorn + Uvicorn workers
~fastapi/gunicorn.conf.py:
import multiprocessing
bind = "127.0.0.1:8000"
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "uvicorn.workers.UvicornWorker"
worker_connections = 1000
timeout = 60
keepalive = 5
graceful_timeout = 30
accesslog = "-"
errorlog = "-"
loglevel = "info"
preload_app = True # fork after load — faster reloadsTest:
cd ~fastapi
export DATABASE_URL=postgresql+asyncpg://fastapi:changeme@localhost/fastapi_prod
.venv/bin/gunicorn -c gunicorn.conf.py app.main:appVisit http://localhost:8000/docs — Swagger UI shows your API.
Step 7 — systemd service
/etc/systemd/system/fastapi.service:
[Unit]
Description=FastAPI Application
After=network.target postgresql.service redis.service
[Service]
Type=notify
User=fastapi
Group=fastapi
WorkingDirectory=/home/fastapi
ExecStart=/home/fastapi/.venv/bin/gunicorn -c /home/fastapi/gunicorn.conf.py app.main:app
ExecReload=/bin/kill -HUP $MAINPID
KillMode=mixed
KillSignal=SIGTERM
TimeoutStopSec=30
Restart=on-failure
RestartSec=5
EnvironmentFile=/home/fastapi/.env
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/home/fastapi
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target~fastapi/.env:
DATABASE_URL=postgresql+asyncpg://fastapi:changeme@localhost/fastapi_prod
REDIS_URL=redis://localhost:6379/0
JWT_SECRET=random-long-string
ENV=productionStart:
sudo systemctl daemon-reload
sudo systemctl enable --now fastapi
sudo journalctl -u fastapi -fStep 8 — nginx reverse proxy + SSL
/etc/nginx/conf.d/fastapi.conf:
upstream fastapi {
server 127.0.0.1:8000 fail_timeout=0;
keepalive 32;
}
server {
listen 80;
server_name api.yourcompany.com;
client_max_body_size 20M;
location / {
proxy_pass http://fastapi;
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 "";
proxy_read_timeout 60s;
proxy_buffering off; # better for streaming responses
}
}sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d api.yourcompany.comStep 9 — Zero-downtime deploys
Gunicorn supports SIGHUP to gracefully reload workers without dropping connections:
# Deploy hook
cd /home/fastapi
git pull
.venv/bin/pip install -r requirements.txt
.venv/bin/alembic upgrade head
sudo systemctl reload fastapi # sends SIGHUP via ExecReloadWorkers restart one at a time; in-flight requests finish cleanly.
Step 10 — Observability
Add Prometheus metrics:
pip install prometheus-fastapi-instrumentatorfrom prometheus_fastapi_instrumentator import Instrumentator
app = FastAPI(...)
Instrumentator().instrument(app).expose(app)
# Now /metrics endpoint is liveScrape with Prometheus (see our Observability guide).
Background tasks — which tool?
| Tool | Best for | Complexity |
|---|---|---|
FastAPI BackgroundTasks | Short tasks triggered by request | Zero — built-in |
| APScheduler | Cron-like schedules in-process | Low |
| Celery + Redis | Heavy async work, multiple workers | Medium |
| Arq | Lightweight async task queue | Low |
Start with BackgroundTasks for simple needs; scale to Celery or Arq when you need retries + multiple worker machines.
Common pitfalls
FAQ
FastAPI for modern APIs (async, typed, OpenAPI). Django for full-stack with templates, admin, ORM. Flask if you're maintaining existing Flask apps.
Uvicorn alone works for dev + light prod. Gunicorn adds process management, graceful reload, better logging — use it for production.
Shared cPanel's Setup Python App runs simple FastAPI apps (via Passenger). For async + multiple workers, use VPS.
Asyncpg is faster but less feature-complete. Psycopg3 (async mode) is more versatile, supports synchronous adapters. For greenfield FastAPI: asyncpg.
With 4 Uvicorn workers × 1000 connections each = 4K theoretical. Real-world with DB-backed API: 500-2000/sec. Bottleneck is usually Postgres.
FastAPI in production wants a solid VPS with PostgreSQL. Order VPS