Homelab Haven

Cloudflare Tunnel vs. Tailscale: Which One Should Guard Your Homelab?

June 15, 20265 min read

New to Docker Compose? Start with the basics first — this series assumes you can already read a compose.yaml.

At some point every homelab hits the same question: how do you reach your own services from outside your house without opening ports on your router and hoping for the best? Two tools come up constantly — Cloudflare Tunnel and Tailscale — and they get compared like they're competing answers to the same question. They aren't. They solve two different problems that happen to both involve the word "tunnel."

Summary

  • Cloudflare Tunnel publishes an app to the public internet, at a real domain, for anyone to reach — no port forwarding, because your server opens an outbound connection to Cloudflare instead of accepting inbound ones.
  • Tailscale puts your own devices on one private network (a "tailnet") — nothing is public, ever; only devices you've explicitly added can reach anything.
  • The real question isn't "which is better," it's "does this app need to be public?" I run both, side by side, and let that one question decide per app.
  • Part 2 sets up Cloudflare Tunnel; Part 3 sets up Tailscale.

Two different threat models

Cloudflare Tunnel answers: "I want my family / the internet / a webhook provider to reach this app at a real URL, without exposing my home IP or opening a port." The cloudflared daemon on your server makes an outbound connection to Cloudflare's edge; Cloudflare terminates public traffic there and relays it down that same connection. Your router's firewall never has an inbound rule to manage, because there's nothing incoming to allow — from the router's point of view, it just looks like your server browsing the web.

Tailscale answers a completely different question: "I want my own laptop, my phone, and my server to all be able to reach each other directly, as if they were on the same LAN — without either of us being reachable by the actual internet." It builds a private mesh (a tailnet) over WireGuard; each device gets a stable 100.x.y.z address that only other devices on that same tailnet can reach. There is no public endpoint at all — someone without a Tailscale account and an invite to your tailnet cannot reach these apps, full stop, regardless of what they know about your network.

graph TB
  subgraph "Public internet"
    ANY[Anyone, any browser]
  end
  subgraph "Cloudflare edge"
    CF[Cloudflare]
  end
  subgraph "Your tailnet — private, invite-only"
    YOU[Your phone / laptop]
  end
  subgraph "Your server"
    CFD[cloudflared<br/>outbound only]
    TS[Tailscale<br/>on the host]
    APP1[Public app<br/>e.g. a blog]
    APP2[Private app<br/>e.g. admin tools]
  end

  ANY -->|https://app.example.com| CF
  CF -->|relayed through the<br/>outbound tunnel| CFD
  CFD --> APP1

  YOU -->|100.x.y.z, WireGuard| TS
  TS --> APP2

The hybrid I actually run

Nearly every real homelab ends up running both, because most homelabs have both kinds of app:

  • Public, on purpose: a blog, a request page my household uses from their phones on cellular data, a webhook endpoint some SaaS needs to reach. These go through Cloudflare Tunnel — real domain, real TLS certificate, reachable from anywhere.
  • Private, on purpose: admin dashboards, anything holding financial or personal data, internal tooling with no login screen worth trusting to the public internet. These are Tailscale-only — no Cloudflare route exists for them at all, by policy, not by oversight. If it's not meant to be public, the fix isn't "add a password," it's "don't make it reachable in the first place."

That second rule is worth stating explicitly because it's easy to get backwards: the temptation is to expose everything through Cloudflare and lock the sensitive ones down with auth. The safer default is the opposite — default private, and only promote an app to public when it genuinely needs to be. An app with no public route can't be found by a scanner, can't be brute-forced from outside, and can't leak through a misconfigured auth rule, because there's no path to it that doesn't already require tailnet membership.

What they cost you

Neither is free in complexity, just in different currencies:

Cloudflare TunnelTailscale
Reachable byAnyone (or gated by Cloudflare Access)Only devices on your tailnet
Needs a domainYesNo
Needs a Cloudflare accountYes (free tier is enough)No
Open inbound portsNoneNone
Setup per new deviceNone — it's server-sideInstall Tailscale, log in
Good forAnything meant to be publicAnything meant to stay private

Neither requires forwarding a port on your router — that part of the old "self-hosting means opening your firewall to the internet" advice is genuinely gone. Both do it by having your side open an outbound connection instead of waiting for inbound ones.

Which do you actually need?

  • Only want to reach your own stuff, from your own devices? Start with Tailscale alone — it's the simpler setup, and "nothing is public" is a very good default while you figure out what you're doing.
  • Need someone else to reach an app — family, a webhook, the public? You need Cloudflare Tunnel for that app specifically, whether or not you also run Tailscale for everything else.
  • Both, per app, based on who needs to reach it? That's the hybrid above, and it's where most homelabs land eventually.

Checkpoint

You should be able to answer, for any app you're about to deploy: "does anyone outside my tailnet need to reach this?" Yes → it's a Cloudflare Tunnel candidate. No → keep it on Tailscale only, and don't publish a route for it at all.

Next: Part 2 — Cloudflare Tunnel with Docker.

If this saved you some time, a coffee keeps the lights on and the posts coming.

☕ Buy me a coffee