Cloudflare Tunnels for Self-Hosters: Expose Your Home Lab Without Opening a Single Port
Self-Hosting

Cloudflare Tunnels for Self-Hosters: Expose Your Home Lab Without Opening a Single Port

Ricardo Gil
April 27, 2026
9 min read
#Cloudflare Tunnels #Self-Hosting #Home Lab #Proxmox #Networking #Security #cloudflared

Why Port Forwarding Is a Terrible Idea in 2026

If you're still punching holes in your firewall to access self-hosted services from outside your home network, stop. Every open port is an attack surface. Your ISP may block inbound connections on port 443. You probably don't have a static IP. And if you're running Proxmox with a dozen services β€” Jellyfin, Immich, Gitea, Grafana, your own LLM endpoint β€” managing NAT rules per-service quickly becomes a maintenance nightmare.

Cloudflare Tunnels solve all of this. The cloudflared daemon on your server establishes an outbound connection to Cloudflare's edge. Traffic flows in through that tunnel β€” no inbound ports required, no static IP, full TLS from client to origin. Cloudflare becomes your reverse proxy, your DDoS absorber, and your certificate authority, all without you touching your router's port forwarding table.

I've been running this setup on my Proxmox cluster for several months. Here's exactly how it works, the real configuration you need, and the gotchas that will trip you up.

---

How Cloudflare Tunnels Actually Work

The architecture is simple but worth understanding before you write a single config file:

1. You install cloudflared on a host inside your network (a Proxmox LXC, a VM, a container). 2. cloudflared establishes persistent outbound WebSocket connections to Cloudflare's edge β€” typically 4 connections for redundancy. 3. You configure DNS records (CNAME) pointing your subdomain to Cloudflare's tunnel endpoint. 3. Incoming HTTPS requests to jellyfin.yourdomain.com hit Cloudflare, traverse the tunnel, and land at http://192.168.0.x:8096 on your internal network.

Your router sees zero inbound connections. Your origin is never directly reachable from the internet. Cloudflare terminates TLS at the edge, so you get valid certificates without running Certbot or Let's Encrypt internally (though you can add internal TLS too for defense in depth).

The free tier covers everything a home lab needs. You get unlimited tunnels, unlimited requests, and Cloudflare Access for zero-trust auth. The only requirement: your domain must be on Cloudflare DNS.

---

