I wrote separately about why the monthly bill was the thing that finally made me move. This is the part that mattered more once the decision was made: the actual architecture, and specifically the three pieces that made a handful of scattered VPS instances feel like one coherent system instead of four boxes I had to remember the IP addresses of.
The problem with what I had before
Before this, every new project got its own VPS, provisioned by hand, with its own reverse proxy config, its own TLS setup, its own firewall rules I’d half-remember six months later. Nothing talked to anything else. If a service on box A needed to reach a database on box B, that meant either exposing the database publicly with an IP allowlist — a bad habit I kept meaning to fix — or wiring up a one-off VPN tunnel I’d inevitably forget existed.
The three problems, specifically: no shared private network between boxes, no single place to deploy from, and no consistent way to expose an internal service publicly without opening it to the entire internet.
The topology
Four nodes: two Oracle Cloud “always free” instances, a mini PC sitting at home, and one small paid VPS that stays reachable even if my home connection drops. None of that matters on its own — what makes it one system is that all four sit on the same Tailscale network.
Tailscale: the part that makes the rest possible
Tailscale sits underneath everything else. Every node gets a stable, private IP on the mesh (a 100.x address) regardless of where it physically lives or what its public IP is, and the four nodes can reach each other exactly as if they were on the same LAN. The part that matters isn’t “VPN,” it’s ACLs and tags — I don’t get one flat network where everything can reach everything. Each node is tagged by role (tag:app-host, tag:db, tag:ingress), and the ACL policy defines which tags can reach which ports on which other tags. The database node accepts connections from app hosts on the Postgres port and nothing else, regardless of what else is on the mesh.
That’s the actual upgrade over “just use a VPN”: a traditional VPN gets you one flat private network, and you’re back to firewall rules to segment it. Tailscale’s ACLs make the segmentation part of the network definition itself.
Coolify: the control plane
Before this, every deploy meant hand-writing a docker-compose.yml, wiring up Caddy or nginx as a reverse proxy, and manually renewing or configuring TLS. Coolify replaces that with one control plane across all the app-hosting nodes: push to a git branch, Coolify builds and deploys, handles the reverse proxy and TLS automatically, and gives me one dashboard instead of four boxes I have to SSH into separately to check what’s running. It’s not doing anything I couldn’t script myself — it’s doing the specific, repetitive parts I was re-doing badly by hand for every new project.
Self-hosted Supabase vs. Supabase Cloud
This is the one decision in the stack I’d actively tell people to think twice about before copying. Supabase Cloud is genuinely good, and for a project with real users I’d default to it — managed Postgres backups, point-in-time recovery, and someone else on call for the database at 3am are worth paying for once something depends on uptime you can’t personally guarantee.
I run it self-hosted on a dedicated OCI node because every project on it is either mine or low-stakes client-adjacent work where I’m already the one who’d get paged, and the projects don’t yet justify the managed price. The trade I’m making explicitly: I own backups (a nightly pg_dump to a separate node, not just a hope), and I own recovery if the node itself dies. That’s a real risk I’ve accepted, not one I’ve stopped thinking about.
Cloudflare Tunnel: ingress without an open port
The home mini PC is the one node that serves public traffic, and it has zero inbound ports open on the router. Cloudflare Tunnel runs as an outbound-only daemon on that box, establishes a connection out to Cloudflare’s edge, and Cloudflare routes public requests down that same connection. There’s no port-forward, no dynamic DNS hack, and no exposure if someone scans my home IP — there’s nothing listening on it to find.
What broke
The Tailscale ACL mistake I mentioned in the cost post: I once tagged a new node tag:app-host and forgot it also needed a rule to reach the DB node’s replica port, not just the primary. The service came up, connected to the primary fine, and a background job quietly failed for two days before I noticed the read-replica queries were timing out. Nothing catastrophic, but it’s the exact kind of failure this architecture makes possible — subtle, network-level, and easy to miss because everything mostly works.
The fix wasn’t a Tailscale problem, it was a process gap: any new node’s ACL tags now get checked against a short list of “what does this need to reach” before it goes live, instead of assumed correct because the primary connection worked.
Would I recommend this stack
If you already know you’re going to be the one operating whatever you build — client work, your own products, side projects with no on-call rotation but you — this combination gets you most of what a managed platform gives you, for infrastructure you actually understand end to end. If uptime is someone else’s job to guarantee, or you’d rather not own ACL debugging at 11pm, staying managed is the better call, not a worse one.