Python Development

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

By Domain India Team · DomainIndia EngineeringPublished 10 min read
Knowledge base article
Contents (15 sections)

FastAPI is one of the most popular Python frameworks for building APIs: async-first, typed with Pydantic, and it generates OpenAPI documentation for you. This guide deploys a production FastAPI app on a Domain India VPS with Uvicorn workers under Gunicorn, systemd, an nginx reverse proxy with free SSL, PostgreSQL through SQLAlchemy, Alembic migrations and graceful reloads.

Key takeaways

Run FastAPI with Uvicorn workers managed by Gunicorn, start it with systemd as its own user, and put nginx with a Let's Encrypt certificate in front. Use an async PostgreSQL driver (asyncpg or psycopg 3), manage schema changes with Alembic, and reload with SIGHUP for deploys without dropped requests. FastAPI is an ASGI app, so on Domain India run it on a VPS or on the App Platform with a Dockerfile, not on shared hosting.

1. Why FastAPI

  • Async-first, so one worker can serve many slow I/O requests at once
  • Pydantic models for type-checked request and response bodies
  • Automatic OpenAPI/Swagger documentation at /docs
  • Good performance for a Python framework
  • Works with SQLAlchemy 2, Alembic, Celery and other common libraries

Good fit: REST APIs, WebSocket services, ML inference endpoints. For server-rendered HTML sites with an admin panel, Django is often the faster route.

2. The stack we're deploying

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

3. Prepare the VPS

bash
# AlmaLinux 9 / Rocky Linux 9 (certbot comes from EPEL)
sudo dnf install -y epel-release
sudo dnf install -y python3.12 python3.12-pip nginx postgresql-server postgresql-contrib git certbot python3-certbot-nginx

# Ubuntu 24.04
sudo apt install -y python3.12-venv python3-pip nginx postgresql git certbot python3-certbot-nginx

# Create the app user
sudo useradd -r -m -d /home/fastapi -s /bin/bash fastapi
sudo su - fastapi

Install Redis too (redis on AlmaLinux, redis-server on Ubuntu) only if your app uses it.

4. A sample FastAPI app

~fastapi/app/main.py:

python
from contextlib import asynccontextmanager
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import BaseModel, ConfigDict, EmailStr

from .db import get_db, engine
from .models import User

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Startup
    yield
    # Shutdown
    await engine.dispose()

app = FastAPI(title="MyAPI", version="1.0.0", lifespan=lifespan)

class UserCreate(BaseModel):
    email: EmailStr
    name: str

class UserOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: str
    email: str
    name: str

@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
import os
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker

DATABASE_URL = os.environ["DATABASE_URL"]  # postgresql+asyncpg://...

engine = create_async_engine(DATABASE_URL, pool_size=5, max_overflow=5, pool_pre_ping=True)
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
uvicorn[standard]
uvicorn-worker
gunicorn
sqlalchemy[asyncio]
asyncpg
alembic
pydantic[email]

Once everything works, pin the exact versions you tested with (pip freeze > requirements.lock) and deploy from that file, so production installs match your tests.

5. Create the virtual environment

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

6. PostgreSQL

bash
# As root (AlmaLinux; Ubuntu initialises the cluster for you)
sudo postgresql-setup --initdb
sudo systemctl enable --now postgresql

sudo -u postgres psql <<EOF
CREATE USER fastapi WITH PASSWORD 'use-a-long-random-password';
CREATE DATABASE fastapi_prod OWNER fastapi;
EOF

Making fastapi the database owner avoids "permission denied for schema public" on PostgreSQL 15 and later.

AlmaLinux: allow password logins over TCP

The default pg_hba.conf on AlmaLinux and Rocky Linux uses ident for connections to 127.0.0.1, so a password login from your app fails. Change those host lines to scram-sha-256 in /var/lib/pgsql/data/pg_hba.conf and reload PostgreSQL.

7. Alembic migrations

bash
cd ~fastapi
alembic init -t async alembic

Edit alembic/env.py to import your models and read DATABASE_URL from the environment:

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 and apply the first migration:

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

Always read autogenerated migrations before applying them; Alembic cannot detect every change (for example, column renames).

8. Gunicorn with Uvicorn workers

~fastapi/gunicorn.conf.py:

python
import multiprocessing

bind = "127.0.0.1:8000"
workers = max(2, multiprocessing.cpu_count())
worker_class = "uvicorn_worker.UvicornWorker"

timeout = 60
keepalive = 5
graceful_timeout = 30

accesslog = "-"
errorlog = "-"
loglevel = "info"

# Leave preload_app off so a SIGHUP reload picks up new code
preload_app = False

Async workers each handle many connections, so you need fewer of them than with sync frameworks: start with one per CPU core and adjust after watching RAM and CPU. The UvicornWorker class now lives in the separate uvicorn-worker package; the old uvicorn.workers path is deprecated.

Test it:

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

From the VPS, curl http://127.0.0.1:8000/health should return {"status":"ok"}. Consider disabling /docs in production (docs_url=None) or protecting it.

9. systemd service

/etc/systemd/system/fastapi.service:

ini
[Unit]
Description=FastAPI Application
After=network-online.target postgresql.service
Wants=network-online.target

