·5 min read·infrastructure
Share

One Mac, Three Domains, One Cloudflare Tunnel: Running Production Without a Server

Three live Next.js sites, one Mac, zero VPS bill and zero open inbound ports. The self-hosted pattern: one named Cloudflare tunnel, launchd, and a dark origin.

I run three production websites — a music catalog, a creator-economy site, and a sovereign-AI gallery — off a single Mac sitting in my apartment. No VPS. No Vercel bill. No inbound ports open on my router. The whole edge is one named Cloudflare tunnel and a handful of launchd jobs. This is the pattern, end to end, and why I'd build it the same way again.

One codebase, three tenants, three ports

All three sites are the same Next.js codebase running multi-tenant. I don't deploy three copies — I run three processes of the same build, each bound to its own localhost port:

  • Site A → localhost:3002
  • Site B → localhost:3001
  • Site C → localhost:3003

Each process is its own launchd job with KeepAlive set, so if one crashes it comes back on its own. The app reads the incoming Host header and renders the right tenant — colors, copy, schema, sitemap — from one shared src/lib layer. One npm run build produces the artifact all three serve.

The tunnel is the whole edge

Here's the part people miss: my Mac never accepts a single inbound connection from the public internet. The Cloudflare tunnel daemon (cloudflared) makes an outbound connection to Cloudflare's edge and holds it open. Public requests hit Cloudflare, get TLS-terminated there, and ride back down that outbound tunnel to my localhost ports.

The routing lives in one ingress file — conceptually:

ingress:
  - hostname: site-a.com
    service: http://localhost:3002
  - hostname: site-b.io
    service: http://localhost:3001
  - hostname: site-c.com
    service: http://localhost:3003
  - service: http_status:404

One tunnel, host-routed to three ports. Adding a fourth site is two lines and a DNS record — no new server, no new certificate, no firewall change.

Why launchd instead of pm2 or Docker

On a Mac, launchd is already the init system — it's what the OS uses to keep things alive. Wrapping each site (and the tunnel itself) in a launchd job means the supervisor is the platform, not a third-party process manager I have to babysit. A .plist per service, each with KeepAlive and RunAtLoad, and the machine boots straight back into a full production stack after a power blip. No pm2 resurrect, no Docker daemon eating RAM in the background.

Deploys are boring on purpose:

  • npm run build — if it exits non-zero, I stop. The old build keeps serving.
  • Only on a clean build: launchctl kickstart -k each site's job to pick up the new artifact.

A failed build never reaches the public site, because the kickstart only runs after the build returns 0.

A 10-minute health probe closes the loop

A separate launchd job pings every public hostname on a 10-minute interval and checks three things: the site is reachable, it returns 2xx, and the TLS cert has more than two weeks left. If anything fails it notifies me. Because the origin is dark and the edge is Cloudflare, the only things that actually break are a crashed Node process (launchd already restarts it) or a bad deploy (the health probe catches it before I would have).

What this buys you

  • No server bill. The hardware is a Mac I already own.
  • No attack surface. Nothing listens for inbound public traffic; the origin is unreachable except through the tunnel I control.
  • TLS for free. Cloudflare terminates it at the edge — no certbot, no renewal cron.
  • Sovereignty. The code, the data, and the box are all mine. I can pull the plug and the internet simply stops seeing my sites — nobody can take them down but me.

It's not the setup for a site doing a million requests an hour. For an independent operator running a few content sites that need to be fast, indexed, and fully owned, it's hard to beat one Mac, one tunnel, and launchd.

FAQ

Can you really run production websites from a home Mac?

Yes, for the right workload. Static-leaning Next.js sites with edge caching in front handle real traffic comfortably, and Cloudflare absorbs the spikes. The limit is sustained heavy concurrency or anything needing five-nines uptime — for that you want managed hosting. For owned content sites, a Mac plus a tunnel is plenty.

Why a Cloudflare tunnel instead of port forwarding?

Port forwarding means opening inbound ports on your router and exposing your home IP — an attack surface and a privacy leak. A tunnel reverses the direction: your machine dials out to Cloudflare, so nothing inbound is ever open. You also get edge TLS and DDoS protection for free.

How do you host multiple domains from one machine?

Run each site on its own localhost port as a separate process, then host-route in the tunnel's ingress rules so each public hostname maps to the right port. The Next.js app reads the Host header and renders the correct tenant from one shared codebase. Adding a domain is a couple of ingress lines plus a DNS record.

What happens when the Mac reboots?

launchd brings everything back. Each site process and the tunnel daemon are launchd jobs with RunAtLoad and KeepAlive, so after a reboot or power blip the machine boots straight back into the full production stack with no manual steps.

Follow Hellcat Blondie everywhere

OnlyFans, Instagram, TikTok, and more. One page, all links.

Related