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?
- Quick Temporary Tunnel
- Named Tunnel Setup
- Docker Compose Setup
- Adding Authentication
- Common Use Cases
- Troubleshooting
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:
- Open a browser (or provide a URL to visit)
- Ask you to log into Cloudflare
- Select which domain to authorize
- 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
Docker Compose Setup (Recommended)
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
-
Go to Cloudflare Dashboard → Zero Trust → Access → Applications
-
Click Add an Application → Self-hosted
-
Configure:
- Application name: Grafana
- Session duration: 24 hours
- Application domain:
grafana.vish.gg
-
Create a Policy:
- Policy name: Allow Me
- Action: Allow
- Include:
- Emails:
your-email@gmail.com - Or Emails ending in:
@yourdomain.com
- Emails:
-
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
-
Check service is running locally:
curl http://localhost:8080 -
Check Docker networking:
- If using container names, ensure same Docker network
- If using localhost, use
--network hostor host IP
-
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
- Always use HTTPS - Cloudflare handles this automatically
- Enable Cloudflare Access for sensitive services
- Use service tokens for automated/API access
- Monitor tunnel logs for suspicious activity
- Rotate credentials periodically
- Limit ingress rules to only what's needed
Related Documentation
Last Updated: 2026-01-29