Self-Hosting on Budget: Run Real Apps for $20/mo

by David Park
Self-Hosting on Budget: Run Real Apps for $20/mo

Most people assume self-hosting means either paying enterprise prices or babysitting a janky homelab at 2 AM. Neither is true. I've been running production SaaS on cheap VPS iron for nine years, and the gap between "good enough" and "expensive" is mostly marketing.

This guide is for indie hackers who want real apps running in production — not toy projects — without a $500/month AWS bill. I'll show you exactly what I run, what I pay, and what tradeoffs I've accepted. Self-hosting on budget is absolutely doable if you know what to optimize.

Why Cloud Costs Spiral So Fast

AWS, GCP, and Azure are engineered to make the first dollar easy and the hundredth dollar invisible. Egress fees, NAT gateway charges, load balancer hours — none of it shows up until your bill does. A "free tier" t2.micro instance with a small RDS, a load balancer, and 50 GB of egress will run you $80–120/month before you've written a single line of business logic.

Hetzner, by contrast, charges €3.79/month for a CX22 (2 vCPU, 4 GB RAM, 40 GB SSD) as of mid-2025. Egress is included up to 20 TB. That's not a promotional price — that's just what servers cost when a provider isn't subsidizing a stock price.

I'm not saying Hetzner is perfect. Their US East region has less capacity than EU, support is email-only, and their object storage (Hetzner S3-compatible) had a rough 2023. But for budget self-hosting, the price-to-performance ratio is hard to beat.

The $20/Month Stack I Actually Use

Here's my current setup, not a hypothetical:

  • 1× Hetzner CX32 (4 vCPU, 8 GB RAM): €7.49/month — runs three small SaaS apps via Docker Compose
  • 1× Hetzner CX22 (2 vCPU, 4 GB RAM): €3.79/month — handles monitoring, backups, and a staging environment
  • Hetzner Volume (40 GB block storage): €1.92/month — Postgres data directory lives here, separate from the OS disk
  • Hetzner Object Storage (250 GB): ~€2.50/month — daily pg_dump backups, static assets
  • Cloudflare Free — DNS, DDoS protection, and caching at zero cost

Total: roughly €15.70/month (~$17 USD at current rates). I round up to $20 for currency fluctuation.

That stack handles a combined ~8,000 monthly active users across three apps. It's not Twitter-scale. It doesn't need to be.

Picking the Right VPS for Budget Self-Hosting

Not all cheap VPS providers are equal. I've burned money on providers that oversell CPU, throttle disk I/O at peak hours, or disappear entirely (RIP, a few names I won't bother listing).

Here's a quick comparison of providers I've actually tested:

Provider Cheapest useful tier RAM Egress included Verdict
Hetzner €3.79/mo (CX22) 4 GB 20 TB Best value in EU
Vultr $6/mo 1 GB 1 TB Decent, more US PoPs
DigitalOcean $6/mo (Basic) 1 GB 1 TB Polished UX, pricier
Linode/Akamai $5/mo 1 GB 1 TB Solid, good support
Oracle Free Tier $0 1 GB (ARM) 10 TB Free but flaky provisioning

Oracle's Always Free tier looks amazing on paper — 4 ARM cores, 24 GB RAM if you stack Ampere instances — but provisioning availability in popular regions is a lottery, and I don't want my infrastructure dependent on a company that might kill the program. Use it for non-critical workloads if you get lucky with provisioning.

For most indie hackers, start with a single Hetzner CX32 and add capacity only when you actually need it.

Docker Compose Over Kubernetes (For Now)

Kubernetes is impressive engineering. It's also complete overkill for a single-node budget setup. The control plane alone wants 2 GB of RAM before your app runs a single request.

Docker Compose with a restart: always policy and a proper health check covers 90% of what most small SaaS apps need. Here's a minimal but production-worthy docker-compose.yml pattern I use:

version: "3.9"
services:
  app:
    image: myapp:latest
    restart: always
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/mydb
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16
    restart: always
    volumes:
      - /mnt/hetzner-volume/pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
      interval: 10s
      timeout: 5s
      retries: 5

  caddy:
    image: caddy:2
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data

volumes:
  caddy_data:

Caddy handles TLS automatically via Let's Encrypt. No Certbot cron jobs, no nginx config files that look like ancient runes. Point your DNS at the server, write a three-line Caddyfile, done.

myapp.com {
  reverse_proxy app:3000
}

That's the whole reverse proxy config. Caddy renews certs automatically. If you've been hand-rolling nginx configs, try Caddy once and you won't go back.

Postgres on a Budget VPS: What to Watch

Running Postgres on a small VPS is fine. Running it badly will bite you. Three things matter:

