Files
homelab-optimized/docs/infrastructure/cloudflare-tunnels.md
Gitea Mirror Bot a542ff9df0
Some checks failed
Documentation / Deploy to GitHub Pages (push) Has been cancelled
Documentation / Build Docusaurus (push) Has been cancelled
Sanitized mirror from private repository - 2026-03-24 12:49:25 UTC
2026-03-24 12:49:25 +00:00

13 KiB

Cloudflare Tunnels Guide

Last Updated: 2026-01-29

This guide covers how to use Cloudflare Tunnels (cloudflared) to expose local services to the internet securely, without opening ports on your router.

Table of Contents


What is Cloudflared?

Cloudflared is Cloudflare's tunnel client that creates a secure, encrypted connection between your local machine and Cloudflare's edge network. It allows you to expose local services to the internet without opening ports on your router or having a public IP.

How It Works

Your Local Service → cloudflared → Cloudflare Edge → Public URL → Visitor's Browser
     (port 8080)      (outbound)     (proxy/CDN)     (your domain)

Key insight: cloudflared makes an OUTBOUND connection to Cloudflare, so you don't need to configure any firewall rules or port forwarding.

Benefits

  • No port forwarding required
  • DDoS protection via Cloudflare
  • Free SSL certificates
  • Optional authentication (Cloudflare Access)
  • Works behind CGNAT
  • Multiple services on one tunnel

Quick Temporary Tunnel (No Account Needed)

This is the fastest way to share something temporarily. No Cloudflare account required.

Option 1: Using Docker (Easiest)

# Expose a local service running on port 8080
docker run --rm -it --network host cloudflare/cloudflared:latest tunnel --url http://localhost:8080

# Examples for specific services:
# Jellyfin
docker run --rm -it --network host cloudflare/cloudflared:latest tunnel --url http://localhost:8096

# Grafana
docker run --rm -it --network host cloudflare/cloudflared:latest tunnel --url http://localhost:3000

# Any web service
docker run --rm -it --network host cloudflare/cloudflared:latest tunnel --url http://localhost:PORT

Option 2: Install cloudflared Directly

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

# On macOS
brew install cloudflared

# On Windows (PowerShell)
winget install Cloudflare.cloudflared

# Then run:
cloudflared tunnel --url http://localhost:8080

What You'll See

INF Thank you for trying Cloudflare Tunnel...
INF Your quick Tunnel has been created! Visit it at:
INF https://random-words-here.trycloudflare.com

Share that URL with your friend! When done, press Ctrl+C to close the tunnel.

Quick Tunnel Limitations

  • URL changes every time you restart
  • No authentication
  • No uptime guarantee
  • Single service per tunnel

Named Tunnel Setup

Named tunnels give you a permanent, custom URL on your own domain with optional authentication.

Prerequisites

  • Cloudflare account (free tier works)
  • Domain on Cloudflare DNS (e.g., vish.gg, thevish.io)
  • cloudflared installed

Step 1: Install cloudflared

# For Synology/Debian/Ubuntu:
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared
chmod +x /usr/local/bin/cloudflared

# Verify installation
cloudflared --version

Step 2: Authenticate with Cloudflare

cloudflared tunnel login

This will:

  1. Open a browser (or provide a URL to visit)
  2. Ask you to log into Cloudflare
  3. Select which domain to authorize
  4. Save a certificate to ~/.cloudflared/cert.pem

Step 3: Create a Named Tunnel

# Create a tunnel named "homelab"
cloudflared tunnel create homelab

Output:

Created tunnel homelab with id a1b2c3d4-e5f6-7890-abcd-ef1234567890

Save that UUID! It's your tunnel's unique identifier.

This also creates a credentials file at: ~/.cloudflared/<TUNNEL_UUID>.json

Step 4: Create a Config File

Create ~/.cloudflared/config.yml:

# Tunnel UUID (from step 3)
tunnel: a1b2c3d4-e5f6-7890-abcd-ef1234567890
credentials-file: /root/.cloudflared/a1b2c3d4-e5f6-7890-abcd-ef1234567890.json

