Running a service at home — a personal Git server, a Node app, a Pi hole dashboard — is easy. Making it safely reachable from outside your network is where mos…
Running a service at home — a personal Git server, a Node app, a Pi-hole dashboard — is easy. Making it safely reachable from outside your network is where most people either give up or make a security mistake, usually by forwarding a port straight through their router and hoping for the best. Cloudflare Tunnel solves this cleanly: no open inbound ports, no static IP required, and free HTTPS baked in. This guide walks through the full setup, from a bare install to a production-ready tunnel.
Why Cloudflare Tunnel Instead of Port Forwarding
Traditional port forwarding punches a hole in your router's firewall and points it at a machine on your LAN. That machine is now directly reachable by anyone scanning the internet — a real risk if it's ever misconfigured or unpatched. Cloudflare Tunnel flips the model: your server makes an outbound connection to Cloudflare's network, and Cloudflare routes public traffic to it through that tunnel. Nothing on your router needs to change, and no inbound port is ever opened.
Prerequisites / What You'll Need
- A domain name added to a Cloudflare account (free tier works fine)
- A Linux server or machine to run the tunnel from (a Raspberry Pi, VPS, or home server all work)
- Root or
sudo access on that machine
- A service already running locally that you want to expose (e.g., something on
localhost:3000)
- Basic comfort with the terminal
Step 1: Install cloudflared
cloudflared is Cloudflare's lightweight daemon that establishes and maintains the tunnel connection.
On Debian/Ubuntu:
curl -L https://pkg.cloudflare.com/cloudflare-main.gpg -o /usr/share/keyrings/cloudflare-main.gpg
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt update
sudo apt install cloudflared
Verify it installed correctly:
cloudflared --version
Step 2: Authenticate with Cloudflare
This links cloudflared to your Cloudflare account and the domain you'll be using:
cloudflared tunnel login
This opens a browser window (or prints a URL if you're on a headless server) asking you to select the domain you want to use. Once authorized, a certificate file is saved to ~/.cloudflared/cert.pem — this is what authenticates future tunnel commands, so treat it like a credential.
Step 3: Create the Tunnel
Give your tunnel a descriptive name — this becomes its identifier going forward:
cloudflared tunnel create homeserver
This generates a tunnel with a unique UUID and saves a credentials file (<UUID>.json) in ~/.cloudflared/. Take note of the UUID printed in the output; you'll need it in the next step.
Step 4: Configure Routing
Create a config file at ~/.cloudflared/config.yml:
tunnel: homeserver
credentials-file: /home/youruser/.cloudflared/<UUID>.json
ingress:
- hostname: app.yourdomain.com
service: http://localhost:3000
- hostname: files.yourdomain.com
service: http://localhost:8080
- service: http_status:404
A few things worth understanding here:
- Each
hostname entry maps a public subdomain to a local service and port.
- The final
service: http_status:404 is required — it's a catch-all rule that rejects any request not matching a defined hostname. Without it, cloudflared will refuse to start.
- You can point to any local service: a web app, an internal dashboard, even a raw TCP service with the right ingress rule type.
Step 5: Route DNS to the Tunnel
For each hostname in your config, create the matching DNS record:
cloudflared tunnel route dns homeserver app.yourdomain.com
cloudflared tunnel route dns homeserver files.yourdomain.com
This automatically creates a CNAME record in your Cloudflare DNS pointing to the tunnel — no manual DNS dashboard editing required.
Step 6: Run the Tunnel
Test it directly first:
cloudflared tunnel run homeserver
Visit https://app.yourdomain.com in a browser. If your local service is running on the configured port, it should load — over HTTPS, automatically, with no certificate setup on your end.
Once confirmed working, kill the foreground process and install it as a system service so it survives reboots and crashes:
sudo cloudflared service install
sudo systemctl enable --now cloudflared
Check that it's running:
sudo systemctl status cloudflared
Step 7: Lock It Down with Access Policies (Optional but Recommended)
For anything sensitive — an admin panel, internal tooling, a Git server — don't just rely on obscurity. Cloudflare Access lets you require authentication before traffic even reaches your service:
- In the Cloudflare dashboard, go to Zero Trust → Access → Applications
- Add an application for your tunnel's hostname
- Set a policy — for example, "allow only emails ending in
@yourdomain.com" or "require a one-time PIN sent to a specific email"
This adds a login wall in front of the service, enforced entirely at Cloudflare's edge, before any traffic reaches your home network.
Troubleshooting & Common Pitfalls
- "Unable to reach the origin service" errors. Usually means the local port in your
config.yml doesn't match what your app is actually listening on, or the service isn't running. Confirm with curl http://localhost:3000 on the server itself.
- DNS not resolving. CNAME propagation can take a few minutes. Also double-check the record was actually created —
cloudflared tunnel route dns sometimes silently no-ops if a conflicting record already exists.
- Tunnel connects but shows a 404. Check that your
hostname in config.yml exactly matches the DNS record, and that the catch-all http_status:404 rule isn't accidentally matching first (ingress rules are evaluated top to bottom — the catch-all must be last).
- Credentials file lost. If you lose the
<UUID>.json file, you can't recover it — you'll need to delete the tunnel (cloudflared tunnel delete homeserver) and recreate it. Back this file up somewhere safe, but never commit it to a public repo.
- Multiple services, one tunnel. You don't need a separate tunnel per app — a single tunnel can route many hostnames, as shown in the config above. Fewer tunnels are easier to manage and monitor.
- Server reboots and the tunnel doesn't come back. This means you skipped the
systemctl enable step — always run the tunnel as a system service, never just in a terminal session you might close.
Wrapping Up
You now have a home or VPS-hosted service reachable over HTTPS from anywhere, without opening a single inbound port on your router. That's a meaningfully smaller attack surface than traditional port forwarding, and it comes with free TLS certificates and optional identity-based access control layered on top — protection that would otherwise take real effort to set up manually.
From here, natural next steps include adding more services to the same tunnel, setting up Cloudflare Access policies for anything sensitive, and pairing this with a reverse proxy like Nginx locally if you're running several apps behind a single port.
Further Reading