Home
Python
Deploying a FastAPI + PostgreSQL Application to a VPS
Daniel Nguyen
Daniel Nguyen
October 06, 2026
2 min

Table Of Contents

01
1. Prepare the VPS
02
2. Configure Firewall
03
3. Install Docker
04
4. Clone the Repository
05
5. Production Environment Variables
06
6. Production Docker Compose
07
7. Configure the API Subdomain
08
8. Configure Nginx
09
9. Start the Application
10
10. Enable HTTPS with Certbot
11
11. Database Migrations
12
12. Updating the API Later
13
13. Connect the Vercel Frontend

This guide documents the production deployment flow for a FastAPI + PostgreSQL application using Docker Compose, Nginx, and Let’s Encrypt.

The architecture is:

Frontend (Vercel)
│
│ HTTPS
▼
api.ngocnganbentre.vn
│
▼
Nginx :443
│
▼
127.0.0.1:8000
│
▼
FastAPI (Docker)
│
▼
PostgreSQL (Docker)

The PostgreSQL database and FastAPI port are not exposed directly to the Internet.

1. Prepare the VPS

SSH into the server:

ssh root@YOUR_SERVER_IP

Update Ubuntu:

apt update && apt upgrade -y

Install basic packages:

apt install -y ca-certificates curl git nginx ufw

2. Configure Firewall

Only expose SSH, HTTP, and HTTPS:

ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
ufw status

Do not expose:

5432 PostgreSQL
8000 FastAPI

FastAPI will only listen on:

127.0.0.1:8000

3. Install Docker

Install Docker Engine and Compose plugin using the official Docker repository:

apt install -y ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
-o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo $VERSION_CODENAME) stable" \
> /etc/apt/sources.list.d/docker.list
apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

Verify:

docker --version
docker compose version

4. Clone the Repository

Create a deployment directory:

mkdir -p /opt
cd /opt

Clone the backend:

git clone git@github.com:YOUR_USERNAME/green-garden-api.git
cd green-garden-api

If using a private GitHub repository, configure an SSH key on the VPS and add the public key to GitHub.

5. Production Environment Variables

Create the production .env:

nano .env

Example:

APP_ENV=production
DEBUG=false
POSTGRES_DB=green_garden
POSTGRES_USER=green_garden
POSTGRES_PASSWORD=CHANGE_ME
POSTGRES_PORT=5432
DATABASE_URL=postgresql+psycopg://green_garden:CHANGE_ME@db:5432/green_garden
JWT_SECRET_KEY=CHANGE_ME
JWT_ALGORITHM=HS256
AUTH_COOKIE_SECURE=true
AUTH_COOKIE_SAMESITE=none
AUTH_COOKIE_DOMAIN=.ngocnganbentre.vn
CORS_ORIGINS=https://www.ngocnganbentre.vn,https://ngocnganbentre.vn

Generate secrets instead of using simple passwords:

openssl rand -hex 32

Never commit .env to Git.

.env.example can contain:

AUTH_COOKIE_DOMAIN=

For local development the value can remain empty. Production uses:

AUTH_COOKIE_DOMAIN=.ngocnganbentre.vn

This allows authentication cookies to work across:

www.ngocnganbentre.vn
api.ngocnganbentre.vn

6. Production Docker Compose

Use a production-oriented docker-compose.yml:

services:
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test:
[
"CMD-SHELL",
"pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"
]
interval: 5s
timeout: 5s
retries: 10
start_period: 10s
restart: unless-stopped
api:
build: .
ports:
- "127.0.0.1:8000:8000"
env_file:
- .env
environment:
DATABASE_URL: ${DATABASE_URL}
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_PORT: ${POSTGRES_PORT}
depends_on:
db:
condition: service_healthy
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 2
restart: unless-stopped
volumes:
postgres_data:

Notice that PostgreSQL has no ports section.

FastAPI is only accessible from the VPS:

127.0.0.1:8000

Validate the Compose file without printing environment secrets:

docker compose config --quiet

7. Configure the API Subdomain