[Service]
Type=notify
NotifyAccess=main
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 -s HUP $MAINPID
KillMode=mixed
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 (run chmod 600 on it):

code
DATABASE_URL=postgresql+asyncpg://fastapi:use-a-long-random-password@localhost/fastapi_prod
JWT_SECRET=use-a-long-random-secret
ENV=production

Start it:

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

10. nginx reverse proxy and 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

On AlmaLinux, allow nginx to connect to your app once with sudo setsebool -P httpd_can_network_connect 1; otherwise SELinux blocks it and nginx returns a 502. If you serve WebSockets, add Upgrade and Connection "upgrade" headers for that location.

11. Deploys without dropped requests

Gunicorn reloads on SIGHUP: it starts new workers with the new code, then lets the old ones finish their in-flight requests.

bash
# Deploy script
cd /home/fastapi
git pull
.venv/bin/pip install -r requirements.lock
.venv/bin/alembic upgrade head
sudo systemctl reload fastapi   # sends SIGHUP via ExecReload

Make migrations backwards-compatible (add columns before code uses them, remove them later), because old and new workers run side by side for a moment.

12. Observability

Add Prometheus metrics:

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

app = FastAPI(...)
Instrumentator().instrument(app).expose(app)
# /metrics is now live; block it from the public internet in nginx

Scrape it with Prometheus; see our observability guide.

13. Background tasks: which tool?

ToolBest forComplexity
FastAPI BackgroundTasksShort tasks after a responseNone, built in
APSchedulerCron-like schedules inside the appLow
Celery + RedisHeavy work, retries, many workersMedium
ARQLightweight async task queue on RedisLow

Start with BackgroundTasks for simple needs. Move to Celery or ARQ when you need retries, schedules or separate worker processes. See our job queue comparison.

14. Common pitfalls

Sync database drivers in async code
psycopg2 or other blocking calls freeze the event loop. Use asyncpg or psycopg 3 in async mode, or define the endpoint with plain def so FastAPI runs it in a thread.
Forgetting await
You get a coroutine object instead of a result, and usually a confusing error later. Await every async call.
Too many database connections
Each worker has its own pool: pool_size plus max_overflow, times the number of workers. PostgreSQL allows 100 connections by default.
CORS misconfigured
Browsers block the requests. Add CORSMiddleware with an explicit list of allowed origins.
--reload in production
That is the development mode. Use Gunicorn with Uvicorn workers.
Gunicorn timeout killing long requests
Raise timeout, or better, move long work to a background job.

15. Running FastAPI on Domain India

  • VPS: everything in this guide. Domain India VPS plans are self-managed with full root access on KVM, so you install and patch Python, nginx and PostgreSQL yourself. Plans start from ₹553 a month, excluding 18% GST (Domain India list price on 30 September 2026). No backups or snapshots are included; schedule pg_dump and copy the dumps off the server.
  • App Platform: a good fit if you don't want to manage a server. Package the app with your own Dockerfile (only Node.js is detected automatically), deploy from GitHub with Deploy Now or with a deploy token, and use the PostgreSQL database and free SSL included in every plan. It has no SSH, no Redis and no WebSocket support, and it doesn't deploy automatically on every push. Plans are ₹100, ₹250 and ₹500 a month, excluding GST. See getting started with the App Platform.
  • Shared hosting: the Python app tool on cPanel and DirectAdmin runs WSGI apps through Passenger. FastAPI is ASGI and Uvicorn is a long-running server, so it does not fit; see deploying a Python app on shared hosting for what does.
Flask, FastAPI or Django?

FastAPI for typed, async APIs with automatic documentation. Django for full-stack sites with templates, an admin panel and a built-in ORM. Flask for small apps or when you are maintaining an existing Flask codebase.

Uvicorn alone, or Gunicorn with Uvicorn workers?

Both work in production. Uvicorn can run several workers itself (uvicorn --workers 4, or fastapi run --workers 4). Gunicorn adds a mature process manager with graceful SIGHUP reloads, which this guide uses for deploys.

Can I run FastAPI on Domain India shared hosting?

No. The shared hosting Python tool runs WSGI apps through Passenger, and FastAPI is an ASGI app served by Uvicorn. Use the App Platform with a Dockerfile, or a VPS.

asyncpg or psycopg 3?

asyncpg is fast and widely used with SQLAlchemy's async engine. psycopg 3 supports both sync and async code with one driver. Either is a good choice; pick one and use it throughout.

How many requests can a 2 GB VPS handle?

It depends on your endpoints. A simple cached endpoint can serve thousands of requests per second, while database-heavy endpoints are usually limited by PostgreSQL. Load-test your own API (for example with k6 or Locust) before launch.

Does the App Platform support FastAPI?

Yes, through your own Dockerfile. It includes PostgreSQL and free SSL, but no WebSockets and no Redis, so apps that need those belong on a VPS.

Ready to go live? Run FastAPI with full control on a VPS, or deploy it from a Dockerfile on the App Platform.

Deploy FastAPI on a VPS

Full root access for Gunicorn, nginx, PostgreSQL and any worker your API needs.

See VPS plans

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