Most guides to putting a home service online start with a step you might not be able to do: log into your router and forward port 443. I couldn't do it either, because my connection sits behind carrier-grade NAT — my router's "public" address is shared with hundreds of other customers, and forwarding a port simply isn't something the ISP lets me do. Your version of the wall might look different, maybe a public IP that changes every few days or a landlord's router you can't touch, but it works out to the same thing.
Cloudflare Tunnel goes around all of it. Nothing at your house listens for incoming connections. Instead, a small program on your machine dials out to Cloudflare and keeps that connection open. When someone visits your domain, the traffic hits Cloudflare first, then rides back down that same connection into your house. Your firewall stays shut. Your ISP never has to cooperate.
It's free, and the HTTPS certificate is included.
What you need
- A domain, with its nameservers pointed at Cloudflare (the free plan is fine)
- Any machine at home that stays on: a NAS, a mini PC, a Raspberry Pi
- A service already running on that machine, reachable at
http://localhost:<port>from the machine itself
That last point matters more than it looks. Get your service working locally
first. If curl http://localhost:3000 doesn't return your page while you're
sitting on that machine, the tunnel can't help you. It only carries traffic;
it doesn't fix a service that isn't running.
The mental model
Three pieces have to line up. Mixing them up is where most of the confusion comes from, so here they are:
| Piece | Lives where | What it does |
|---|---|---|
| Tunnel | Cloudflare's dashboard, one per machine | The persistent outbound connection |
Connector (cloudflared) |
Your machine | Dials out and keeps the tunnel alive |
| Public hostname | Cloudflare's dashboard | Maps app.example.com to a local address |
One tunnel can carry many hostnames. So one connector on your NAS can publish a dozen services on a dozen subdomains. What one tunnel can't do is span two machines.
Create the tunnel
In the Cloudflare dashboard, go to Zero Trust → Networks → Tunnels and create a tunnel (Cloudflare's own docs cover the same steps in more detail). Name it after the machine, not the app. You'll add more apps later, and a tunnel called "jellyfin" running six other things gets confusing fast.
Cloudflare hands you a token. Treat it like a password — anyone who has it can publish services onto your domain.
This is the "remotely managed" style of tunnel, where the routing config
lives in the dashboard instead of a local config.yml. For a home setup
that's the right default. There's no config file to lose, and nothing to
re-edit when you add a hostname later. One trade-off worth knowing: the
dashboard is now the source of truth. When you're wondering "why does this
hostname point there?", the answer is never in a file on your machine.
Run the connector
If you have Docker:
docker run -d --name cloudflared --restart unless-stopped \
--network host \
cloudflare/cloudflared:latest tunnel --no-autoupdate run --token <YOUR_TOKEN>
--network host matters. The connector needs to reach services running on
the host itself. Inside a normal bridge network, localhost means the
container — not your machine — and nothing will connect.
If you don't have Docker, or your NAS won't let your user anywhere near it,
that's fine too, because cloudflared is just a single file
(grab the right build from the releases page)
with no dependencies:
# pick the build matching your architecture
curl -L -o cloudflared \
https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64
chmod +x cloudflared
nohup ./cloudflared tunnel --no-autoupdate run --token <YOUR_TOKEN> > cloudflared.log 2>&1 &
I want to say this plainly, because a lot of NAS tutorials treat Docker as a requirement: it isn't. All the container really buys you is a restart policy and log rotation, so on a locked-down box where you can't reach the Docker socket, the bare binary isn't a fallback at all — it's the shorter path. I hit exactly that wall on a UGREEN NAS, and that guide walks through it.
Give it a few seconds and the dashboard should show the connector as healthy.
Publish a hostname
Add a public hostname to the tunnel: subdomain app, your domain, and a
service URL of http://localhost:3000.
Use http://, not https://. This is the one that gets everybody,
including me. Your local service is almost certainly speaking plain HTTP
with no certificate. Tell Cloudflare to reach it over https:// and the TLS
handshake fails, so every request comes back as 502 Bad Gateway — with
nothing in the error hinting that the protocol scheme is the problem. Your
visitors still get real HTTPS. Encryption ends at Cloudflare's edge, and the
hop from there into your house runs inside the tunnel.
The DNS record gets created for you. There's nothing to add by hand.
Check that it actually works
curl -sI https://app.example.com | head -3
A 200 means the whole chain works. If you get a 502, check in this order:
- Does
curl http://localhost:3000work on the machine itself? If not, the problem was never the tunnel. - Is the service URL
http://and nothttps://? - Is the port right? A service bound to
127.0.0.1while the connector runs in a bridge network is the same failure wearing a different hat.
A 530 or 1033 error means the tunnel itself is down. Check that the connector process is actually running — not just that you started it once.
Make it survive a reboot
A tunnel that dies when the power flickers is a demo, not infrastructure.
With Docker, --restart unless-stopped handles it. Without Docker, use
whatever your system gives you: systemd on a normal Linux box, or the
scheduled-task UI on a NAS that has locked cron away.
Then actually reboot and watch it come back, because "it should come back" and "it came back" are different claims. The test takes five minutes, and it saves you from finding out the hard way during a power cut while you're traveling.
What this doesn't solve
Your site is now exactly as available as your house. Cloudflare doesn't cache HTML by default, so nearly every request travels back to your machine, which means a power cut, an ISP outage, or someone unplugging the wrong thing while vacuuming takes the site down with it. For a personal project that's a fair trade. For anything where downtime costs money, it's a real limit, and no tunnel setting changes it.
One hard number to know before you lean on it: requests through Cloudflare's free plan are capped at 100 MB of upload per request, and a tunnel can't opt out. Browsing, streaming, and dashboards will never notice it, but uploading your phone's camera roll to your own Immich or Nextcloud hits it immediately, unless the app splits uploads into smaller chunks. Check that before you promise yourself off-cloud photo backup through a tunnel.
And nothing here controls who gets in. A public hostname is public. If you're exposing something that was never meant to face the internet — a media server's admin panel, a database UI — put Cloudflare Access in front of it before you point the world at it.
When it's worth paying
Free covers more than people expect here: unlimited tunnels, unlimited hostnames, the certificate, and enough bandwidth that a content site will never notice a limit. For publishing services from home, there's genuinely nothing to buy.
The point where money starts to make sense isn't traffic, it's availability. If "what happens when the power goes out at home?" stops being an acceptable question, no tunnel feature fixes that — the fix is moving the service into someone else's building, which means a cheap VPS. I've written a separate guide on when free self-hosting actually runs out, because that's a different decision, made for a different reason, and it's worth making on purpose instead of drifting into it.