docker.recipes
Networking9 min read

Cloudflare Tunnels: Zero-Trust Remote Access

Expose your self-hosted services securely without port forwarding using Cloudflare Tunnels (cloudflared).

01Why Cloudflare Tunnels?

Traditional self-hosting requires opening ports on your router and exposing your home IP address. Cloudflare Tunnels flip this model: your server connects outbound to Cloudflare, and they route traffic to your services. **Benefits:** • No port forwarding needed • Your home IP stays hidden • Free SSL certificates • DDoS protection included • Access policies and authentication • Works behind CGNAT **How it works:** 1. cloudflared runs on your server 2. It establishes an outbound connection to Cloudflare 3. Traffic to your domain routes through this tunnel 4. Your firewall can block all inbound connections

Cloudflare Tunnels are free for unlimited bandwidth. You only need a Cloudflare account and a domain using their DNS.

02Creating and Configuring a Tunnel

First, create a tunnel in the Cloudflare Zero Trust dashboard (one.dash.cloudflare.com). You'll get a tunnel token that cloudflared uses to authenticate. **Steps:** 1. Go to Zero Trust → Networks → Tunnels 2. Click 'Create a tunnel' 3. Name your tunnel (e.g., 'homelab') 4. Copy the tunnel token 5. Deploy cloudflared with Docker
1# docker-compose.yml
2services:
3 cloudflared:
4 image: cloudflare/cloudflared:latest
5 container_name: cloudflared
6 restart: unless-stopped
7 command: tunnel run
8 environment:
9 - TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN}
10 networks:
11 - proxy
12
13networks:
14 proxy:
15 external: true

Never commit your tunnel token to version control. Use environment variables or Docker secrets.

03Exposing Services Through the Tunnel

Configure which services to expose in the Cloudflare dashboard under your tunnel's 'Public Hostname' settings. Each hostname maps to an internal service. **Example mappings:** • jellyfin.yourdomain.com → http://jellyfin:8096 • nextcloud.yourdomain.com → http://nextcloud:80 • homeassistant.yourdomain.com → http://homeassistant:8123 Cloudflared reaches services by their Docker network name, so ensure all services share a network with cloudflared.
1# Full example with multiple services
2services:
3 cloudflared:
4 image: cloudflare/cloudflared:latest
5 restart: unless-stopped
6 command: tunnel run
7 environment:
8 - TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN}
9 networks:
10 - proxy
11
12 jellyfin:
13 image: jellyfin/jellyfin:latest
14 volumes:
15 - ./config:/config
16 - /media:/media:ro
17 networks:
18 - proxy
19 # No ports exposed - only accessible via tunnel!
20
21 vaultwarden:
22 image: vaultwarden/server:latest
23 volumes:
24 - ./vaultwarden:/data
25 networks:
26 - proxy
27
28networks:
29 proxy:
30 name: proxy

Services don't need to expose ports to the host when using tunnels. This reduces your attack surface significantly.

04Config File Method (Alternative)

Instead of configuring routes in the dashboard, you can use a local config file. This is useful for version-controlling your tunnel configuration. **When to use config files:** • You want infrastructure-as-code • You need complex routing rules • You're managing multiple environments Create a config.yml and mount it into cloudflared:
1# cloudflare-config.yml
2tunnel: your-tunnel-id
3credentials-file: /etc/cloudflared/creds.json
4
5ingress:
6 - hostname: jellyfin.yourdomain.com
7 service: http://jellyfin:8096
8 - hostname: nextcloud.yourdomain.com
9 service: http://nextcloud:80
10 originRequest:
11 noTLSVerify: true
12 - hostname: ssh.yourdomain.com
13 service: ssh://localhost:22
14 # Catch-all rule (required)
15 - service: http_status:404

05Adding Authentication with Access Policies

Cloudflare Access lets you add authentication to any service—even those without built-in auth. You can require email verification, SSO, or hardware keys. **Setting up Access:** 1. Zero Trust → Access → Applications 2. Add an application (Self-hosted) 3. Set the domain (e.g., admin.yourdomain.com) 4. Configure identity providers (email OTP, Google, GitHub) 5. Create policies (allow specific emails/domains) **Policy examples:** • Allow only @yourcompany.com emails • Require both email AND country match • Allow specific email addresses
1# Test that access is working
2curl -I https://protected.yourdomain.com
3# Should redirect to Cloudflare Access login
4
5# Bypass for specific IPs (set in dashboard)
6# Useful for local network access

Use Access policies to protect admin interfaces, databases, and internal tools even if they have their own authentication.

06WebSockets and Streaming Services

Some services need special configuration for WebSockets or long-running connections. Cloudflare Tunnels support these but you may need to adjust settings. **Services that need WebSocket support:** • Home Assistant • Vaultwarden (notifications) • Code Server • Jupyter notebooks WebSockets work by default, but streaming services like Jellyfin may need HTTP/2 origin connections disabled:
1# config.yml for streaming-heavy services
2tunnel: your-tunnel-id
3credentials-file: /etc/cloudflared/creds.json
4
5ingress:
6 - hostname: jellyfin.yourdomain.com
7 service: http://jellyfin:8096
8 originRequest:
9 # Disable HTTP/2 for better streaming compatibility
10 http2Origin: false
11 # Increase timeouts for long streams
12 connectTimeout: 30s
13 noHappyEyeballs: true
14 - service: http_status:404

Large file transfers (like Nextcloud uploads) may timeout. Increase connectTimeout and consider using direct connections for bulk transfers.

07Monitoring Tunnel Health

Monitor your tunnel status to ensure services remain accessible. Cloudflare provides metrics, and you can also check locally. **Monitoring approaches:** • Cloudflare dashboard shows tunnel status • Use cloudflared metrics endpoint • Set up uptime monitoring (Uptime Kuma) • Configure alerts for tunnel disconnections
1services:
2 cloudflared:
3 image: cloudflare/cloudflared:latest
4 restart: unless-stopped
5 command: tunnel --metrics 0.0.0.0:2000 run
6 environment:
7 - TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN}
8 networks:
9 - proxy
10
11 # Monitor the tunnel
12 uptime-kuma:
13 image: louislam/uptime-kuma:1
14 volumes:
15 - ./uptime-kuma:/app/data
16 networks:
17 - proxy
18 # Add monitors for:
19 # - cloudflared:2000/metrics (internal)
20 # - Your public hostnames (external)

Run two cloudflared replicas on different machines for high availability. Cloudflare load balances between them automatically.