Python Development

FastAPI Production Deployment on DomainIndia VPS (Gunicorn + Uvicorn + nginx + PostgreSQL)

By Domain India Team · DomainIndia EngineeringPublished · Updated 4 min read
Knowledge base article
Contents (15 sections)
TL;DR

FastAPI is the dominant Python API framework of 2026 — async-first, type-safe, auto-OpenAPI docs. This guide deploys a production FastAPI app on DomainIndia VPS: Uvicorn workers under Gunicorn, systemd, nginx reverse proxy, PostgreSQL with SQLAlchemy, Alembic migrations, and zero-downtime reloads.

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

code
Client → nginx (443) → Gunicorn → 4× Uvicorn workers → FastAPI app → PostgreSQL
                                                                ↑
                                                          Redis (cache/queue)

Step 1 — Prepare VPS

bash
# 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 - fastapi

Step 2 — Sample FastAPI app

~fastapi/app/main.py:

python
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:

python
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:

python
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:

code
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.1

Step 3 — Install + setup venv

bash
cd ~fastapi
python3.12 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

Step 4 — PostgreSQL

bash
# 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;
EOF

Step 5 — Alembic migrations

bash
cd ~fastapi
alembic init -t async alembic

Edit alembic/env.py to import your models + use DATABASE_URL from env.

python
from app.models import Base
from app.db import DATABASE_URL
config.set_main_option("sqlalchemy.url", DATABASE_URL)
target_metadata = Base.metadata

Create migration:

bash
alembic revision --autogenerate -m "create users"
alembic upgrade head

Step 6 — Gunicorn + Uvicorn workers

~fastapi/gunicorn.conf.py:

python
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 reloads

Test:

bash
cd ~fastapi
export DATABASE_URL=postgresql+asyncpg://fastapi:changeme@localhost/fastapi_prod
.venv/bin/gunicorn -c gunicorn.conf.py app.main:app

Visit http://localhost:8000/docs — Swagger UI shows your API.

Step 7 — systemd service

/etc/systemd/system/fastapi.service:

ini
[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:

code
DATABASE_URL=postgresql+asyncpg://fastapi:changeme@localhost/fastapi_prod
REDIS_URL=redis://localhost:6379/0
JWT_SECRET=random-long-string
ENV=production

Start:

bash
sudo systemctl daemon-reload
sudo systemctl enable --now fastapi
sudo journalctl -u fastapi -f

Step 8 — nginx reverse proxy + SSL

/etc/nginx/conf.d/fastapi.conf:

nginx
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
    }
}
bash
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d api.yourcompany.com

Step 9 — Zero-downtime deploys

Gunicorn supports SIGHUP to gracefully reload workers without dropping connections:

bash
# 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 ExecReload

Workers restart one at a time; in-flight requests finish cleanly.

Step 10 — Observability

Add Prometheus metrics:

bash
pip install prometheus-fastapi-instrumentator
python
from prometheus_fastapi_instrumentator import Instrumentator

app = FastAPI(...)
Instrumentator().instrument(app).expose(app)
# Now /metrics endpoint is live

Scrape with Prometheus (see our Observability guide).

Background tasks — which tool?

ToolBest forComplexity
FastAPI BackgroundTasksShort tasks triggered by requestZero — built-in
APSchedulerCron-like schedules in-processLow
Celery + RedisHeavy async work, multiple workersMedium
ArqLightweight async task queueLow

Start with BackgroundTasks for simple needs; scale to Celery or Arq when you need retries + multiple worker machines.

Common pitfalls

Mixing sync DB (psycopg2) with async FastAPI
blocks the event loop. Use asyncpg or psycopg3 async throughout.
Forgetting await
Pydantic silently accepts coroutines, returns them in JSON. Always await async calls.
Too many DB connections
default pool_size=5, with 4 workers = 20 connections. Postgres default max is 100. Scale wisely.
CORS misconfigured
browser blocks requests. Use fastapi.middleware.cors.CORSMiddleware with allowed origins.
--reload in production
devserver mode. Use gunicorn with uvicorn workers for prod.
Gunicorn timeout killing long requests
increase timeout in config or push long work to background task.

FAQ

Flask, FastAPI, or Django?

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 or Gunicorn + Uvicorn?

Uvicorn alone works for dev + light prod. Gunicorn adds process management, graceful reload, better logging — use it for production.

Can I run this on DomainIndia shared hosting?

Shared cPanel's Setup Python App runs simple FastAPI apps (via Passenger). For async + multiple workers, use VPS.

Asyncpg or psycopg3?

Asyncpg is faster but less feature-complete. Psycopg3 (async mode) is more versatile, supports synchronous adapters. For greenfield FastAPI: asyncpg.

How many concurrent requests on a 2 GB VPS?

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

Was this article helpful?

Your answer helps us decide what to improve next.

Still need help? Open a support ticket and our team will reply.

Prefer an app? Add this site to your home screen.Get the app
FastAPI Production Deployment on DomainIndia VPS