Self-Host Your Entire SaaS Stack on a $5 VPS
Most developers pay $95-120/month for managed services before landing their first customer. Vercel Pro ($20/mo per seat), PlanetScale ($30/mo for HA), Clerk ($25/mo), and Mailchimp ($20-45/mo) add up fast.
There's a better way. A single Raff Technologies VPS running Docker can replace all of it for $6-11/month — using open-source tools that give you the same capabilities.
This guide covers everything: architecture, full configs, email setup, backups, and auto-deploy.
What You'll Build
| Component | Tool | Replaces | Savings |
|---|---|---|---|
| Hosting | Raff Technologies VPS | Vercel Pro ($20/mo) | ~$10-15/mo |
| Database | PostgreSQL (open-source) | PlanetScale ($30/mo) | $30/mo |
| Listmonk + Amazon SES | Mailchimp ($20-45/mo) | ~$19-44/mo | |
| HTTPS | Caddy + Let's Encrypt (open-source) | nginx + certbot | Free |
| Auth | Self-hosted | Clerk ($25/mo) | $25/mo |
| Deploys | Webhook auto-deploy (open-source) | Vercel CI/CD | Free |
Total: ~$6-11/month instead of $95-120/month.
Step 1: Get a VPS
Start with a Raff Technologies VPS — Ubuntu 22.04 or 24.04 LTS, 2 vCPU, 2GB+ RAM, NVMe storage.
Why Raff:
- US-based infrastructure (Vint Hill, VA) — low latency for US and LATAM users
- AMD EPYC processors with NVMe storage standard
- 40-60% cheaper than DigitalOcean or Vultr for equivalent specs
- Production-ready specs start at $5-10/month
👉 Browse Raff Technologies VPS plans
Step 2: Server Setup (5 Minutes)
SSH into your Ubuntu server:
# Update system
apt update && apt upgrade -y
# Create deploy user
adduser deploy
usermod -aG sudo deploy
# Firewall — only allow SSH, HTTP, HTTPS
ufw allow 22
ufw allow 80
ufw allow 443
ufw enable
# Install Docker
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker deploy
# Install Git
apt install git -y
Point your domain's A record to the Raff VPS IP address. Done.
Step 3: Docker Compose — The Entire Stack
One file. Six services. Everything you need.
The architecture:
┌──────────────────────────────────────────────┐
│ Raff Technologies VPS ($5-10/mo) │
│ │
│ ┌─────────┐ ┌──────────┐ ┌─────────────┐ │
│ │ Caddy │→ │ Next.js │ │ Backup │ │
│ │ :80/443 │ │ :3000 │ │ (6-hour) │ │
│ └─────────┘ └──────────┘ └─────────────┘ │
│ ↓ ↓ ↓ │
│ ┌────────────────────────────────────────┐ │
│ │ PostgreSQL (app DB + Listmonk DB) │ │
│ └────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────┐ │
│ │ Listmonk → Amazon SES │ │
│ └────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────┐ │
│ │ Webhook listener (auto-deploy) │ │
│ └────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
Create docker-compose.yml:
version: '3.8'
services:
# Your Next.js app
app:
build: .
restart: unless-stopped
environment:
- DATABASE_URL=postgresql://app:${DB_PASSWORD}@db:5432/app
- NODE_ENV=production
depends_on:
db:
condition: service_healthy
networks:
- web
- internal
# PostgreSQL
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
- POSTGRES_USER=app
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=app
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- internal
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
timeout: 5s
retries: 5
# Listmonk (email campaigns + transactional)
listmonk:
image: listmonk/listmonk:latest
restart: unless-stopped
environment:
- TZ=UTC
volumes:
- ./listmonk/config.toml:/listmonk/config.toml
depends_on:
listmonk_db:
condition: service_healthy
networks:
- web
- internal
# Listmonk needs its own PostgreSQL
listmonk_db:
image: postgres:16-alpine
restart: unless-stopped
environment:
- POSTGRES_USER=listmonk
- POSTGRES_PASSWORD=${LISTMONK_DB_PASSWORD}
- POSTGRES_DB=listmonk
volumes:
- listmonk_data:/var/lib/postgresql/data
networks:
- internal
healthcheck:
test: ["CMD-SHELL", "pg_isready -U listmonk"]
interval: 5s
timeout: 5s
retries: 5
# Caddy (reverse proxy + auto HTTPS via Let's Encrypt)
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
networks:
- web
# Automated backups (every 6 hours)
backup:
image: postgres:16-alpine
restart: unless-stopped
environment:
- PGPASSWORD=${DB_PASSWORD}
- LISTMONK_PGPASSWORD=${LISTMONK_DB_PASSWORD}
volumes:
- ./backup.sh:/backup.sh:ro
- backup_data:/backups
entrypoint: ["/bin/sh", "-c", "while true; do sh /backup.sh; sleep 21600; done"]
networks:
- internal
volumes:
postgres_data:
listmonk_data:
caddy_data:
caddy_config:
backup_data:
networks:
web:
internal:
Create .env:
DB_PASSWORD=your-strong-password-here
LISTMONK_DB_PASSWORD=another-strong-password-here
Add .env to .gitignore. Never commit secrets.
Only Caddy exposes ports 80/443 to the internet. Everything else communicates through Docker's internal networks.
Step 4: Automatic HTTPS with Caddy + Let's Encrypt
Forget certbot, nginx config files, and renewal cron jobs. Caddy handles everything automatically.
Create Caddyfile:
yourdomain.com {
reverse_proxy app:3000
encode gzip
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
Referrer-Policy strict-origin-when-cross-origin
}
}
mail.yourdomain.com {
reverse_proxy listmonk:9000
}
What Caddy does automatically:
- Obtains SSL certificates from Let's Encrypt
- Renews them before expiry (no cron needed)
- Redirects HTTP → HTTPS
- Enables gzip compression
If you've ever fought with certbot + nginx, this alone is worth the switch.
Step 5: Email — Listmonk + Amazon SES
This is the biggest cost savings in the entire stack.
Mailchimp Standard: $20/mo for 500 contacts (jumps to ~$45 at 2,500 contacts) Listmonk + Amazon SES: ~$1/mo for 10,000 emails
Why Amazon SES and Not Just Any SMTP?
Listmonk supports any SMTP provider — but deliverability matters more than price. A self-hosted mail server or random SMTP relay will get emails flagged as spam.
Amazon SES provides:
- High deliverability backed by AWS infrastructure
- Built-in DKIM, SPF, and DMARC authentication
- IP reputation monitoring
- Bounce and complaint management
- $0.10 per 1,000 emails ($1 per 10,000 emails)
- Free tier: 3,000 emails/month for the first 12 months
The $0.10/1,000 covers infrastructure and reputation management that would take months to build independently.
Amazon SES Setup
- Go to AWS Console → SES
- Verify your domain (add the DNS records AWS provides)
- Request production access (takes 24-48 hours)
- Create SMTP credentials under Account Dashboard → SMTP Settings
Listmonk Config
Create listmonk/config.toml:
[app]
address = "0.0.0.0:9000"
admin_username = "admin"
admin_password = "your-admin-password"
[db]
host = "listmonk_db"
port = 5432
user = "listmonk"
password = "your-listmonk-db-password"
database = "listmonk"
ssl_mode = "disable"
After starting the stack, configure SES as your SMTP provider in Listmonk's admin panel:
- Host:
email-smtp.us-east-1.amazonaws.com(use your SES region) - Port: 587
- Auth protocol: PLAIN
- Username/Password: Your SES SMTP credentials
- TLS: STARTTLS
Now you have campaign emails, transactional emails, and subscriber management with analytics. Self-hosted. For pennies.
Step 6: Automated Backups
Create backup.sh:
#!/bin/bash
set -e
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/backups"
# Backup app database
pg_dump -h db -U app -d app -Fc > "$BACKUP_DIR/app_$TIMESTAMP.dump"
# Backup listmonk database
PGPASSWORD=$LISTMONK_PGPASSWORD pg_dump -h listmonk_db -U listmonk -d listmonk -Fc > "$BACKUP_DIR/listmonk_$TIMESTAMP.dump"
# Clean backups older than 30 days
find "$BACKUP_DIR" -name "*.dump" -mtime +30 -delete
echo "Backup done: $TIMESTAMP"
Test a Restore (Do This Once)
docker exec -it db createdb -U app app_test
docker exec -i db pg_restore -U app -d app_test < app_20250201_120000.dump
docker exec -it db psql -U app -d app_test -c "SELECT COUNT(*) FROM users;"
docker exec -it db dropdb -U app app_test
If you haven't tested a restore, you don't have backups. You have hopes.
Step 7: Auto-Deploy on Git Push
No need for GitHub Actions or Vercel's build pipeline.
Option A: Webhook Listener (Recommended)
Install webhook on your Ubuntu server:
sudo apt install webhook -y
Create /home/deploy/hooks.json:
[
{
"id": "deploy",
"execute-command": "/home/deploy/app/deploy.sh",
"command-working-directory": "/home/deploy/app",
"trigger-rule": {
"and": [
{
"match": {
"type": "payload-hash-sha256",
"secret": "your-webhook-secret",
"parameter": {
"source": "header",
"name": "X-Hub-Signature-256"
}
}
},
{
"match": {
"type": "value",
"value": "refs/heads/main",
"parameter": {
"source": "payload",
"name": "ref"
}
}
}
]
}
}
]
Run the webhook listener:
webhook -hooks /home/deploy/hooks.json -port 9001 -verbose
(Run it as a systemd service so it survives reboots.)
Add to your Caddyfile:
hooks.yourdomain.com {
reverse_proxy localhost:9001
}
Then in GitHub → Settings → Webhooks:
- Payload URL:
https://hooks.yourdomain.com/hooks/deploy - Content type:
application/json - Secret: same secret from
hooks.json - Events: Just the push event
Option B: Simple Cron Pull
If webhooks feel like overkill:
# crontab -e
*/5 * * * * cd /home/deploy/app && git fetch origin main && [ $(git rev-parse HEAD) != $(git rev-parse origin/main) ] && bash deploy.sh >> /var/log/deploy.log 2>&1
Checks every 5 minutes. Not instant, but dead simple.
The Deploy Script
Either way, deploy.sh stays the same:
#!/bin/bash
set -e
cd /home/deploy/app
git pull origin main
docker compose build app
docker compose run --rm app npm run db:migrate
docker compose up -d --no-deps app
docker image prune -f
echo "Deployed at $(date)"
Push to main → server builds and deploys automatically. ~30 seconds from push to live.
Production Checklist
- Ubuntu 22.04/24.04 LTS fully updated
- HTTPS working (Caddy + Let's Encrypt)
-
.envnot committed to git - Firewall active (
ufw status) - PostgreSQL healthchecks passing
- Listmonk admin panel accessible
- Amazon SES domain verified + production access
- Backup container running
- Restore tested manually
- Webhook or cron auto-deploy working
- Health endpoint live (
/api/health)
Cost Comparison
| Service | Managed Tool | Managed Cost | Self-Hosted on Raff |
|---|---|---|---|
| Hosting | Vercel Pro | $20/mo | $5-10/mo |
| Database | PlanetScale HA | $30/mo | $0 (PostgreSQL) |
| Mailchimp | $20-45/mo | ~$1/mo (Listmonk + SES) | |
| Auth | Clerk Pro | $25/mo | $0 (self-hosted) |
| Total | $95-120/mo | $6-11/mo | |
| Yearly | $1,140-1,440 | $72-132 |
Savings: $1,000+/year with a Raff Technologies VPS.
Full Stack Summary
Paid Services
| Tool | Purpose | Cost |
|---|---|---|
| Raff Technologies | VPS hosting — US-based, AMD EPYC, NVMe | $5-10/mo |
| Amazon SES | Email delivery via SMTP | $0.10 per 1,000 emails |
Open-Source / Free
| Tool | Purpose | Replaces |
|---|---|---|
| Ubuntu 22.04 LTS | Operating system | — |
| Docker | Containerization | — |
| Next.js | Web framework | — |
| PostgreSQL | Database | PlanetScale ($30/mo) |
| Caddy | Reverse proxy + auto HTTPS via Let's Encrypt | nginx + certbot |
| Listmonk | Email campaigns + transactional | Mailchimp ($20-45/mo) |
| webhook | Auto-deploy on git push | GitHub Actions / Vercel CI |
Two paid services. Everything else is free and open-source.
When to Upgrade
This setup handles most early-stage SaaS workloads comfortably.
Start lean. Scale when the product demands it.
Get Started
- Pick a Raff Technologies VPS plan →
- Follow this guide step by step
- Ship your SaaS for under $11/month
Have questions about which Raff Technologies VPS plan fits your stack? Get in touch — we're happy to help.
