
Cloudflare Tunnel on Ubuntu: Publish Apps Safely
Publishing a self-hosted application usually means exposing ports on a router or firewall, assigning a reachable origin address, and protecting that address from direct traffic. Cloudflare Tunnel takes a different approach: the cloudflared connector initiates outbound connections to Cloudflare, and public requests are routed back through those established connections to a local service.
This tutorial creates a locally managed tunnel on Ubuntu, maps two hostnames with ingress rules, validates routing, installs the connector as a service, and covers access control and common failures. It assumes your domain already uses Cloudflare nameservers and your local application responds on 127.0.0.1:3000.
Understand what the tunnel changes
The origin no longer needs a public inbound port for the published application. The connector makes outbound connections, so the firewall can keep HTTP and HTTPS closed from the Internet while allowing the connector's required egress traffic.
This reduces origin exposure, but it is not application authorization. A hostname published through a tunnel is public unless you add Cloudflare Access or another authentication layer. Keep the application patched, validate forwarded request information, and apply least privilege to the connector credentials.
Verify the local application first
Do not debug the application and tunnel at the same time:
curl --fail --silent http://127.0.0.1:3000/health
ss -lntp | grep ':3000'
Binding the application to loopback is a useful default when only cloudflared and a local reverse proxy need access. If the connector runs in a different container or host, use a private address that is reachable from that connector instead.
Install cloudflared from Cloudflare's APT repository
sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \
| sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main"
| sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt-get update
sudo apt-get install cloudflared
cloudflared --version
Using the signed package repository gives the server a normal update path. Review connector release notes before production upgrades and avoid downloading an unverified binary from an unofficial mirror.
Authenticate and create the tunnel
cloudflared tunnel login
cloudflared tunnel create internal-apps
cloudflared tunnel list
The login command opens an authorization flow and creates an account certificate in the default cloudflared directory. Creating the tunnel returns a UUID and writes a tunnel-specific JSON credentials file. The account certificate can manage tunnels; the JSON file authorizes this connector to run one tunnel. Protect both and do not commit them.
For unattended infrastructure automation, use narrowly scoped Cloudflare API tokens and the supported API or Terraform workflow instead of copying a personal account certificate between servers.
Create explicit ingress rules
Create ~/.cloudflared/config.yml and replace the placeholder UUID:
tunnel: 11111111-2222-3333-4444-555555555555
credentials-file: /home/deploy/.cloudflared/11111111-2222-3333-4444-555555555555.json
ingress:
hostname: app.example.com
service: http://127.0.0.1:3000
originRequest:
connectTimeout: 10s
hostname: status.example.com
service: http://127.0.0.1:4000
service: http_status:404
Rules are evaluated from top to bottom. The final catch-all is required and prevents unmatched hostnames from falling through to an application. Use explicit hostnames for sensitive services rather than a broad wildcard unless the wildcard behavior is intentional.
Validate and test matching before launch
cloudflared tunnel ingress validate
cloudflared tunnel ingress rule https://app.example.com
cloudflared tunnel ingress rule https://unknown.example.com
The first command checks the configuration structure. The rule command shows which ingress entry would match a URL. The unknown hostname should reach the final 404 rule. This catches ordering mistakes before DNS sends users to the tunnel.
Create the public DNS routes
cloudflared tunnel route dns internal-apps app.example.com
cloudflared tunnel route dns internal-apps status.example.com
For a locally managed tunnel, this creates CNAME records pointing the hostname to the tunnel's cfargotunnel.com target. A DNS route and an ingress rule are separate requirements: DNS selects the tunnel, while ingress selects the local service.
Run the tunnel interactively once
cloudflared tunnel run internal-apps
In another terminal, verify the public hostname and inspect response headers:
curl --fail --head https://app.example.com
cloudflared tunnel info internal-apps
Test ordinary pages, uploads, streaming responses, WebSockets, and application-generated redirects. If the application builds absolute URLs, configure its trusted proxy and public base URL so it recognizes the external HTTPS scheme and hostname.
Install cloudflared as a Linux service
Cloudflare documents a service installation path for persistent operation. When using a locally managed configuration, run the installation with the configuration in its final location and verify what the generated unit references:
sudo cloudflared --config /home/deploy/.cloudflared/config.yml service install
sudo systemctl enable --now cloudflared
sudo systemctl status cloudflared --no-pager
sudo journalctl -u cloudflared -n 100 --no-pager
Do not delete or move the credentials file after installing the service. Ensure the service account can read the configuration and tunnel credential but other unprivileged users cannot. If you prefer a root-owned layout, move the files deliberately, update credentials-file, validate again, and reinstall or update the unit.
Keep the origin firewall closed
After the tunnel works, confirm that the application port is not exposed on a public interface:
ss -lntp | grep ':3000'
sudo ufw status verbose
sudo nft list ruleset
The ideal local output shows the app bound to 127.0.0.1:3000. Do not open ports 80, 443, or 3000 merely because the hostname is public through the tunnel. The connector still needs outbound connectivity; Cloudflare's current guide tells administrators behind restrictive firewalls to verify reachability to Cloudflare on port 7844.
Add authentication for private applications
A tunnel protects the path to the origin, not the contents of the application. For an admin panel or internal tool, create a Cloudflare Access self-hosted application for the hostname and define an allow policy using your identity provider, groups, device posture, or service tokens.
Keep server-side authorization inside the application. Edge authentication is an additional gate, not a replacement for checking whether a signed-in user may read a record or perform an action.
Plan availability and updates
One connector process is a single local failure point. Cloudflare supports replicas for the same tunnel. Run replicas on independent hosts or failure domains when uptime matters, and monitor tunnel health rather than relying on a successful DNS lookup.
When changing a locally managed configuration, validate the new file and bring up a replica with it before stopping the old connector. Cloudflare notes that stopping the only connector drops long-lived HTTP, WebSocket, TCP, and UDP flows; new connections resume when a connector is available.
Troubleshooting common failures
Cloudflare returns 502 Bad Gateway
The tunnel is reachable but cloudflared cannot connect to the configured origin. Run the exact local URL with curl, confirm the port, protocol, and container network, and inspect the connector journal.
The tunnel is healthy but the hostname returns 404
Use cloudflared tunnel ingress rule. The hostname may not match, a broader rule may appear first, or the request may correctly reach the catch-all.
Redirect loops appear
The origin may force HTTPS without trusting the proxy's forwarded scheme. Configure trusted proxy handling and the application's public URL. Do not disable TLS verification globally as a shortcut.
Uploads or WebSockets fail
Test the origin directly, then inspect application limits, proxy trust, timeouts, and protocol support. A basic page load does not prove every request type works.
The service cannot find config.yml
system services do not use the same home directory as an interactive shell. Inspect the systemd unit, use an absolute --config path, and verify file ownership and permissions.
Production checklist
- Verify the origin locally before configuring the tunnel.
- Install
cloudflaredfrom a signed, maintained source. - Protect account and tunnel credentials from source control.
- Use explicit ingress rules with a final catch-all.
- Validate configuration and test rule matching before DNS cutover.
- Bind private origins to loopback or a restricted network.
- Add Cloudflare Access for non-public applications.
- Monitor service logs, tunnel health, and public health checks.
- Use independent connector replicas when availability matters.
- Test rollback and long-lived connections during upgrades.