$linuxjunkies
>

Expose a Service with Cloudflare Tunnel

Expose local services to the internet without port-forwarding using Cloudflare Tunnel. Install cloudflared, create a named tunnel, configure ingress rules, and run as a systemd service.

IntermediateUbuntuDebianFedoraArch8 min readUpdated June 7, 2026

Before you start

  • A domain name with DNS managed by Cloudflare (free account works)
  • A local service already running and listening on a TCP port
  • sudo or root access on the host machine
  • Outbound internet access on TCP 443 from the host

Cloudflare Tunnel (formerly Argo Tunnel) lets you expose a local service to the internet without opening a single port on your router or firewall. The cloudflared daemon creates an outbound-only connection to Cloudflare's edge, which proxies public traffic back to your service. This replaces port-forwarding, dynamic DNS hacks, and the risk of exposing your home IP. You need a domain managed by Cloudflare — the free plan works fine.

Install cloudflared

Debian / Ubuntu

Cloudflare publishes a signed apt repository. Add it and install:

curl -fsSL 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 cloudflared -y

Fedora / RHEL / Rocky

sudo dnf install -y dnf-plugins-core
sudo dnf config-manager --add-repo \
  https://pkg.cloudflare.com/cloudflared-ascii.repo
sudo dnf install -y cloudflared

Arch Linux

yay -S cloudflared
# or with paru:
paru -S cloudflared

Verify the install:

cloudflared --version
# cloudflared version 2024.x.x (built ...)

Authenticate cloudflared to Your Cloudflare Account

This step opens a browser window so you can log in and authorise a specific zone (domain). On a headless server, copy the URL from the terminal and open it on another machine.

cloudflared tunnel login

After authorisation, a certificate file is written to ~/.cloudflared/cert.pem. This credential lets cloudflared create and manage tunnels for the chosen zone.

Create a Named Tunnel

Named tunnels have a stable UUID and credential file, so the tunnel identity survives daemon restarts and host reboots. Use a descriptive name — you cannot rename a tunnel later.

cloudflared tunnel create my-homelab

Output will confirm the tunnel UUID and the path to its credential JSON, typically ~/.cloudflared/<UUID>.json. Note the UUID — you will reference it in the config file.

Write the Tunnel Configuration File

Create ~/.cloudflared/config.yml (or /etc/cloudflared/config.yml for a system-wide service). The key sections are tunnel, credentials-file, and ingress.

mkdir -p ~/.cloudflared
cat > ~/.cloudflared/config.yml <<'EOF'
tunnel: my-homelab
credentials-file: /home/youruser/.cloudflared/<UUID>.json

ingress:
  # Serve a local web app on subdomain app.example.com
  - hostname: app.example.com
    service: http://localhost:8080

  # Serve a Grafana instance
  - hostname: grafana.example.com
    service: http://localhost:3000

  # Catch-all rule — required, must be last
  - service: http_status:404
EOF

Replace <UUID> with the actual UUID from the previous step, youruser with your username, and adjust hostnames and ports to match your services. Every ingress block must end with a catch-all that has no hostname field; without it, cloudflared will refuse to start.

Ingress Rule Options

  • http://localhost:PORT — plain HTTP to a local service (most common)
  • https://localhost:PORT — TLS to a local service; add originRequest: {noTLSVerify: true} if using a self-signed cert
  • tcp://localhost:PORT — raw TCP (requires cloudflared access on the client side)
  • ssh://localhost:22 — SSH via browser or cloudflared access ssh
  • http_status:404 — return a static HTTP status (used for catch-all)

Create DNS Records

For each hostname in your ingress config, create a CNAME pointing to the tunnel. The cloudflared CLI can do this automatically:

cloudflared tunnel route dns my-homelab app.example.com
cloudflared tunnel route dns my-homelab grafana.example.com

This creates a CNAME record in Cloudflare DNS: app.example.com → <UUID>.cfargotunnel.com. Cloudflare proxies the record (orange cloud), so your origin IP is never exposed.

Run the Tunnel as a systemd Service

Running cloudflared tunnel run manually is fine for testing. For production, install it as a systemd service so it starts on boot and restarts on failure.

