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.
sudoon the origin server for the systemd service.
Steps
1. Install cloudflared
brew install cloudflared # macOSOn 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>.jsonThe 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:404The 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 testThe 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 cloudflaredcloudflared 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 -1The 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 withss -tlnp | grep 3000. - Config changes have no effect. Restart the service:
sudo systemctl restart cloudflared. - The wrong site is served. The UUID in
config.ymlbelongs to a different tunnel, or an earlier ingress rule matches first; rules are evaluated in order. tunnel logincannot 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.