# 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?](#what-is-cloudflared) - [Quick Temporary Tunnel](#quick-temporary-tunnel-no-account-needed) - [Named Tunnel Setup](#named-tunnel-setup) - [Docker Compose Setup](#docker-compose-setup-recommended) - [Adding Authentication](#adding-authentication-cloudflare-access) - [Common Use Cases](#common-use-cases) - [Troubleshooting](#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) ```bash # 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 ```bash # 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 ```bash # 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 ```bash 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 ```bash # 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/.json` ### Step 4: Create a Config File Create `~/.cloudflared/config.yml`: ```yaml # 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: ```bash # 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 `.cfargotunnel.com` ### Step 6: Run the Tunnel ```bash # 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) ```bash # 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/.json ``` ### docker-compose.yml ```yaml 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) ```yaml 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 ```bash 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 Dashboard** → **Zero Trust** → **Access** → **Applications** 2. Click **Add an Application** → **Self-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 ```bash # 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 ```yaml # In config.yml ingress: - hostname: ssh.vish.gg service: ssh://localhost:22 ``` Client connects via: ```bash # 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 ```yaml ingress: - hostname: rdp.vish.gg service: rdp://localhost:3389 - hostname: vnc.vish.gg service: tcp://localhost:5900 ``` ### Multiple Services Example ```yaml 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 ```bash # Authentication cloudflared tunnel login # Authenticate with Cloudflare cloudflared tunnel logout # Remove authentication # Tunnel Management cloudflared tunnel list # List all tunnels cloudflared tunnel info # Get tunnel details cloudflared tunnel create # Create new tunnel cloudflared tunnel delete # Delete tunnel (must stop first) # DNS Routes cloudflared tunnel route dns # Create DNS route cloudflared tunnel route dns list # List all routes # Running Tunnels cloudflared tunnel run # Run tunnel cloudflared tunnel --config config.yml run # Run with config cloudflared tunnel ingress validate # Validate config # Debugging cloudflared tunnel --loglevel debug run # Debug logging cloudflared tunnel info # Tunnel info ``` --- ## Troubleshooting ### Tunnel won't start ```bash # Check config syntax cloudflared tunnel ingress validate # Run with debug logging cloudflared tunnel --loglevel debug run homelab ``` ### DNS not resolving ```bash # Verify DNS route exists cloudflared tunnel route dns list # Check CNAME in Cloudflare dashboard # Should point to: .cfargotunnel.com ``` ### Service unreachable 1. **Check service is running locally:** ```bash 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 ```bash # 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 --- ## Related Documentation - [Cloudflare Tunnel Docs](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/) - [Cloudflare Access Docs](https://developers.cloudflare.com/cloudflare-one/policies/access/) - [Zero Trust Dashboard](https://one.dash.cloudflare.com/) --- *Last Updated: 2026-01-29*