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.
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 accesson 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.
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
Build a Mesh VPN with Nebula
Build a fully self-hosted mesh VPN with Nebula: create a CA, sign node certs, configure lighthouses, enforce group-based firewall rules, and run as a systemd service.
Common Linux Network Ports Reference
Learn Linux port ranges, read /etc/services, find what's listening with ss and nmap, and apply solid firewall rules to expose or block the right ports.
How to Configure a Static IP on Linux
Configure a static IP on Linux using Netplan, NetworkManager (nmcli), or systemd-networkd across Ubuntu, Fedora, Debian, and Arch with verified steps.
firewalld Zones and Rich Rules in Practice
Assign interfaces to firewalld zones, open services, write rich rules for source-based and rate-limited policies, and manage runtime vs permanent config.