1. Put the data directory on a separate volume. If your OS disk fills up, Postgres crashes. A Hetzner Volume at €0.048/GB/month is cheap insurance. Mount it at /mnt/data and point PGDATA there.

2. Tune shared_buffers and work_mem for your RAM. The default Postgres config is designed for a machine with 1 GB of RAM in 2005. On a 4 GB VPS, set shared_buffers = 1GB and work_mem = 32MB. Use pgtune.leopard.in.ua to generate a full config for your hardware.

3. Automate backups before you need them. I run a simple shell script via cron that does pg_dump, compresses with zstd, and uploads to Hetzner Object Storage using rclone. The whole thing runs in under 2 minutes for a 5 GB database.

#!/bin/bash
set -euo pipefail

TIMESTAMP=$(date +%Y%m%d_%H%M%S)
DUMP_FILE="/tmp/mydb_${TIMESTAMP}.sql.zst"

pg_dump -U myuser mydb | zstd -o "$DUMP_FILE"
rclone copy "$DUMP_FILE" hetzner-s3:my-backups/postgres/
rm "$DUMP_FILE"

echo "Backup complete: $DUMP_FILE"

Keep 7 daily backups, 4 weekly, 3 monthly. S3-compatible storage is cheap enough that there's no excuse for a single-point backup.

Monitoring Without Paying for Datadog

Datadog starts at $15/host/month and climbs fast. For budget self-hosting, that's a significant percentage of your total infrastructure spend.

Here's what I use instead:

  • Netdata (free, open source): Real-time metrics, runs in Docker, has a free cloud dashboard at netdata.cloud. Install takes about 5 minutes.
  • Uptime Kuma: Self-hosted uptime monitoring. Runs in Docker, sends alerts via Telegram, Slack, email, or a dozen other channels. Completely free.
  • Loki + Grafana (if you want logs): Heavier to run, but a CX22 handles it fine for small log volumes.

For a two-server setup, Netdata + Uptime Kuma is all you need. I get paged if any service goes down, and I can see CPU/memory/disk trends without paying a SaaS tax.

Uptime Kuma setup is one Docker command:

docker run -d --restart=always \
  -p 3001:3001 \
  -v uptime-kuma:/app/data \
  --name uptime-kuma \
  louislam/uptime-kuma:1

Then proxy it behind Caddy like everything else.

Security Basics You Can't Skip

Running cheap hardware doesn't mean running insecure hardware. Three non-negotiable steps:

Disable password SSH login. Only key-based auth. Add this to /etc/ssh/sshd_config:

PasswordAuthentication no
PermitRootLogin no

Use UFW. Allow only what you need:

ufw default deny incoming
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable

Enable unattended upgrades for security patches. On Debian/Ubuntu:

apt install unattended-upgrades
dpkg-reconfigure --priority=low unattended-upgrades

This handles kernel and package security patches automatically. You still need to handle major version upgrades manually, but day-to-day CVEs get patched without you thinking about it.

For more on hardening a fresh VPS, check out this guide on initial server setup for indie hackers.

When to Scale Up (And When Not To)

The temptation when things get slow is to throw money at bigger hardware. Sometimes that's right. Often it's not.

Before upgrading, profile first:

  • Is it CPU? Run top or htop during peak load. If you're consistently above 80% CPU, you might need more cores — or you might have an N+1 query problem.
  • Is it memory? Check free -h. If you're swapping, add RAM. If you're not swapping and memory looks fine, the problem is elsewhere.
  • Is it database? pg_stat_statements will show you your slowest queries. Adding an index often beats adding a server.

I ran one of my apps on a CX22 until it hit 3,000 MAU. Moving to a CX32 cost me an extra €3.70/month. A proper database index eliminated a slow query that was causing 80% of my CPU usage. The index was free.

For a deeper look at when self-hosting starts to strain, see the comparison on VPS performance tuning for small SaaS.

Self-Hosting on Budget: The Bottom Line

Self-hosting on budget isn't about heroic sysadmin work or accepting broken infrastructure. It's about being deliberate: pick providers with honest pricing, use boring tools that work (Docker Compose, Caddy, Postgres), automate backups before you need them, and monitor enough to know when something breaks.

My $20/month stack has had better uptime than some AWS-hosted products I've used as a customer. Cheap doesn't mean unreliable — it means you have to be thoughtful instead of throwing managed services at every problem.

What to do tomorrow: Sign up for a Hetzner account, spin up a CX22 (€3.79/month, cancel anytime), and migrate one non-critical app to Docker Compose with Caddy. Get comfortable with the workflow before you move anything important. The learning curve is one weekend, and the savings are permanent.