I have a small pile of side projects. A WhatsApp appointment scheduler, a delayed-delivery messaging app, a travel-agency backoffice, a World Cup betting game for my friends, and a couple more circling the runway. For a while each one lived on a different free tier — Vercel here, Supabase there, MongoDB Atlas, a forgotten Oracle Cloud box, Resend for email. It worked, in the sense that a house of cards works until someone sneezes.
The problem with free tiers isn't the price. It's that each one is a separate dashboard, a separate failure mode, and a separate clock counting down to a pricing email. Supabase pauses your database after seven idle days. Vercel gets opinionated the moment you smell like commerce. I wanted one place I understood completely, that I could reason about at 2am, and that cost about the same as two coffees.
So I moved everything onto one server. Here's the whole map.
Users (LatAm)
│
┌──────────────┐
│ Cloudflare │ DNS · CDN · WAF · R2
└──────┬───────┘ proxied; origin firewalled to CF
│
┌───────────┴────────────────────────┐
│ Vultr · Santiago · 1 vCPU / 2 GB │
│ Dokploy (Traefik + Docker) │
│ ├─ atendi (Next.js) │
│ ├─ postamis (REST API) │
│ ├─ patagonica (Next.js) │
│ ├─ fubol (Next.js) │
│ ├─ shared-worker (pg-boss) │
│ └─ postgres-16 (pg_cron, vector) │
└─────────────────────────────────────┘
Admin in over a private tailnet.
Backups out: pg_dump → R2, nightly.
The edge: Cloudflare
Everything public enters through Cloudflare — DNS, CDN, WAF, and TLS. The droplet's real address is firewalled so it only answers traffic that arrived through Cloudflare; the origin is invisible from the open internet. The same account gives me R2 for object storage (S3-compatible, zero egress fees) and a transactional email service. One vendor quietly replaced four.
Compute: one Vultr droplet, run by Dokploy
The whole thing runs on a Vultr High Performance instance in Santiago — 1 vCPU, 2 GB of RAM, 50 GB of NVMe. Santiago because it's the only mainstream cloud region actually in Chile, which means about 5ms to the people who use these apps instead of a sad round trip to Virginia.
On top of it sits Dokploy, which is the part that makes this pleasant rather than masochistic. It's an open-source, self-hosted layer over Traefik and Docker that gives you Railway-style deploys: point it at a GitHub repo, it builds the Dockerfile, wires up routing and Let's Encrypt certs, done. Same developer experience as the platforms I left, except I own it and it costs nothing extra. Every app is just a container on a shared Docker network, so they find each other and the database by name.
One Postgres, many databases
There's a single Postgres 16 container, and
each project gets its own database and its own role inside
it. atendi, postamis,
patagonica, fubol — separate
databases, no shared tables, no cross-app reads. It's
boring, it's portable, and a pg_dump is the only
backup primitive I need. I added pgvector for
the apps that want embeddings and pg_cron for
in-database timers, both of which are just extensions rather
than another service to babysit.
Auth lives in each app (Better Auth or NextAuth, depending on the project), not in the database vendor. That was a deliberate move away from Supabase's row-level-security everything. Less magic, less lock-in, fewer surprises when I want to do something the framework didn't anticipate.
Background jobs, without Redis
Two of these apps are basically clocks. Atendi sends appointment reminders; Postamis is a messaging app whose entire premise is delivering things late, on purpose. Both need durable, scheduled work.
The reflex here is to reach for Redis and a job queue. I didn't. There's a single shared worker running pg-boss, which uses the Postgres I already have as its queue. It wakes up on a schedule and pokes each app over HTTP — it never reads anyone else's database. So the queue is durable, the jobs survive restarts, and I added exactly zero new moving parts. The thing you don't run can't page you.
Email, split on purpose
Email is the one place I deliberately don't consolidate, because the failure modes are different. Human mailboxes — the addresses I actually read — go through a dedicated mail host. App-generated mail (login codes, reminders) goes out through Cloudflare's email service, which auto-manages SPF, DKIM and DMARC because Cloudflare already runs my DNS. If a transactional send ever gets my sending reputation in trouble, it can't take my personal inbox down with it. Separation here is about safety, not cost.
Backups and the back door
Every night a script runs pg_dump on each
database and ships the result to R2 with rclone.
Off-box, versioned, inside the free tier, and I have a
restore-test script so the backups aren't just
Schrödinger's backups. Administration (the Dokploy
dashboard, SSH) happens over a private
Tailscale
network, never the public internet. The control plane simply
isn't reachable unless you're already on my tailnet.
The bill
The reason any of this is worth writing down:
| Item | Provider | /mo |
|---|---|---|
| Droplet · 1 vCPU / 2 GB · Santiago | Vultr | $12 |
| DNS · CDN · WAF · R2 | Cloudflare | $0 |
| Transactional email | Cloudflare | ~$5 |
| Human mailboxes | MXroute | ~$5 |
| Off-box backups | R2 (free tier) | $0 |
| Total | ~$22 |
Roughly $22 a month for the whole estate, against a ceiling I set myself at $40. The headroom isn't spent because nothing has earned it yet — when one project needs a bigger box, I bump the droplet and keep going. One server has comfortably carried five apps so far, and a 2 GB box building Docker images is the tightest part, not serving traffic.
Would I recommend it?
If you have paying customers and an on-call rotation: no, keep your managed everything. But for a solo developer with a handful of projects and no live clients, the platform tax is real and the operational cost of one well-kept server is lower than it looks. The trade I made is more setup up front in exchange for a system I fully understand and a bill that doesn't surprise me. So far it's been the right one.
The boring stack wins more often than it gets credit for.