Install the service (all distros)

If the config file is in /etc/cloudflared/config.yml, install system-wide:

sudo mkdir -p /etc/cloudflared
sudo cp ~/.cloudflared/config.yml /etc/cloudflared/config.yml
sudo cp ~/.cloudflared/<UUID>.json /etc/cloudflared/<UUID>.json
sudo cp ~/.cloudflared/cert.pem /etc/cloudflared/cert.pem

# Update credentials-file path in the system config
sudo sed -i 's|/home/youruser/.cloudflared|/etc/cloudflared|g' \
  /etc/cloudflared/config.yml

sudo cloudflared service install
sudo systemctl enable --now cloudflared
sudo systemctl status cloudflared

cloudflared service install writes a unit file to /etc/systemd/system/cloudflared.service. The service runs as root by default; for tighter security, create a dedicated system user and adjust the unit's User= directive.

Verify the Tunnel Is Working

# Check the daemon is connected
cloudflared tunnel info my-homelab
# Should show: Status: healthy, Connections: 4 (one per PoP)
# Test HTTP access from outside (replace with your actual hostname)
curl -I https://app.example.com
# HTTP/2 200  (or whatever your app returns)
# Check systemd journal for errors
sudo journalctl -u cloudflared -n 50 --no-pager

Replacing Port-Forwarding: Key Differences

  • No inbound ports required. Your router/firewall needs zero holes. The tunnel is outbound TCP 443 (or 7844 for QUIC).
  • No dynamic DNS. Cloudflare resolves the CNAME regardless of your ISP-assigned IP changing.
  • TLS is automatic. Cloudflare terminates TLS at the edge with a managed certificate. Your origin service can stay on plain HTTP locally.
  • Access policies. In the Cloudflare Zero Trust dashboard you can layer on email-based or SSO authentication — far stronger than exposing a port protected only by an application password.
  • Traffic routing via Cloudflare. All public traffic passes through Cloudflare's network. For most self-hosted use cases this is fine; for very high bandwidth (video streaming, large file hosting) check your plan limits.

Troubleshooting

Tunnel shows "no connections"

Check outbound connectivity on port 7844 (QUIC) and 443 (fallback). Some corporate networks block 7844; force TCP fallback:

echo 'protocol: http2' >> /etc/cloudflared/config.yml
sudo systemctl restart cloudflared

502 Bad Gateway on your hostname

The tunnel is connected but can't reach the local service. Confirm the service is listening on the expected port:

ss -tlnp | grep 8080
# Should show the process listening on 0.0.0.0:8080 or 127.0.0.1:8080

Certificate errors on the origin

If your local service uses a self-signed cert and cloudflared rejects it, add an originRequest block to that ingress rule:

ingress:
  - hostname: app.example.com
    service: https://localhost:8443
    originRequest:
      noTLSVerify: true

DNS record not resolving

Run cloudflared tunnel route dns again and check the Cloudflare DNS dashboard. The CNAME must be proxied (orange cloud icon), not DNS-only, or the tunnel address will be visible.

tested on:Ubuntu 24.04Debian 12Fedora 40Arch rolling

Frequently asked questions

Do I need to open any ports on my router or firewall?
No. cloudflared makes outbound connections only, on TCP 443 or UDP 7844 (QUIC). You need zero inbound port-forwards or firewall holes.
Does my domain have to be on a paid Cloudflare plan?
No. The free Cloudflare plan supports tunnels for personal and homelab use. Paid plans unlock higher request limits, Argo Smart Routing, and advanced Access policies.
Can I expose non-HTTP services like SSH or a game server?
SSH works natively via cloudflared access ssh or browser-rendered terminals in the Zero Trust dashboard. Raw TCP services are possible but require the cloudflared client on the connecting machine, not just a browser.
What happens to the tunnel if my server reboots?
When installed as a systemd service with systemctl enable, cloudflared starts automatically at boot and reconnects. The DNS CNAME is permanent until you delete it.
Is it safe to run multiple services through one tunnel?
Yes. A single named tunnel supports as many ingress rules as you need, each routing a different hostname to a different local port. There is no meaningful performance penalty for this.

Related guides