← back to blog
·
devopsinfrastructurelinux

Setting Up a Production VPS — My Stack and Why

A walkthrough of how I set up my VPS for Certeli and CV Human: Caddy, PM2, Cloudflare, PostgreSQL, and zero-downtime deployments.

The Stack

After building on Vercel and Supabase for a while, I needed something more hands-on for the larger platforms. Here's what I settled on:

ComponentChoice
OSUbuntu 24.04
Web serverCaddy (auto-TLS)
Process managerPM2
Reverse proxyCloudflare (CDN + DDoS)
DatabasePostgreSQL 16 (self-hosted)
CI/CDGitHub Actions self-hosted runner

Caddy

Caddy handles TLS automatically via Let's Encrypt. The configuration is clean — a few dozen lines for each domain. No certbot dance, no nginx config gymnastics.

PM2

Simple, battle-tested process management. I run:

  • Next.js apps on :3001
  • FastAPI backend on :8000
  • A tcp-proxy for zero-downtime deploys

Database

Self-hosted PostgreSQL 16. Backups are automated via cron. The connection is local-only (no exposed port), so the database isn't accessible outside the VPS.

Deployment Flow

Push to main → GitHub Actions picks it up on the self-hosted runner → builds → swaps traffic via tcp-proxy → done. No downtime.

Things I'd Do Differently

I'd automate the initial server setup with Ansible or a script. Setting up PostgreSQL, Caddy, PM2, and the deploy runner manually works, but it's not repeatable.