Create an A record at your DNS provider:

Type: A
Name: api
Value: YOUR_VPS_IP
TTL: 3600

For example:

api.ngocnganbentre.vn → 180.93.115.67

Verify:

dig api.ngocnganbentre.vn

The result should contain:

api.ngocnganbentre.vn. 3600 IN A YOUR_VPS_IP

8. Configure Nginx

Create an Nginx site:

nano /etc/nginx/sites-available/green-garden-api

Example:

server {
listen 80;
listen [::]:80;
server_name api.ngocnganbentre.vn;
location / {
proxy_pass http://127.0.0.1:8000;
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;
}
}

Enable it:

ln -s /etc/nginx/sites-available/green-garden-api \
/etc/nginx/sites-enabled/green-garden-api

Test:

nginx -t

Reload:

systemctl reload nginx

Check:

systemctl status nginx --no-pager

9. Start the Application

Build the API:

docker compose build api

Start the services:

docker compose up -d

Check:

docker compose ps

Expected:

api Up
db Up (healthy)

Check API logs:

docker compose logs --tail=100 api

Test locally on the VPS:

curl http://127.0.0.1:8000/docs

Test through Nginx:

curl -I http://api.ngocnganbentre.vn

A 404 from / can be normal if FastAPI does not define a / route. The important thing is that Nginx can reach FastAPI.

10. Enable HTTPS with Certbot

Install Certbot:

apt install -y certbot python3-certbot-nginx

Run:

sudo certbot --nginx -d api.ngocnganbentre.vn

Certbot will configure the Let’s Encrypt certificate and HTTPS.

Choose HTTP → HTTPS redirect when prompted.

Verify:

curl -I https://api.ngocnganbentre.vn

Then open:

https://api.ngocnganbentre.vn/docs

FastAPI Swagger should be available.

11. Database Migrations

If the project uses Alembic, after deploying a new migration:

git pull origin master

Build the updated API:

docker compose build api

Start/recreate the container:

docker compose up -d

Then run:

docker compose exec api alembic upgrade head

Check:

docker compose logs --tail=50 api

Important

Never run:

docker compose down -v

on a production database unless you intentionally want to delete the PostgreSQL volume and its data.

12. Updating the API Later

The normal deployment workflow becomes:

Local development
↓
Run tests
↓
git commit
↓
git push
↓
GitHub
↓
VPS: git pull
↓
docker compose build api
↓
docker compose up -d
↓
alembic upgrade head
↓
Check logs

On the VPS:

cd /opt/green-garden-api
git status
git pull origin master
docker compose build api
docker compose up -d
docker compose exec api alembic upgrade head
docker compose ps
docker compose logs --tail=100 api

If docker-compose.yml itself changes, docker compose up -d will recreate the affected services as needed.

13. Connect the Vercel Frontend

In Vercel → Project → Settings → Environment Variables, configure:

NEXT_PUBLIC_API_URL=https://api.ngocnganbentre.vn

Use Config, not Secret, because NEXT_PUBLIC_* values are exposed to the browser.

Redeploy the frontend after changing the variable.

The final architecture is:

Internet
│
┌────────┴────────┐
│ │
▼ ▼
www.ngocnganbentre.vn api.ngocnganbentre.vn
│ │
Vercel VPS
│
Nginx
│
127.0.0.1:8000
│
FastAPI
│
Docker
│
PostgreSQL

This gives you a repeatable deployment process: GitHub is the source of code/config, .env stays only on the server, Nginx handles HTTPS, FastAPI stays behind Nginx, and PostgreSQL stays private.


Tags

#Python#FastAPI

Share

Daniel Nguyen

Daniel Nguyen

Frontend Developer

Frontend developer specializing in React, Next.js, and JavaScript. Writing practical guides on modern web development at Dev98.

Expertise

React
Next.js
JavaScript
TypeScript
Python

Social Media

githublinkedinyoutubewebsite

Related Posts

AI
Introduction to LangChain
October 05, 2026
1 min
Dev98

Dev98

React · Next.js · Web development