Skip to main content

Cloudflare Tunnel

Learning Focus

By the end of this lesson you will understand how Cloudflare Tunnel works, how to set it up with cloudflared, and how to securely expose internal services.

What Is Cloudflare Tunnel?

Cloudflare Tunnel creates a secure, outbound-only connection from your server to Cloudflare's network. This means you can expose web services (like a self-hosted app or home server) to the internet without opening any inbound ports on your firewall.

flowchart LR
INTERNET["Internet\n(Visitors)"] --> CF["Cloudflare Edge\n(Security + CDN)"]
CF <-->|"Outbound-only tunnel\n(no open ports)"| TUNNEL["cloudflared\n(Your Server)"]
TUNNEL --> APP["Internal App\n(localhost:3000)"]

style CF fill:#f6821f,color:#fff,stroke:#e5711e
style TUNNEL fill:#2563eb,color:#fff,stroke:#1e40af
style APP fill:#16a34a,color:#fff,stroke:#15803d

Why Cloudflare Tunnel?

FeatureTraditional Port ForwardingCloudflare Tunnel
Open ports✅ Required (80, 443, etc.)❌ No open ports
Public IP required✅ Yes❌ No — works behind NAT/CGNAT
DDoS protection❌ None unless separate service✅ Cloudflare DDoS built-in
SSL certificateYou manage (Let's Encrypt, etc.)✅ Auto — Cloudflare handles SSL
Firewall bypassNAT/CGNAT can block you✅ Works through any NAT
CostPort forwarding is free, but exposes your IP✅ Free
Key Benefit

Cloudflare Tunnel is the safest way to expose self-hosted services. Your server's IP is never revealed, no ports are open, and all traffic is filtered through Cloudflare's security stack.

How Tunnel Works

  1. You run cloudflared (a lightweight daemon) on your server
  2. cloudflared creates an outbound connection to Cloudflare's edge (no inbound ports needed)
  3. You configure a DNS record (e.g., app.example.com) to point to the tunnel
  4. Visitors access app.example.com → Cloudflare → Tunnel → Your internal app
sequenceDiagram
participant User as Visitor
participant CF as Cloudflare Edge
participant CD as cloudflared (Your Server)
participant App as Internal App (localhost:3000)

CD->>CF: Establish outbound tunnel (persistent)
User->>CF: HTTPS request to app.example.com
CF->>CD: Forward request through tunnel
CD->>App: Forward to localhost:3000
App-->>CD: Response
CD-->>CF: Response through tunnel
CF-->>User: HTTPS response

Setting Up a Tunnel

Method 1: Dashboard Setup (Easiest)

  1. Go to Cloudflare Dashboard → Zero Trust → Networks → Tunnels
  2. Click "Create a tunnel"
  3. Name your tunnel (e.g., homelab)
  4. Install cloudflared on your server using the provided command
  5. Configure a public hostname:
    • Subdomain: app
    • Domain: example.com
    • Service: http://localhost:3000
  6. Save — your tunnel is live at app.example.com

Method 2: CLI Setup

Install cloudflared
# Debian/Ubuntu
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb \
-o cloudflared.deb
sudo dpkg -i cloudflared.deb

# Or via package manager
sudo apt install cloudflared
Authenticate and create tunnel
# Authenticate with your Cloudflare account
cloudflared tunnel login

# Create a tunnel
cloudflared tunnel create homelab

# This creates a credentials file at:
# ~/.cloudflared/<tunnel-id>.json
~/.cloudflared/config.yml
tunnel: <tunnel-id>
credentials-file: /home/user/.cloudflared/<tunnel-id>.json

ingress:
# Route app.example.com to your local application
- hostname: app.example.com
service: http://localhost:3000

# Route grafana.example.com to Grafana
- hostname: grafana.example.com
service: http://localhost:3001

# Catch-all (required)
- service: http_status:404
Create DNS records and run
# Create DNS record for the tunnel
cloudflared tunnel route dns homelab app.example.com
cloudflared tunnel route dns homelab grafana.example.com

# Run the tunnel
cloudflared tunnel run homelab

Running as a System Service

Install as a systemd service
sudo cloudflared service install

# The service is now managed by systemd:
sudo systemctl status cloudflared
sudo systemctl restart cloudflared
sudo systemctl enable cloudflared

# Logs
sudo journalctl -u cloudflared -f

Multiple Services on One Tunnel

A single tunnel can expose multiple services using hostnames:

config.yml — Multiple services
tunnel: homelab
credentials-file: /home/user/.cloudflared/<tunnel-id>.json

ingress:
- hostname: app.example.com
service: http://localhost:3000

- hostname: api.example.com
service: http://localhost:8080

- hostname: grafana.example.com
service: http://localhost:3001

- hostname: ssh.example.com
service: ssh://localhost:22

- hostname: rdp.example.com
service: rdp://localhost:3389

# Catch-all (required — must be last)
- service: http_status:404

Tunnel with Docker

docker-compose.yml
services:
cloudflared:
image: cloudflare/cloudflared:latest
command: tunnel --config /etc/cloudflared/config.yml run
volumes:
- ./cloudflared:/etc/cloudflared
restart: unless-stopped
network_mode: host # or use Docker network to reach other containers

my-app:
image: nginx:alpine
ports:
- "3000:80"
tip

When running in Docker, use network_mode: host so cloudflared can reach services on localhost. Alternatively, use Docker's internal DNS names if both containers are on the same Docker network.

Combining with Cloudflare Access

Cloudflare Tunnel + Access = secure access to internal tools without a VPN:

  1. Set up a tunnel to expose your internal service (e.g., grafana.example.com)
  2. Add an Access policy to require authentication (Google, GitHub, email OTP)
  3. Only authenticated users can reach the service — everyone else is blocked

This is covered in detail in the Access lesson.

Common Misconceptions

"Cloudflare Tunnel requires a public IP"

Reality: Tunnel makes outbound connections only. It works behind NAT, CGNAT, double NAT — any network where your server can make outbound HTTPS connections.

"I need to open ports for the tunnel to work"

Reality: Zero inbound ports are needed. cloudflared initiates the connection outward to Cloudflare.

"Tunnel is only for web applications"

Reality: Tunnel supports HTTP, HTTPS, SSH, RDP, TCP, and UDP services. You can tunnel almost any protocol.

"There's a bandwidth limit on free tunnels"

Reality: Cloudflare Tunnel is free with no bandwidth limits. You can run as many tunnels and hostnames as you need.

Key Takeaways

  • Cloudflare Tunnel exposes internal services without opening any ports — completely free.
  • Works through NAT, CGNAT, and firewalls — only needs outbound HTTPS.
  • A single tunnel can serve multiple services via different hostnames.
  • Combine with Cloudflare Access for authenticated access to internal tools.
  • Your origin IP is never exposed — traffic is filtered through Cloudflare's security stack.
  • Run as a systemd service or Docker container for production reliability.

What's Next

  • Continue to Access to learn how to add authentication to your tunneled services.