Cloudflare Tunnels: Zero-Trust Access to Your Self-Hosted Services
The traditional way to expose a self-hosted service to the internet goes like this: open a port on your router, set up dynamic DNS, configure a reverse proxy, get TLS certificates, and hope nobody finds your open port before you've locked everything down properly.
Cloudflare Tunnels flips this model. Instead of opening inbound ports, you run a small daemon (cloudflared) on your server that creates an outbound connection to Cloudflare's network. Traffic flows through Cloudflare to your service, and your home IP address is never exposed. No port forwarding, no dynamic DNS, no inbound firewall rules.
How It Works
The architecture is straightforward:
User → Cloudflare Edge → Tunnel → cloudflared (your server) → Your Service
cloudflaredruns on your server and establishes an outbound connection to Cloudflare- You configure DNS records in Cloudflare to point your domain at the tunnel
- When someone visits
jellyfin.yourdomain.com, Cloudflare routes the request through the tunnel to your server - Your server processes the request and sends the response back through the tunnel
Your router's firewall stays completely closed. No port 80, no port 443, no port forwarding rules at all. The only outbound connection is from cloudflared to Cloudflare's network.
Why This Matters for Self-Hosters
No exposed home IP
When you port-forward, anyone who resolves your domain sees your home IP address. With Cloudflare Tunnels, DNS resolves to Cloudflare's IPs. Your actual IP is hidden behind their network.
No port forwarding
Your router doesn't need any inbound rules opened. This eliminates an entire category of misconfiguration risks — you can't accidentally expose an admin panel if there are no open ports.
Automatic TLS
Cloudflare handles TLS termination at their edge. You get HTTPS with valid certificates without running Let's Encrypt or managing certificate renewals.
Works behind CGNAT
If your ISP uses Carrier-Grade NAT (common with 5G/LTE home internet and some fiber providers), you literally cannot port-forward. Cloudflare Tunnels work because the connection is outbound from your server.
Cloudflare Tunnels vs. Alternatives
| Feature | Cloudflare Tunnels | Tailscale Funnel | ngrok | Pangolin |
|---|---|---|---|---|
| Price | Free (generous) | Free (limited) | Free (limited) | Free (self-hosted) |
| Custom domains | Yes (your domain on Cloudflare) | *.ts.net subdomains |
Paid plans only | Yes |
| IP hiding | Yes | Yes | Yes | Depends on setup |
| Port forwarding needed | No | No | No | No |
| Access policies | Yes (Cloudflare Access) | Tailscale ACLs | IP restrictions (paid) | Basic auth |
| DDoS protection | Yes (Cloudflare's network) | No | No | No |
| CDN/caching | Yes | No | No | No |
| Bandwidth limits | None on free tier | Limited | 1 GB/mo free | Unlimited (self-hosted) |
| Self-hosted option | No (SaaS) | Partial (Headscale) | No | Yes |
| WebSocket support | Yes | Yes | Yes | Yes |
| TCP/UDP tunnels | Yes (paid for arbitrary TCP) | Yes | Yes (paid) | Yes |
| Setup complexity | Low-medium | Very low | Very low | Medium |
Choose Cloudflare Tunnels when
- You already use Cloudflare for DNS (or are willing to move your domain there)
- You want free custom domain access with DDoS protection
- You want access policies (who can reach what) without running your own auth
- You need to expose services publicly (not just to your own devices)
- You're behind CGNAT and can't port-forward
Choose Tailscale Funnel when
- You primarily want to share services with specific people (not the public)
- You already use Tailscale for your network
- You want the absolute simplest setup (one command)
- You don't need custom domains or don't want to use Cloudflare
Choose ngrok when
- You need temporary tunnels for development or demos
- You want a public URL for a local service in 30 seconds
- You don't want to configure DNS or Cloudflare
Choose Pangolin when
- You want a fully self-hosted tunnel solution with no third-party dependency
- You have a VPS with a public IP to run the tunnel server
- You want complete control over the infrastructure
Setting Up Cloudflare Tunnels
Prerequisites
- A domain name with DNS managed by Cloudflare (free plan works)
- A server running Docker (or ability to install
cloudflared) - A Cloudflare account
Step 1: Create a tunnel
You can create tunnels through the Cloudflare dashboard (Zero Trust > Networks > Tunnels) or via the CLI. The dashboard method is simpler for getting started:
- Go to Cloudflare Zero Trust dashboard
- Navigate to Networks > Tunnels
- Click Create a tunnel
- Name it (e.g., "homelab")
- Cloudflare generates a tunnel token
Step 2: Run cloudflared
Using Docker:
services:
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
command: tunnel run
environment:
- TUNNEL_TOKEN=your-tunnel-token-here
restart: unless-stopped
# If routing to containers on the same Docker network:
networks:
- homelab
Or install directly on the host:
# Debian/Ubuntu
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt update && sudo apt install cloudflared
# Run as a service
sudo cloudflared service install your-tunnel-token-here
On Fedora or RHEL-based systems:
sudo dnf install cloudflared
sudo cloudflared service install your-tunnel-token-here
Step 3: Configure routes
Back in the Cloudflare dashboard, add public hostnames to your tunnel. Each hostname maps to a local service:
| Public hostname | Service | URL |
|---|---|---|
| jellyfin.yourdomain.com | HTTP | http://localhost:8096 |
| grafana.yourdomain.com | HTTP | http://localhost:3000 |
| nextcloud.yourdomain.com | HTTP | http://localhost:8080 |
If cloudflared runs in Docker and your services are on a Docker network, use the container name instead of localhost:
| Public hostname | Service | URL |
|---|---|---|
| jellyfin.yourdomain.com | HTTP | http://jellyfin:8096 |
Cloudflare automatically creates the CNAME DNS records for you.
Step 4: Verify
Visit https://jellyfin.yourdomain.com — you should see your Jellyfin instance with a valid Cloudflare-issued TLS certificate. No port forwarding configured, no Let's Encrypt, no reverse proxy.
Configuration File Approach
Instead of the dashboard, you can manage tunnel config as code. Create a config.yml:
tunnel: your-tunnel-id
credentials-file: /etc/cloudflared/credentials.json
ingress:
- hostname: jellyfin.yourdomain.com
service: http://localhost:8096
- hostname: grafana.yourdomain.com
service: http://localhost:3000
- hostname: nextcloud.yourdomain.com
service: http://localhost:8080
originRequest:
noTLSVerify: true # If the origin uses self-signed certs
# Catch-all rule (required)
- service: http_status:404
services:
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
command: tunnel run
volumes:
- ./config.yml:/etc/cloudflared/config.yml
- ./credentials.json:/etc/cloudflared/credentials.json
restart: unless-stopped
The config-file approach is better for version control and reproducibility. The dashboard approach is better for quick changes and initial setup.
Note: You can use dashboard-managed tunnels (token-based) or locally-managed tunnels (config file). They work differently and can't be mixed for the same tunnel. Dashboard-managed is newer and generally recommended for simplicity.
Access Policies with Cloudflare Access
Here's where Cloudflare Tunnels get really powerful. You can put authentication in front of any tunneled service, even services that have no built-in auth:
- In Zero Trust dashboard, go to Access > Applications
- Add a new application
- Set the domain (e.g.,
grafana.yourdomain.com) - Create a policy defining who can access it
Authentication methods
Cloudflare Access supports multiple identity providers:
- One-time PIN — Cloudflare emails a code to approved email addresses (no setup needed)
- Google/GitHub/Microsoft SSO — OAuth login through major providers
- SAML — Enterprise identity providers
- Service tokens — For API/machine access
Example policy: email-based access
Application: grafana.yourdomain.com
Policy: Allow
- Include: Email ends with @yourdomain.com
- Include: Email is [email protected]
Anyone visiting grafana.yourdomain.com sees a Cloudflare login page. They enter their email, receive a one-time PIN, and get access. No accounts to create, no passwords to manage.
Example policy: GitHub org access
Application: admin-tools.yourdomain.com
Policy: Allow
- Include: GitHub Organization is "your-github-org"
Only members of your GitHub organization can access the service. This is excellent for team homelab setups or shared infrastructure.
Bypass for specific services
Some services don't work well with an auth page in front of them (APIs, webhooks, mobile apps). You can bypass Access for specific paths:
Application: nextcloud.yourdomain.com
Policy: Bypass
- Path: /remote.php/*
- Path: /ocs/*
Policy: Allow
- Include: Email is [email protected]
This lets Nextcloud's mobile app and CalDAV/CardDAV clients connect directly while still requiring auth for browser access.
Free Tier vs. Paid Features
Free (Zero Trust free plan — up to 50 users)
- Unlimited tunnels
- Unlimited bandwidth
- Custom domains (must be on Cloudflare DNS)
- HTTP/HTTPS tunnels
- Cloudflare Access with up to 50 users
- DDoS protection
- Cloudflare's CDN and caching
Paid (Zero Trust paid plans)
- More than 50 Access users
- Arbitrary TCP tunnels (SSH, RDP, databases)
- Advanced Access policies (device posture, mTLS)
- Gateway DNS filtering
- Browser isolation
- Audit logging and analytics
For most self-hosters, the free tier is more than enough. You hit the paid wall when you need TCP tunnels for non-HTTP services (like SSH access through the tunnel) or when you have more than 50 users accessing your services.
The WARP client workaround for TCP
You can access TCP services (SSH, RDP) through Cloudflare Tunnels on the free tier if users install the WARP client with your Zero Trust organization configured. WARP routes traffic through your tunnel without needing the paid TCP tunnel feature. The trade-off is that every user needs WARP installed and configured.
Practical Considerations
Latency
Your traffic goes: user -> Cloudflare edge -> tunnel -> your server -> tunnel -> Cloudflare edge -> user. This adds some latency compared to a direct connection. For web applications, this is usually unnoticeable (10-50ms added). For latency-sensitive applications like game servers or real-time video, it can matter.
Cloudflare's Terms of Service
Cloudflare's free tier TOS historically restricted serving large amounts of non-HTML content (like video streaming). This has been relaxed significantly, and many people run Jellyfin/Plex through tunnels without issues. However, it's worth being aware that Cloudflare could theoretically enforce bandwidth restrictions. If you're streaming terabytes of video monthly, consider whether a tunnel is the right approach.
WebSocket and long-lived connections
Cloudflare Tunnels support WebSockets, but there are idle timeout limits (usually 100 seconds of inactivity). Applications that rely on persistent WebSocket connections (some real-time apps, VS Code Server) may experience disconnections. You can usually work around this with keep-alive messages.
Single point of failure
Your access depends on Cloudflare's network being available. If Cloudflare has an outage (rare but it happens), all your tunneled services go offline, even for local network users accessing them through the domain. Mitigate this by also configuring local DNS or running a local reverse proxy as a fallback.
Not truly zero-trust
While "zero-trust" is the marketing term, using Cloudflare Tunnels means you're trusting Cloudflare completely. They terminate your TLS, see your unencrypted traffic, and control access to your services. For most self-hosters this is an acceptable trade-off, but it's worth understanding: you're not eliminating trust, you're placing it in Cloudflare rather than your ISP or your own infrastructure.
Common Gotchas
Docker networking
If cloudflared runs in Docker and needs to reach services on the host, localhost inside the container is the container itself, not the host. Options:
# Option 1: Use host networking
services:
cloudflared:
network_mode: host
# Option 2: Use host.docker.internal (Docker Desktop) or host gateway
services:
cloudflared:
extra_hosts:
- "host.docker.internal:host-gateway"
# Then use http://host.docker.internal:8096 in routes
# Option 3: Put services on the same Docker network (recommended)
services:
cloudflared:
networks:
- homelab
jellyfin:
networks:
- homelab
# Then use http://jellyfin:8096 in routes
Nextcloud and similar apps
Some applications check the incoming host header and reject requests that don't match their configured domain. Add your tunnel domain to Nextcloud's trusted_domains array, or any equivalent configuration in other apps.
Large file uploads
Cloudflare has a 100 MB upload limit on the free plan (300 MB on Pro). If you need to upload large files through a tunneled service (like Nextcloud), this limit will bite you. Workarounds: use the Nextcloud desktop sync client, or upgrade to a Cloudflare paid plan.
This is one of the most common surprises for self-hosters using Cloudflare Tunnels.
Multiple Tunnels vs. One Tunnel
You can route many services through a single tunnel. One cloudflared instance can serve dozens of hostnames. There's no performance reason to create multiple tunnels unless:
- Services run on different physical servers (each server needs its own
cloudflared) - You want to isolate services so taking down one tunnel doesn't affect others
- You're managing tunnels for different projects or environments
For a typical single-server homelab, one tunnel handles everything.
The Honest Trade-offs
Cloudflare Tunnels are great if:
- You want remote access without port forwarding
- You're behind CGNAT
- You want free TLS, DDoS protection, and CDN for your services
- You want easy access policies without running your own auth server
- You already use Cloudflare for DNS
Cloudflare Tunnels are not ideal if:
- You don't want a third party handling your traffic
- You need to serve large files (100 MB upload limit on free tier)
- You need low-latency connections for real-time applications
- You want a fully self-hosted, no-dependency solution (look at Pangolin or WireGuard + a VPS)
- You need TCP tunnel access on a budget (SSH, databases)
Bottom line: For most self-hosters who want to access their services remotely, Cloudflare Tunnels are the pragmatic choice. The setup is simple, the free tier is generous, and not having to open ports on your router is a genuine security improvement. Just understand that you're trusting Cloudflare with your traffic, and plan around the upload size limit if it affects your use case.