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.
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
Only expose SSH, HTTP, and HTTPS:
ufw allow OpenSSHufw allow 80/tcpufw allow 443/tcpufw enableufw status
Do not expose:
5432 PostgreSQL8000 FastAPI
FastAPI will only listen on:
127.0.0.1:8000
Install Docker Engine and Compose plugin using the official Docker repository:
apt install -y ca-certificates curlinstall -m 0755 -d /etc/apt/keyringscurl -fsSL https://download.docker.com/linux/ubuntu/gpg \-o /etc/apt/keyrings/docker.ascchmod a+r /etc/apt/keyrings/docker.ascecho "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.listapt updateapt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
Verify:
docker --versiondocker compose version
Create a deployment directory:
mkdir -p /optcd /opt
Clone the backend:
git clone git@github.com:YOUR_USERNAME/green-garden-api.gitcd green-garden-api
If using a private GitHub repository, configure an SSH key on the VPS and add the public key to GitHub.
Create the production .env:
nano .env
Example:
APP_ENV=productionDEBUG=falsePOSTGRES_DB=green_gardenPOSTGRES_USER=green_gardenPOSTGRES_PASSWORD=CHANGE_MEPOSTGRES_PORT=5432DATABASE_URL=postgresql+psycopg://green_garden:CHANGE_ME@db:5432/green_gardenJWT_SECRET_KEY=CHANGE_MEJWT_ALGORITHM=HS256AUTH_COOKIE_SECURE=trueAUTH_COOKIE_SAMESITE=noneAUTH_COOKIE_DOMAIN=.ngocnganbentre.vnCORS_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.vnapi.ngocnganbentre.vn
Use a production-oriented docker-compose.yml:
services:db:image: postgres:16-alpineenvironment:POSTGRES_DB: ${POSTGRES_DB}POSTGRES_USER: ${POSTGRES_USER}POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}volumes:- postgres_data:/var/lib/postgresql/datahealthcheck:test:["CMD-SHELL","pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]interval: 5stimeout: 5sretries: 10start_period: 10srestart: unless-stoppedapi:build: .ports:- "127.0.0.1:8000:8000"env_file:- .envenvironment: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_healthycommand: uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 2restart: unless-stoppedvolumes: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
Create an A record at your DNS provider:
Type: AName: apiValue: YOUR_VPS_IPTTL: 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
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
Build the API:
docker compose build api
Start the services:
docker compose up -d
Check:
docker compose ps
Expected:
api Updb 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.
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.
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
Never run:
docker compose down -v
on a production database unless you intentionally want to delete the PostgreSQL volume and its data.
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-apigit statusgit pull origin masterdocker compose build apidocker compose up -ddocker compose exec api alembic upgrade headdocker compose psdocker compose logs --tail=100 api
If docker-compose.yml itself changes, docker compose up -d will recreate the affected services as needed.
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.