Independent tech intelligence, checked against primary sources.

TechPulseMind Useful technology.
No manufactured hype.

How to Expose Your Home Server with Cloudflare Tunnel

Securely expose home services with Cloudflare Tunnel, set up systemd, configure Access policies, and verify without opening inbound ports.

How to Expose Your Home Server with Cloudflare Tunnel

Before you start: versions, package names and storage identifiers change over time. Check the commands against the official documentation for your setup before running them, especially anything that creates, deletes or overwrites data.

A Cloudflare Tunnel gives a service on your home server a public hostname without opening a single inbound port on your router. cloudflared makes an outbound connection to Cloudflare and traffic comes back down it, so your firewall stays shut and your home IP stays off the DNS record.

This guide covers the throwaway version, the permanent version, and the distinction between them that causes the most confusion once a tunnel is running.

Decide which kind of tunnel you want first

This is the choice that catches people out later, so make it deliberately. There are two ways to configure a named tunnel, and they are not interchangeable:

Remotely-managed Locally-managed
Configured from The Cloudflare dashboard config.yml on your server
Routes edited in Dashboard UI The file, then restart
Good for A few services, managed from anywhere Many services, config in version control

If you set up a tunnel locally and then go looking for its routes in the dashboard, Cloudflare tells you:

This tunnel is locally managed. Routes are configured via the local configuration file and cannot be modified from the dashboard.

That is not an error and nothing is broken — it is the expected message for a locally-managed tunnel. Edit config.yml on the server and restart the service instead. Everything below uses the locally-managed route, because it is the one that fits a homelab where you want the config in a file you can back up.

The 30-second version: quick tunnels

Before the permanent setup, know this exists. To expose a local port right now with no account, no domain and no config:

cloudflared tunnel --url http://127.0.0.1:3000

cloudflared prints a random *.trycloudflare.com URL that forwards to that port.

It is genuinely useful for showing a colleague a dev server or testing a webhook. It is also the wrong tool for anything permanent: the hostname is random, it changes every run, there is no access control, and the tunnel dies with the process. Use it as a demo, not as hosting.

Prerequisites for the permanent setup

  • An always-on Linux server (x86_64 or ARM) running the service you want to expose — say http://127.0.0.1:3000
  • A domain on Cloudflare nameservers, with permission to add DNS records
  • Outbound HTTPS from the server. Cloudflare Tunnel needs to reach port 7844; if you are behind a restrictive firewall, confirm that is allowed
  • sudo on the server

Step 1: Authenticate

cloudflared tunnel login

This opens a browser to pick the zone. On a headless box, copy the printed URL to a browser elsewhere — it writes a certificate into ~/.cloudflared/ when you are done.

Step 2: Create the tunnel

cloudflared tunnel create homelab

This prints a tunnel UUID and writes a credentials JSON file into ~/.cloudflared/<UUID>.json. Note the UUID; you need it next.

That JSON file is the tunnel’s secret. Back it up somewhere sensible and do not commit it alongside your config.

Step 3: Write config.yml

By default cloudflared expects its configuration at $HOME/.cloudflared/config.yml. A minimal single-service file:

tunnel: <Tunnel-UUID>
credentials-file: /home/youruser/.cloudflared/<Tunnel-UUID>.json

ingress:
  - hostname: app.example.com
    service: http://127.0.0.1:3000
  - service: http_status:404

Two rules about ingress worth internalising now rather than debugging later:

  • Rules are matched in order, first match wins.
  • The last rule must be a catch-all with no hostname — the http_status:404 line above. Without it cloudflared refuses to start.

Adding a second service is just another block above the catch-all:

ingress:
  - hostname: app.example.com
    service: http://127.0.0.1:3000
  - hostname: grafana.example.com
    service: http://127.0.0.1:3001
  - service: http_status:404

Step 4: Point DNS at the tunnel

cloudflared tunnel route dns homelab app.example.com

This creates the proxied CNAME in your zone. Repeat per hostname. If the record already exists, delete it first or the command will not overwrite it.

Step 5: Test in the foreground

cloudflared tunnel run homelab

Load the hostname in a browser. When it works, stop it with Ctrl+C and make it permanent — do not skip the foreground run, because errors here are far easier to read than in journald.

To confirm what Cloudflare thinks the tunnel is doing:

cloudflared tunnel info homelab

Step 6: Install as a service

sudo cloudflared service install

There is a specific trap here, and it is the most common failure in the whole process. Run with sudo, $HOME points to /root, so cloudflared looks for /root/.cloudflared/config.yml and does not find the file you wrote in your own home directory. The service installs and then fails to start.

Pass the path explicitly:

sudo cloudflared --config /home/youruser/.cloudflared/config.yml service install

Then:

sudo systemctl enable --now cloudflared
sudo systemctl status cloudflared
sudo journalctl -u cloudflared -f

On Windows the equivalent config path is %USERPROFILE%\.cloudflared\config.yml, and on macOS cloudflared installs as a launch agent that starts when you log in — which means it is not running when nobody is logged in, a poor fit for a server.

Put access control in front of it

A tunnel makes a service reachable from the internet. That is the whole point, and it is also the risk: a Grafana or Portainer panel with a default password is now exposed to everyone, not just your LAN.

For anything administrative, add a Cloudflare Access policy on the hostname so Cloudflare authenticates the request before it ever enters the tunnel. This is the step people postpone, and it is the one that matters most. The tunnel removes your router as a barrier; Access is what replaces it.

Troubleshooting the common failures

Symptom Usual cause
Service installs but will not start sudo and $HOME — pass --config explicitly (above)
cloudflared exits immediately on start Missing catch-all ingress rule, or invalid YAML indentation
Tunnel shows Inactive or Down The server cannot reach Cloudflare on port 7844
Hostname returns 502 Tunnel is up but the local service is not listening on the port in service:
Routes missing from the dashboard Expected — the tunnel is locally managed

Sources

Related reading

Some links on this page may be affiliate links. If you buy through them we may earn a commission at no extra cost to you. See our affiliate disclosure.