Expose a Local Server with Cloudflare Tunnel
25 Nov 2025You don’t need a static IP or an open port to put a server on the internet. Cloudflare Tunnel runs a daemon on your machine that opens an outbound connection to Cloudflare’s network, and Cloudflare routes public traffic to it over that connection. Your router’s firewall never has to know this is happening.
This matters most for home servers, NAS boxes, or anything behind CGNAT where you genuinely can’t get a public IP even if you wanted one.
How it actually works
Normally, exposing a service means: get a public IP, forward a port on your router, open that port in your firewall, point DNS at the IP. Every one of those is an attack surface and a thing that breaks when your ISP changes your IP.
cloudflared, the tunnel daemon, flips this. It runs on your server and dials out to Cloudflare over HTTPS. Cloudflare terminates public traffic on your domain and forwards it down that same outbound connection to your local service. No inbound port is ever opened. Your local firewall can block all inbound traffic and the tunnel still works, because from the network’s perspective, nothing is coming in — it’s a response to a connection your server started.
flowchart LR
D["cloudflared on your server"] -->|"1 - dials out"| CF["Cloudflare edge"]
Vi["Visitor"] -->|"2 - HTTPS request"| CF
CF -->|"3 - routed down the tunnel"| D
D --> S["Local service"]
1. Install cloudflared
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared
chmod +x /usr/local/bin/cloudflared
2. Authenticate and create the tunnel
cloudflared tunnel login
cloudflared tunnel create home-server
This creates a tunnel and drops a credentials file under ~/.cloudflared/. That file is the only secret involved — anyone with it can route traffic through your tunnel, so treat it like an API key.
3. Configure routing
Create ~/.cloudflared/config.yml:
tunnel: home-server
credentials-file: /home/you/.cloudflared/<tunnel-id>.json
ingress:
- hostname: app.example.com
service: http://localhost:3000
- hostname: files.example.com
service: http://localhost:8080
- service: http_status:404
That last rule is mandatory — cloudflared requires a catch-all as the final entry, and http_status:404 is the right one unless you specifically want a default service for unmatched hostnames.
4. Point DNS at the tunnel
cloudflared tunnel route dns home-server app.example.com
cloudflared tunnel route dns home-server files.example.com
This creates CNAME records pointing at your tunnel’s unique <tunnel-id>.cfargotunnel.com address — you don’t manage an A record or an IP at all.
5. Run it as a service
sudo cloudflared service install
sudo systemctl enable --now cloudflared
Without this, the tunnel dies when your SSH session ends or the machine reboots, and every service behind it goes dark with no local error to tell you why — the failure is entirely on Cloudflare’s side, showing as a 502 to visitors.
What you give up
TLS terminates at Cloudflare’s edge by default, not on your box. If your compliance requirements need end-to-end encryption to the origin, you need to also run your local service over HTTPS and set originRequest.noTLSVerify or configure proper origin certs — the plain HTTP example above is fine for a home lab, not for anything handling sensitive data.
You’re also dependent on Cloudflare’s network being up and your outbound connection staying alive. Check tunnel health with:
cloudflared tunnel info home-server
Where this beats a VPN
If the alternative you’re comparing against is a VPN back into your home network, Cloudflare Tunnel is simpler for exposing specific services because you’re not extending your entire local network to every device that connects. You expose exactly the hostnames and ports you list in config.yml — nothing else on your LAN becomes reachable. That’s a meaningfully smaller attack surface than “now this laptop is on my home subnet.”