# 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/<TUNNEL_UUID>.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 `<TUNNEL_UUID>.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/<UUID>.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 <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

```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: <UUID>.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*
