MUJICA.DEV Writing ES Patagonia · CL
← Writing

Infrastructure

One small server.

Five side projects, one $12 droplet in Santiago, behind Cloudflare. No Vercel, no Supabase, no Redis.

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.

The entire production estate.

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:

ItemProvider/mo
Droplet · 1 vCPU / 2 GB · SantiagoVultr$12
DNS · CDN · WAF · R2Cloudflare$0
Transactional emailCloudflare~$5
Human mailboxesMXroute~$5
Off-box backupsR2 (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.

Built it, broke it, wrote it down. Questions → rene@mujica.dev.