Cloudflare Tunnel: Expose Home Lab Services Without Port Forwarding
You've got services running in your homelab — Nextcloud, Jellyfin, Gitea, a dashboard. They work fine on your local network, but accessing them from outside means port forwarding, dynamic DNS, and hoping your ISP doesn't block inbound connections. If you're behind CGNAT, it might not work at all.
Cloudflare Tunnel solves this by creating an outbound connection from your server to Cloudflare's network. No inbound ports needed. No port forwarding. No exposing your home IP address. Your services get a public URL with HTTPS, DDoS protection, and optional authentication — all through a single daemon running on your server.
How Cloudflare Tunnel Works
The cloudflared daemon runs on your server and establishes an outbound connection to Cloudflare's edge network. Since the connection is outbound, it works through NAT, firewalls, and CGNAT without any router configuration.
When someone visits nextcloud.yourdomain.com, the request hits Cloudflare's edge, routes through the tunnel to your server, and cloudflared proxies it to the local service. The response goes back the same way.
Your home IP address is never exposed. DNS records point to Cloudflare, not to your home. Even if someone looks up your domain's IP, they'll see Cloudflare's addresses.
The tunnel maintains persistent connections to multiple Cloudflare data centers for redundancy. If one connection drops, traffic routes through another. It reconnects automatically after network interruptions.
Prerequisites
You need:
- A domain managed by Cloudflare DNS. You can use a free domain registrar and point nameservers to Cloudflare, or register directly through Cloudflare.
- A Cloudflare account. The free plan is sufficient for tunnels.
- A Linux server in your homelab running the services you want to expose.
Installing cloudflared
Debian/Ubuntu
# Add Cloudflare's GPG key and repository
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg > /dev/null
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
Fedora/RHEL
sudo dnf install -y https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-x86_64.rpm
Docker
docker pull cloudflare/cloudflared:latest
Verify Installation
cloudflared --version
# cloudflared version 2025.x.x
Creating a Tunnel
Authenticate with Cloudflare
cloudflared tunnel login
This opens a browser window where you log in to your Cloudflare account and authorize cloudflared to manage tunnels for a specific zone (domain). A certificate is saved to ~/.cloudflared/cert.pem.
Create the Tunnel
cloudflared tunnel create homelab
This creates a tunnel named "homelab" and generates a credentials file at ~/.cloudflared/<TUNNEL_ID>.json. Note the tunnel ID — you'll need it for the config.
# List your tunnels
cloudflared tunnel list
Configure the Tunnel
Create ~/.cloudflared/config.yml:
tunnel: <YOUR_TUNNEL_ID>
credentials-file: /home/youruser/.cloudflared/<YOUR_TUNNEL_ID>.json
ingress:
# Nextcloud
- hostname: cloud.yourdomain.com
service: http://localhost:8080
# Jellyfin
- hostname: media.yourdomain.com
service: http://localhost:8096
# Gitea
- hostname: git.yourdomain.com
service: http://localhost:3000
# Home Assistant
- hostname: ha.yourdomain.com
service: http://localhost:8123
# Catch-all (required — handles unmatched requests)
- service: http_status:404
The ingress section maps public hostnames to local services. Each hostname becomes a subdomain of your domain that routes to the specified local port.
The catch-all rule at the end is required. It tells cloudflared what to do with requests that don't match any hostname.
Create DNS Records
# Create CNAME records for each hostname
cloudflared tunnel route dns homelab cloud.yourdomain.com
cloudflared tunnel route dns homelab media.yourdomain.com
cloudflared tunnel route dns homelab git.yourdomain.com
cloudflared tunnel route dns homelab ha.yourdomain.com
This creates CNAME records in Cloudflare DNS pointing each subdomain to your tunnel. You can also create these manually in the Cloudflare dashboard — they're CNAME records pointing to <TUNNEL_ID>.cfargotunnel.com.
Start the Tunnel
# Test it
cloudflared tunnel run homelab
# Once it works, install as a system service
sudo cloudflared service install
sudo systemctl enable cloudflared
sudo systemctl start cloudflared
Now visit https://cloud.yourdomain.com from anywhere — it routes through Cloudflare to your local Nextcloud instance. HTTPS is handled automatically by Cloudflare; you don't need to set up certificates.
Running cloudflared in Docker
If you prefer Docker:
# docker-compose.yml
services:
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
restart: unless-stopped
command: tunnel run homelab
volumes:
- ./cloudflared:/etc/cloudflared
network_mode: host # Needed to reach services on localhost
Place your config.yml and credentials JSON in the ./cloudflared directory. Using network_mode: host lets cloudflared access services running on localhost ports. Alternatively, point the ingress services at the Docker host's IP instead of localhost.
Cloudflare Access: Adding Authentication
Exposing services to the internet means anyone can reach them. Cloudflare Access adds an authentication layer in front of your tunnel, so only authorized users can access your services.
This is the real power of Cloudflare Tunnel for homelabs. You get enterprise-grade zero-trust authentication in front of your self-hosted services, for free.
Set Up an Access Policy
Go to the Cloudflare Zero Trust dashboard (one.dash.cloudflare.com) and navigate to Access > Applications.
Click Add an application and choose Self-hosted.
Configure the application:
- Application name: Nextcloud
- Session duration: 24 hours
- Application domain:
cloud.yourdomain.com
Then add a policy:
- Policy name: Allow me
- Action: Allow
- Include rule: Emails —
your-email@example.com
Now, when someone visits cloud.yourdomain.com, Cloudflare presents a login page before the request ever reaches your server. Only your email address gets through.
Authentication Methods
Cloudflare Access supports several identity providers:
- One-time PIN (email): Cloudflare sends a code to your email. No setup needed — good for personal use.
- Google/GitHub/Microsoft: OAuth login through your existing accounts.
- SAML/OIDC: Connect any enterprise identity provider.
For a personal homelab, the email one-time PIN is the simplest. Add your email to the allow list, and Cloudflare sends you a verification code whenever you access a service. No passwords to manage.
Bypass for Specific Services
Some services (like Jellyfin) have their own authentication and you might want them publicly accessible. You can create a policy with Action: Bypass for specific applications:
Policy: Public access
Action: Bypass
Include: Everyone
Or set different authentication levels per subdomain — strict access for admin tools, public access for your media server.
Service Tokens for APIs
If you have services that need machine-to-machine communication through the tunnel (webhooks, API calls), create a Service Token in Access:
- Go to Access > Service Auth > Service Tokens
- Create a token
- Use the
CF-Access-Client-IdandCF-Access-Client-Secretheaders in API requests
curl -H "CF-Access-Client-Id: <CLIENT_ID>" \
-H "CF-Access-Client-Secret: <CLIENT_SECRET>" \
https://api.yourdomain.com/webhook
Advanced Configuration
WebSocket Support
Services like Home Assistant, Nextcloud Talk, and code-server use WebSockets. Cloudflare Tunnel supports them natively — no extra configuration needed. Just point the ingress to the service and WebSockets work.
Origin Server Configuration
For services running with self-signed certificates or on HTTPS locally:
ingress:
- hostname: secure.yourdomain.com
service: https://localhost:8443
originRequest:
noTLSVerify: true # Skip certificate verification for self-signed certs
Load Balancing Across Multiple Servers
If you run the same service on multiple machines, you can point the same hostname at different origins by running cloudflared on each server with the same tunnel config. Cloudflare will load balance across the connections.
Private Network Access
Beyond HTTP services, Cloudflare Tunnel can route entire IP ranges through the tunnel using WARP (Cloudflare's VPN client):
cloudflared tunnel route ip add 192.168.1.0/24 homelab
Install the WARP client on your devices, and you can access your entire home LAN as if you were on-site. This is similar to what Tailscale provides, but through Cloudflare's network.
Cloudflare Tunnel vs Port Forwarding vs VPN
| Feature | Cloudflare Tunnel | Port Forwarding | VPN (WireGuard/Tailscale) |
|---|---|---|---|
| Inbound ports needed | None | Yes | One (WireGuard) or none (Tailscale) |
| Works through CGNAT | Yes | No | Tailscale yes, WireGuard no |
| Home IP exposed | No | Yes | WireGuard yes, Tailscale no |
| HTTPS/SSL | Automatic | Manual (Let's Encrypt) | N/A (private network) |
| DDoS protection | Yes | No | N/A |
| Authentication layer | Cloudflare Access | None (app-level only) | VPN auth |
| Public-facing services | Yes | Yes | No (private access only) |
| Latency | Slight increase (Cloudflare edge hop) | Direct | Slight increase (tunnel overhead) |
| Dependency | Cloudflare | Your ISP/router | Tailscale servers or self-hosted |
| Cost | Free | Free | Free |
When to Use Cloudflare Tunnel
- You want to expose services publicly (share Jellyfin with friends, host a blog, run a public Gitea instance)
- You're behind CGNAT and can't port forward
- You want automatic HTTPS without managing certificates
- You want authentication in front of services that don't have good built-in auth
- You don't want your home IP address exposed
When to Use a VPN Instead
- You only need private access (you and your family)
- You want to access your entire LAN, not just specific services
- You don't want any third-party dependency in the critical path
- You need access to non-HTTP services (SSH, RDP, SMB, game servers with UDP)
- Latency matters (gaming, real-time audio/video)
When to Use Both
Many homelabbers run Cloudflare Tunnel for public-facing services and Tailscale for private access. This is a solid architecture:
- Tunnel: Jellyfin (shared with friends), Gitea (public repos), blog
- Tailscale: Proxmox web UI, SSH, NAS, router admin, internal dashboards
Limitations and Gotchas
HTTP/HTTPS only (mostly): Cloudflare Tunnel is primarily designed for HTTP traffic. TCP and UDP tunneling exist but require WARP on the client side. If you need to expose SSH, game servers, or other non-HTTP services to the public, port forwarding or a VPN is simpler.
Cloudflare's Terms of Service: Cloudflare's free plan TOS historically prohibited using their network as a CDN for serving large amounts of non-HTML content (video streaming). They relaxed this significantly, but if you're streaming terabytes of video through Jellyfin, be aware of the terms. In practice, personal homelab usage is fine, but don't point a popular public video streaming site through a free Cloudflare tunnel.
Added latency: Every request routes through Cloudflare's edge. For services you use from home (same network), this adds unnecessary latency. Use local DNS or split-horizon DNS to access services directly when you're on your home network, and through the tunnel when you're away.
Single point of failure: If Cloudflare has an outage, your tunneled services are unreachable. Cloudflare's uptime is excellent, but it's a dependency you should be aware of. Keep a VPN as backup for critical services.
Upload speed: Your home internet upload bandwidth is still the bottleneck. Cloudflare Tunnel doesn't make your connection faster — it just makes it easier to reach from the outside.
Monitoring Your Tunnel
Check tunnel status from the command line:
# See active connections
cloudflared tunnel info homelab
# Check the Cloudflare dashboard for metrics
# Zero Trust Dashboard > Networks > Tunnels
The Zero Trust dashboard shows request counts, response times, and error rates per service. Set up alerts for tunnel disconnections.
You can also monitor locally:
# cloudflared exposes a metrics endpoint
# Add to your config.yml:
# metrics: 0.0.0.0:60123
# Then scrape with Prometheus or check manually:
curl http://localhost:60123/metrics
Quick Start Summary
For those who just want the commands:
# Install
sudo apt install cloudflared # or dnf, or Docker
# Authenticate
cloudflared tunnel login
# Create tunnel
cloudflared tunnel create homelab
# Create config (~/.cloudflared/config.yml)
# Map hostnames to local services
# Create DNS records
cloudflared tunnel route dns homelab app.yourdomain.com
# Run as service
sudo cloudflared service install
sudo systemctl enable --now cloudflared
# Add Cloudflare Access policies in the Zero Trust dashboard
Cloudflare Tunnel is one of those tools that feels too good to be free. You get secure, authenticated, DDoS-protected access to your homelab services with no inbound ports, no certificate management, and no exposed home IP. The main trade-off is depending on Cloudflare — but for most homelabbers, that's a trade-off worth making.