Cloudflare Tunnel vs. Tailscale: Which One Should Guard Your Homelab?
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 Tunnel | Tailscale | |
|---|---|---|
| Reachable by | Anyone (or gated by Cloudflare Access) | Only devices on your tailnet |
| Needs a domain | Yes | No |
| Needs a Cloudflare account | Yes (free tier is enough) | No |
| Open inbound ports | None | None |
| Setup per new device | None — it's server-side | Install Tailscale, log in |
| Good for | Anything meant to be public | Anything 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.
If this saved you some time, a coffee keeps the lights on and the posts coming.
☕ Buy me a coffee