Cloudflare Tunnel: expose things cleanly
Put a service online on your own domain, over HTTPS, without opening a single port on your router. The tunnel goes out from your home, it doesn't come in.
In this guide
Guide checked 3 months ago: some commands may have changed. Let us know if so.
In short
Cloudflare Tunnel puts an app from your mini-PC online on your own domain, over HTTPS, without opening a port on your router or revealing your IP address: the cloudflared program opens an outbound connection to Cloudflare, and public traffic comes back down that tunnel. You need a domain managed by Cloudflare, and the free plan is enough. Keep it for web apps meant for the public, protect the rest with Cloudflare Access, and leave SSH, Ollama or a database on Tailscale.
Do this first: Tailscale: your private networkSecuring access
Tailscale solves one problem: you, from your devices, reach your machine from anywhere, privately. Perfect for working. But sometimes you want the opposite: for the public to reach an app you’ve just set up. A demo to show a colleague, a dashboard for the team, a page you want to share with a simple link. Tailscale can’t do that, it’s private by nature.
The classic answer is port forwarding on the router. It’s fragile (your IP changes, the router reboots, the config breaks), and above all it exposes your home’s IP address to the whole world. Bad idea. Cloudflare Tunnel solves all of this in a smarter way.
The thing that changes everything: the tunnel goes out, it doesn’t come in
Here’s the idea to keep firmly in mind. A small program, cloudflared, runs on your mini-PC and opens a connection toward Cloudflare, just like your browser opens a connection toward a site. The public traffic that arrives at myapp.your-domain.com then comes back down through that same tunnel to your local app.
The concrete consequences, and they’re beautiful:
- No port open on your router. Nothing to configure on the Freebox/Livebox side.
- Your home IP stays hidden. The world sees Cloudflare, never your address.
- Free, automatic HTTPS. The certificate is managed by Cloudflare.
- DDoS protection included, because everything goes through their network first.
The named tunnel, the method that lasts
There’s an express tunnel (we’ll come back to it at the end), but for a service that needs to last over time, you create a named tunnel. It survives reboots, keeps the same config, and plugs in cleanly as a system service. Here’s the full procedure, from the command line, with a config written in a file you can reread and version.
0 of 6 steps done Your ticks stay in this browser.
-
Install cloudflared on the mini-PC
We go through Cloudflare’s official apt repository, to get updates automatically:
# Add Cloudflare's GPG key sudo mkdir -p --mode=0755 /usr/share/keyrings curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \ | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null # Add the repository echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main" \ | sudo tee /etc/apt/sources.list.d/cloudflared.list # Install sudo apt update && sudo apt install cloudflared -
Connect cloudflared to your account
cloudflared tunnel loginThis opens your browser. You choose the domain to authorize, and Cloudflare drops a certificate into
~/.cloudflared/. That’s what proves this tunnel has the right to act on your domain. -
Create the named tunnel
cloudflared tunnel create miniThis creates a tunnel called
miniand writes a credentials file in JSON (something like~/.cloudflared/<a-long-id>.json). Note the identifier shown: we need it right after. -
Write the config file
Create
~/.cloudflared/config.yml. It’s the one that says: “traffic from theminitunnel arriving at such-and-such domain goes to such-and-such local service.”tunnel: <your-tunnel-id> credentials-file: /home/your-user/.cloudflared/<your-tunnel-id>.json ingress: # myapp.your-domain.com → the app running locally on port 8099 - hostname: myapp.your-domain.com service: http://localhost:8099 # Everything else gets a 404, required at the end of the list - service: http_status:404The
ingressblock reads top to bottom. The last linehttp_status:404is a required catch-all: without it,cloudflaredrefuses to start. You can add otherhostnameentries above it to expose several apps through the same tunnel. -
Route the DNS
cloudflared tunnel route dns mini myapp.your-domain.comThis command creates the DNS record at Cloudflare that points your subdomain to the tunnel. Once per hostname.
-
Test, then install as a service
Run the tunnel by hand to check that everything answers:
cloudflared tunnel run miniOpen
https://myapp.your-domain.comin a browser. If your local app responds, you’ve won. Stop it with Ctrl+C, then install it as a service so it survives reboots:# With sudo, ~ means /root: so give the full path to your config sudo cloudflared --config /home/your-user/.cloudflared/config.yml service install # Start the service and check that it's running sudo systemctl start cloudflared systemctl status cloudflaredThat’s the classic trap: run with
sudobut without--config,cloudflaredlooks for its config inroot’s folder and finds nothing. After each change toconfig.yml, asudo systemctl restart cloudflaredapplies it.
A thing that’s exposed is a thing that’s public
Let’s be clear about the risk, because it’s real. As soon as you route a hostname, anyone on the Internet can knock at that door. So:
- Put authentication in front of anything sensitive. Cloudflare Access can filter by email (only the accounts you authorize get through), it’s free for a small group (the free Zero Trust plan goes up to 50 users) and it saves you from exposing an admin dashboard to all comers.
- Never tunnel your raw services: SSH, the Ollama API (port 11434), a database. Those stay on Tailscale, full stop. Cloudflare Tunnel is for web apps meant to be public.
The mental rule: Tailscale for you, Cloudflare Tunnel for the world. Before exposing anything, take a pass through Securing access to know what to lock down.
The express tunnel, for a five-minute demo
Don’t need a permanent tunnel? One command, no config, no domain:
cloudflared tunnel --url http://localhost:8099
Cloudflare spits out a random trycloudflare.com URL you can share right away. Ideal for showing something to a colleague in a meeting. But it’s ephemeral: the URL changes every launch and disappears when you stop the command. Cloudflare keeps this mode for testing and demos, with no uptime guarantee (its docs). For a service meant to stick around, go back to the named tunnel above.
All the commands in this guide
Frequently asked questions
Can I use Cloudflare Tunnel without a domain name?
Yes, for a quick demo. The command cloudflared tunnel --url http://localhost:8099 gives you a random trycloudflare.com address you can share right away. It is short-lived: it changes every time you run it and disappears when you stop the command, and Cloudflare reserves this mode for testing, with no uptime guarantee. For a service meant to last, you need a domain and a named tunnel.
How do I hand my domain name over to Cloudflare?
Buy the domain from any registrar, or from Cloudflare directly; a .com costs around ten euros a year. Then point its nameservers to Cloudflare, a guided step in their dashboard that takes about ten minutes. Once the domain is active on their side, you can create the tunnel.
Should I create the tunnel from the command line or from the Cloudflare dashboard?
Both work. From the dashboard, you declare the tunnel and its addresses with the mouse, then copy an install command carrying a token onto the mini-PC, but the configuration then lives at Cloudflare. From the command line, it sits in a config.yml file on your machine, which you can reread and keep under version control.
Can I expose several apps through a single Cloudflare tunnel?
Yes. In the ingress block of the config.yml file, add one hostname entry per app, each pointing to its local port, above the final http_status:404 line. That last line is mandatory: without it, cloudflared refuses to start. Each new subdomain is routed once with cloudflared tunnel route dns.
Why can't cloudflared find its configuration once installed as a service?
When run with sudo, cloudflared looks for its configuration in root's folder and finds nothing. So you have to give it the full path to the file at install time, with sudo cloudflared --config /home/your-user/.cloudflared/config.yml service install. After each change to config.yml, sudo systemctl restart cloudflared applies it.
Terms in this guide: TailscaleIP addressCloudflare TunnelTerminal (command line)SSHOllamaAPI
Spotted a mistake?
A command stopped working, a price changed?
Tools change every month. Tell me what is wrong in this chapter and I will fix it and update its date.