# Route traffic to local services
ingress:
  # Jellyfin at jellyfin.vish.gg
  - hostname: jellyfin.vish.gg
    service: http://localhost:8096
  
  # Paperless at docs.vish.gg
  - hostname: docs.vish.gg
    service: http://localhost:8000
  
  # Grafana at grafana.vish.gg
  - hostname: grafana.vish.gg
    service: http://localhost:3000
  
  # SSH access at ssh.vish.gg
  - hostname: ssh.vish.gg
    service: ssh://localhost:22

  # Catch-all (required) - returns 404 for unmatched hostnames
  - service: http_status:404

Step 5: Create DNS Routes

For each hostname, create a DNS record pointing to your tunnel:

# Automatically create CNAME records
cloudflared tunnel route dns homelab jellyfin.vish.gg
cloudflared tunnel route dns homelab docs.vish.gg
cloudflared tunnel route dns homelab grafana.vish.gg
cloudflared tunnel route dns homelab ssh.vish.gg

This creates CNAME records pointing to <TUNNEL_UUID>.cfargotunnel.com

Step 6: Run the Tunnel

# Test it first
cloudflared tunnel run homelab

# Or run with specific config file
cloudflared tunnel --config ~/.cloudflared/config.yml run homelab

Step 7: Run as a Service (Persistent)

# Install as a systemd service
sudo cloudflared service install

# Start and enable
sudo systemctl start cloudflared
sudo systemctl enable cloudflared

# Check status
sudo systemctl status cloudflared

# View logs
sudo journalctl -u cloudflared -f

For homelab use, running cloudflared as a Docker container is recommended.

Directory Structure

cloudflared/
├── docker-compose.yml
├── config.yml
└── credentials.json  # Copy from ~/.cloudflared/<UUID>.json

docker-compose.yml

version: "3.9"
services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: cloudflared
    restart: unless-stopped
    command: tunnel --config /etc/cloudflared/config.yml run
    volumes:
      - ./config.yml:/etc/cloudflared/config.yml:ro
      - ./credentials.json:/etc/cloudflared/credentials.json:ro
    networks:
      - homelab

networks:
  homelab:
    external: true

config.yml (Docker version)

tunnel: a1b2c3d4-e5f6-7890-abcd-ef1234567890
credentials-file: /etc/cloudflared/credentials.json

ingress:
  # Use container names when on same Docker network
  - hostname: jellyfin.vish.gg
    service: http://jellyfin:8096
  
  - hostname: paperless.vish.gg
    service: http://paperless-ngx:8000
  
  - hostname: grafana.vish.gg
    service: http://grafana:3000
  
  # For services on the host network, use host IP
  - hostname: portainer.vish.gg
    service: http://192.168.0.200:9000
    
  # Catch-all (required)
  - service: http_status:404

Deploy

cd cloudflared
docker-compose up -d

# Check logs
docker logs -f cloudflared

Adding Authentication (Cloudflare Access)

Protect services with Cloudflare Access (free for up to 50 users).

Setup via Dashboard

  1. Go to Cloudflare DashboardZero TrustAccessApplications

  2. Click Add an ApplicationSelf-hosted

  3. Configure:

    • Application name: Grafana
    • Session duration: 24 hours
    • Application domain: grafana.vish.gg
  4. Create a Policy:

    • Policy name: Allow Me
    • Action: Allow
    • Include:
      • Emails: your-email@gmail.com
      • Or Emails ending in: @yourdomain.com
  5. Save the application

How It Works

Friend visits grafana.vish.gg
    → Cloudflare Access login page
    → Enters email
    → Receives one-time PIN via email
    → Enters PIN
    → Authenticated → Sees Grafana

Authentication Options

Method Description
One-time PIN Email-based OTP (default)
Google/GitHub/etc. OAuth integration
SAML/OIDC Enterprise SSO
Service Token For API/automated access
mTLS Certificate-based

Common Use Cases

Share Jellyfin for Movie Night

# Quick tunnel (temporary)
docker run --rm -it --network host cloudflare/cloudflared:latest tunnel --url http://localhost:8096

