Overview
A Cloudflare Tunnel connects a server behind NAT or a firewall to the internet without opening inbound ports. The cloudflared daemon opens outbound connections to Cloudflare’s edge; traffic flows through those connections. Use a tunnel when you want a private VPS or home server reachable over HTTPS with Cloudflare’s cloudflare-security-headers applied, without exposing SSH or a public IP.
Prerequisites
- A Cloudflare account with the target domain added and nameservers pointed to Cloudflare. See cloudflare-dns.
- A server running the origin service (a web app, a reverse proxy, etc.) that listens on a local port.
sudoaccess on the origin server to installcloudflaredand register a systemd service.
Steps
1. Install cloudflared
Ubuntu / Debian
curl -L https://pkg.cloudflare.com/cloudflare-main.gpg | \
sudo gpg --dearmor -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 -y cloudflared
cloudflared --versionmacOS
brew install cloudflared2. Authenticate with Cloudflare
cloudflared tunnel loginA browser opens. Select the zone (domain) this tunnel will use. cloudflared writes a certificate to ~/.cloudflared/cert.pem.
3. Create a named tunnel
cloudflared tunnel create my-tunnel
# Tunnel credentials written to /root/.cloudflared/<UUID>.jsonRecord the UUID printed in the output. The credentials file is the tunnel’s identity; back it up.
4. Create the tunnel config
Write /etc/cloudflared/config.yml (or ~/.cloudflared/config.yml for a user-level tunnel):
tunnel: <UUID>
credentials-file: /root/.cloudflared/<UUID>.json
ingress:
- hostname: app.example.com
service: http://localhost:3000
- service: http_status:404The last service: http_status:404 is a required catch-all. Requests that do not match any hostname rule return a 404. Add more hostname entries for additional services on the same server.
5. Route the hostname through DNS
cloudflared tunnel route dns my-tunnel app.example.comThis creates a CNAME record pointing app.example.com to <UUID>.cfargotunnel.com. The record is automatically proxied through Cloudflare (orange cloud). Verify it appeared:
cloudflared tunnel info my-tunnel6. Run as a systemd service
sudo cloudflared service install
sudo systemctl enable --now cloudflared
sudo systemctl status cloudflaredcloudflared service install writes a systemd unit that reads /etc/cloudflared/config.yml. The service starts on boot and restarts on failure.
For user-level installs (non-root), use cloudflared service install --user.
Verify it worked
# 1. The tunnel is listed as HEALTHY in the Cloudflare dashboard
# under Zero Trust > Networks > Tunnels.
# 2. The hostname resolves through the tunnel.
curl -sI https://app.example.com | head -3
# HTTP/2 200
# 3. The service restarts cleanly.
sudo systemctl restart cloudflared
curl -sI https://app.example.com | head -1
# HTTP/2 200Common errors
tunnel loginnever opens a browser. Run on a headless server? Copy the URL from stdout and open it on a different machine, then authenticate. The cert writes back to the server.ERR_TUNNEL_CONNECTION_FAILEDin browser. The origin service is not listening on the port inconfig.yml. Verify withss -tlnp | grep 3000.- Changes to
config.ymldo not take effect. The service caches the config. Runsudo systemctl restart cloudflared. - Multiple tunnels and wrong hostname served. Each tunnel has its own UUID and credentials file. Confirm the UUID in
config.ymlmatches the tunnel created in step 3. - TLS handshake errors on the origin. By default
cloudflaredconnects to the origin over plain HTTP. If the origin requires HTTPS, setservice: https://localhost:443and addoriginServerName: app.example.comunder the ingress rule.