Overview

A Cloudflare Tunnel lets a server behind NAT or a firewall serve HTTPS without opening inbound ports: the cloudflared daemon makes outbound connections to Cloudflare, and traffic for a hostname flows back through them. Use it to put Cloudflare’s protections (cloudflare-security-headers, WAF) in front of a private origin. This guide uses a locally managed tunnel, configured by a file; Cloudflare also offers dashboard-managed tunnels.

Prerequisites

  • A Cloudflare account with the domain’s DNS on Cloudflare (cloudflare-dns).
  • An origin service listening on a local port.
  • sudo on the origin server for the systemd service.

Steps

1. Install cloudflared

brew install cloudflared            # macOS

On Debian and Ubuntu, add Cloudflare’s package repository and apt install cloudflared; the repository commands are on Cloudflare’s downloads page. Confirm with cloudflared --version.

2. Authenticate and create the tunnel

cloudflared tunnel login                 # pick the zone; writes ~/.cloudflared/cert.pem
cloudflared tunnel create my-tunnel      # writes ~/.cloudflared/<UUID>.json

The credentials file is the tunnel’s identity. Back it up and keep it out of version control.

3. Write ~/.cloudflared/config.yml

tunnel: <UUID>
credentials-file: /home/<user>/.cloudflared/<UUID>.json
 
ingress:
  - hostname: app.example.com
    service: http://localhost:3000
  - service: http_status:404

The final rule with no hostname is a required catch-all; a configuration that has ingress rules must end with one. Add a hostname rule per additional service. If the origin speaks HTTPS, use service: https://localhost:443.

4. Route DNS

cloudflared tunnel route dns my-tunnel app.example.com
cloudflared tunnel run my-tunnel         # foreground test

The route creates a proxied CNAME for the hostname that points at the tunnel.

5. Run it as a service

sudo cloudflared --config /home/<user>/.cloudflared/config.yml service install
sudo systemctl enable --now cloudflared

cloudflared looks for its config under $HOME/.cloudflared/, and under sudo that is /root. Pass --config as shown, or the service starts with no config.

Verify it worked

curl -sI https://app.example.com | head -1        # HTTP/2 200
sudo systemctl restart cloudflared && curl -sI https://app.example.com | head -1

The tunnel also shows as healthy in the dashboard under Zero Trust, Networks, Tunnels.

Common errors

  • Browser error with a 502-style page. The origin is not listening on the port in config.yml; check with ss -tlnp | grep 3000.
  • Config changes have no effect. Restart the service: sudo systemctl restart cloudflared.
  • The wrong site is served. The UUID in config.yml belongs to a different tunnel, or an earlier ingress rule matches first; rules are evaluated in order.
  • tunnel login cannot open a browser on a headless server. Open the printed URL on another machine and authorize the zone.

FAQ

Do I need to open inbound ports for a Cloudflare Tunnel?

No; cloudflared makes outbound connections to Cloudflare, so a server behind NAT or a firewall can serve HTTPS without inbound ports.

Why does cloudflared require a catch-all ingress rule?

A configuration with ingress rules must end with a rule that has no hostname, such as service: http_status:404; rules are evaluated in order.

Why does the cloudflared systemd service start with no config?

Under sudo, cloudflared looks in /root/.cloudflared/, so pass --config with the full path to your config.yml when running service install.

What causes a 502-style error through a Cloudflare Tunnel?

The origin is not listening on the port named in config.yml; check it with ss -tlnp.