# Named tunnel (permanent)
# Add to config.yml:
#   - hostname: watch.vish.gg
#     service: http://localhost:8096

Expose SSH Access

# In config.yml
ingress:
  - hostname: ssh.vish.gg
    service: ssh://localhost:22

Client connects via:

# Install cloudflared on client
cloudflared access ssh --hostname ssh.vish.gg

Or configure SSH config (~/.ssh/config):

Host ssh.vish.gg
    ProxyCommand cloudflared access ssh --hostname %h

Expose RDP/VNC

ingress:
  - hostname: rdp.vish.gg
    service: rdp://localhost:3389
  
  - hostname: vnc.vish.gg
    service: tcp://localhost:5900

Multiple Services Example

tunnel: your-tunnel-uuid
credentials-file: /etc/cloudflared/credentials.json

ingress:
  # Media
  - hostname: jellyfin.vish.gg
    service: http://jellyfin:8096
  - hostname: plex.vish.gg
    service: http://plex:32400
  
  # Productivity
  - hostname: paperless.vish.gg
    service: http://paperless:8000
  - hostname: wiki.vish.gg
    service: http://dokuwiki:80
  
  # Development
  - hostname: git.vish.gg
    service: http://gitea:3000
  - hostname: code.vish.gg
    service: http://code-server:8080
  
  # Monitoring
  - hostname: grafana.vish.gg
    service: http://grafana:3000
  - hostname: uptime.vish.gg
    service: http://uptime-kuma:3001
  
  # Catch-all
  - service: http_status:404

Reference Commands

# Authentication
cloudflared tunnel login          # Authenticate with Cloudflare
cloudflared tunnel logout         # Remove authentication

# Tunnel Management
cloudflared tunnel list           # List all tunnels
cloudflared tunnel info <name>    # Get tunnel details
cloudflared tunnel create <name>  # Create new tunnel
cloudflared tunnel delete <name>  # Delete tunnel (must stop first)

# DNS Routes
cloudflared tunnel route dns <tunnel> <hostname>  # Create DNS route
cloudflared tunnel route dns list                 # List all routes

# Running Tunnels
cloudflared tunnel run <name>                     # Run tunnel
cloudflared tunnel --config config.yml run        # Run with config
cloudflared tunnel ingress validate               # Validate config

# Debugging
cloudflared tunnel --loglevel debug run <name>    # Debug logging
cloudflared tunnel info <name>                    # Tunnel info

Troubleshooting

Tunnel won't start

# Check config syntax
cloudflared tunnel ingress validate

# Run with debug logging
cloudflared tunnel --loglevel debug run homelab

DNS not resolving

# Verify DNS route exists
cloudflared tunnel route dns list

# Check CNAME in Cloudflare dashboard
# Should point to: <UUID>.cfargotunnel.com

Service unreachable

  1. Check service is running locally:

    curl http://localhost:8080
    
  2. Check Docker networking:

    • If using container names, ensure same Docker network
    • If using localhost, use --network host or host IP
  3. Check ingress rules order:

    • More specific rules should come before catch-all
    • Catch-all (http_status:404) must be last

Certificate errors

# Re-authenticate
cloudflared tunnel login

# Check cert exists
ls -la ~/.cloudflared/cert.pem

View tunnel metrics

Cloudflare provides metrics at:

  • Dashboard → Zero Trust → Tunnels → Select tunnel → Metrics

Quick vs Named Tunnel Comparison

Feature Quick Tunnel Named Tunnel
URL random.trycloudflare.com app.yourdomain.com
Cloudflare Account Not needed Required
Persistence Dies with process Permanent
Custom domain No Yes
Multiple services One per tunnel Many via ingress
Authentication None Cloudflare Access
Setup time 10 seconds 10 minutes
Best for Quick demos Production

Security Best Practices

  1. Always use HTTPS - Cloudflare handles this automatically
  2. Enable Cloudflare Access for sensitive services
  3. Use service tokens for automated/API access
  4. Monitor tunnel logs for suspicious activity
  5. Rotate credentials periodically
  6. Limit ingress rules to only what's needed


Last Updated: 2026-01-29