Prerequisites

  • A domain managed on Cloudflare (free plan works)
  • Proxmox host with one or more LXCs/VMs running services
  • A lightweight host to run cloudflared β€” I use a Debian 12 LXC with 512MB RAM and 1 CPU; it handles a dozen services without breaking a sweat
  • Hardware-wise, you don't need anything special. My entire home lab runs on a Beelink EQR6 Mini PC running Proxmox 8, and the cloudflared LXC is one of about 15 containers on it. A Beelink EQ12 or similar N100/N150 mini PC handles this load with CPU utilization under 5% at idle.

    For networking, I run my LXCs on a dedicated VLAN using a TP-Link TL-SG108E managed switch, which lets me isolate traffic between the tunnel LXC and my service LXCs. Not strictly required, but good practice.

    ---

    Step 1: Create the Tunnel in Cloudflare Dashboard

    1. Log into dash.cloudflare.com, go to Zero Trust β†’ Networks β†’ Tunnels. 2. Click Create a tunnel, choose Cloudflared as the connector type. 3. Name it something like homelab-proxmox and save. 4. Cloudflare will generate a tunnel token. Copy it β€” you'll need it in the next step.

    You can also do this via the Cloudflare API or Terraform if you want to version-control your tunnel config, but the dashboard is fine for a home lab.

    ---

    Step 2: Install cloudflared in a Proxmox LXC

    Create a Debian 12 LXC in Proxmox (unprivileged, 512MB RAM, 4GB disk is plenty). Then inside it:

    bash
    # Add Cloudflare's apt repo
    curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
    echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main' | tee /etc/apt/sources.list.d/cloudflared.list

    apt update && apt install -y cloudflared

    Authenticate and install the tunnel using the token from the dashboard

    cloudflared service install <YOUR_TUNNEL_TOKEN>

    Start and enable the service

    systemctl enable --now cloudflared

    That's it. Check the Cloudflare dashboard β€” your connector should show as Healthy within 30 seconds.

    ---

    Step 3: Configure Public Hostnames (Routes)

    This is where you map subdomains to internal services. In the Cloudflare dashboard, go to your tunnel β†’ Public Hostname tab β†’ Add a public hostname.

    Example mappings:

  • jellyfin.yourdomain.com β†’ http://192.168.0.x:8096
  • immich.yourdomain.com β†’ http://192.168.0.y:2283
  • grafana.yourdomain.com β†’ http://192.168.0.z:3000
  • proxmox.yourdomain.com β†’ https://192.168.0.x:8006 (see note below)
  • Cloudflare automatically creates the CNAME DNS record for each hostname. You don't touch DNS manually.

    The Proxmox Web UI Gotcha

    If you're exposing the Proxmox web interface, the service URL needs to be https:// (since Proxmox only serves HTTPS) and you need to enable No TLS Verify under Additional application settings β†’ TLS in the hostname config. Proxmox uses a self-signed cert by default. Alternatively, set up a proper cert on Proxmox with Let's Encrypt β€” Proxmox 8 has a built-in ACME client under Datacenter β†’ ACME.

    Also add this HTTP header override to fix WebSocket issues with the Proxmox console:

    code
    Origin-Header: https://proxmox.yourdomain.com

    Without this, the noVNC console will refuse to connect.

    ---

    Step 4: Add Zero-Trust Auth with Cloudflare Access

    Cloudflare Tunnels expose your services to the internet β€” which means you need auth in front of anything you don't want public. Cloudflare Access handles this as a free zero-trust layer, and it's excellent.

    Go to Zero Trust β†’ Access β†’ Applications β†’ Add an application β†’ Self-hosted.

  • Application domain: grafana.yourdomain.com
  • Session duration: 24 hours
  • Policy: Emails β†’ your email address
  • Now anyone hitting grafana.yourdomain.com will be challenged by Cloudflare's identity flow before the request even reaches your tunnel. You can use GitHub OAuth, Google, a one-time PIN to your email, or Cloudflare's own magic link. I use GitHub auth for anything admin-adjacent and email OTP for shared access.

    For services with their own auth (Jellyfin, Immich, Gitea), you can skip Access or layer it as an extra factor. For services with no auth at all β€” Grafana in anonymous mode, raw APIs β€” Access is non-negotiable.

    bash
    # Confirm Access is working β€” should redirect to Cloudflare auth, not your service
    curl -I https://grafana.yourdomain.com
    

    HTTP/2 302 β†’ Location: https://cloudflareaccess.com/...

    ---

    Step 5: Local Config File (Optional but Recommended)

    If you prefer managing routes as code rather than through the dashboard, cloudflared supports a local YAML config:

    yaml
    # /etc/cloudflared/config.yml
    tunnel: <your-tunnel-id>
    credentials-file: /root/.cloudflared/<tunnel-id>.json

    ingress: - hostname: jellyfin.yourdomain.com service: http://192.168.0.x:8096 - hostname: immich.yourdomain.com service: http://192.168.0.y:2283 originRequest: connectTimeout: 30s - hostname: proxmox.yourdomain.com service: https://192.168.0.x:8006 originRequest: noTLSVerify: true - hostname: grafana.yourdomain.com service: http://192.168.0.z:3000 - service: http_status:404

    The final catch-all http_status:404 is required β€” cloudflared will error on startup without it.

    Apply changes:

    bash
    cloudflared tunnel ingress validate
    systemctl restart cloudflared

    ---

    Performance and Real-World Latency

    The honest answer: Cloudflare Tunnels add latency. Connections route through Cloudflare's edge rather than directly to your IP. For LAN use, you're adding 10–40ms depending on your nearest Cloudflare PoP. For most services β€” Jellyfin, Immich, web dashboards β€” this is imperceptible. For SSH or latency-sensitive admin tasks, I still use Tailscale for direct LAN access.

    The two tools are complementary, not competing: Cloudflare Tunnels for public-facing services, Tailscale (or Headscale self-hosted) for private administrative access. I've got both running simultaneously with zero conflicts.

    On hardware, if you're running everything on a single mini PC, 8-port managed switches let you handle VLAN separation cheaply. I also recommend having a UPS β€” a cloudflared tunnel dropping because of a power blip is annoying, especially if it's in front of something important.

    ---

    Bandwidth Considerations

    Cloudflare's free tier has no bandwidth caps, but their Terms of Service prohibit using Tunnels for "large file serving" in ways that abuse the free tier. In practice, streaming Jellyfin externally is a gray area β€” many people do it, and Cloudflare doesn't actively enforce this. For a family-scale Jellyfin setup, it's fine. For a public Plex alternative with hundreds of users, you'd want Cloudflare's paid tiers or a different approach (direct IP with Caddy + Let's Encrypt, served from a VPS).

    Immich is more explicitly fine since it's typically personal use. Same for Gitea, Grafana, and similar services.

    ---

    The Hardware Running This Setup

    My current tunnel setup sits on:

  • Beelink EQR6 Mini PC β€” 6-core Ryzen 5 6600H, 32GB RAM, 500GB NVMe, running Proxmox 8 as the primary hypervisor
  • TP-Link TL-SG108E 8-Port Managed Switch β€” VLAN segmentation for service isolation
  • APC Back-UPS 600VA β€” protects against momentary outages
  • Samsung 870 EVO 2TB SATA SSD β€” bulk storage for Immich and Jellyfin libraries
  • Raspberry Pi 5 8GB β€” secondary node running Home Assistant and lower-priority services
  • If you're building from scratch, the Beelink EQ14 (Intel N150, ~$200) or the Minisforum UM790 Pro (Ryzen 9 7940HS, if you want serious compute) are my current recommendations. Both run Proxmox 8 flawlessly.

    For networking, pair with a TP-Link Archer AX55 router (Wi-Fi 6, solid OpenWRT support) and the managed switch above.

    ---

    Troubleshooting Common Issues

    Tunnel shows Degraded instead of Healthy: Check that cloudflared can reach region1.cloudflaretunnels.com and region2.cloudflaretunnels.com on port 443 and 7844. Some ISPs block QUIC (7844); cloudflared will fall back to HTTP/2 over 443 automatically, but it logs warnings.

    bash
    cloudflared tunnel info <tunnel-name>
    journalctl -u cloudflared -f

    WebSocket services not working (Proxmox console, n8n, Jupyter): Enable --no-chunked-encoding in the origin config, or add noChunkedEncoding: true in the YAML. Also ensure the service hostname config has HTTP2Origin disabled if the upstream is HTTP/1.1.

    Immich mobile app can't connect: Immich's mobile app needs the server URL to match exactly what's in Access (if enabled). Disable Access for Immich if you're using Immich's own auth β€” the app handles login itself.

    Slow first-byte times: This is usually DNS propagation. After adding a new hostname, wait 1–2 minutes. Also ensure Cloudflare proxy (orange cloud) is enabled on the CNAME records β€” gray cloud bypasses the tunnel entirely.

    ---

    Final Thoughts

    Cloudflare Tunnels are one of the best zero-cost infrastructure upgrades you can make to a home lab. You gain:

  • No exposed ports on your router
  • Automatic valid TLS on every service
  • Optional zero-trust auth via Cloudflare Access
  • DDoS protection by default
  • No dependency on a static IP
  • The tradeoff is that your traffic routes through Cloudflare, so you're trusting them with your HTTP traffic (they terminate TLS at the edge). For sensitive services, use Cloudflare Access + your own application auth, and consider end-to-end encryption at the origin too. For most self-hosted services in a personal lab, this is an entirely acceptable trust boundary.

    It doesn't replace a private mesh like Tailscale for administrative access, but it's the cleanest way to make your self-hosted services available to the outside world without the exposure risks of traditional port forwarding.

    ---

    Disclosure: This post contains affiliate links. If you purchase through these links, I may earn a small commission at no extra cost to you.

    πŸ“¬Weekly Newsletter

    Get the best home lab & AI content

    No spam. One email per week. Unsubscribe anytime.

    Share this article