Cloudflare Tunnels: Securely Expose Your Homelab Services Without Opening a Single Port

Cloudflare Tunnels: Securely Expose Your Homelab Services Without Opening a Single Port

If you’ve been running a homelab, you’ve probably hit the same wall I did: you want to access your self-hosted services from outside your house, but you don’t want to open ports on your router, deal with dynamic IP addresses, or pay for a static IP. Cloudflare Tunnels (formerly Argo Tunnel) solves all three problems at once — and it’s free for personal use.

In this guide, I’ll walk you through exactly how to set up cloudflared to securely expose your homelab services — Nextcloud, Grafana, Home Assistant, whatever you’re running — without touching your firewall, buying a VPN subscription, or fighting with Let’s Encrypt renewals ever again.

What Is a Cloudflare Tunnel?

A Cloudflare Tunnel creates an outbound-only encrypted connection from your server to Cloudflare’s edge network. Instead of the internet connecting to your server, your server’s cloudflared daemon connects out to Cloudflare and keeps that connection persistent. When someone visits your domain, their request goes to Cloudflare’s edge, travels down that tunnel, and hits your local service — all without a single inbound port open on your router.

This fundamentally flips the traditional port-forwarding model:

  • No port 80/443 open on your router
  • No static IP needed — cloudflared reconnects automatically if your IP changes
  • Free TLS certificates, managed and auto-renewed by Cloudflare
  • Built-in DDoS protection from Cloudflare’s global anycast network
  • Optional Zero Trust access policies — require a Google, GitHub, or email login before anyone reaches your service

Compare this to something like Tailscale, which is a peer-to-peer VPN — both are excellent tools, but they solve different problems. Tailscale is perfect for personal access between your own devices. Cloudflare Tunnels shine when you want to share a service with others or access it from any browser, anywhere, without installing any client software. You could, for example, share a Recipes app or a read-only dashboard with family members using just a URL.

Prerequisites

  • A domain name managed by Cloudflare — even a cheap one from Namecheap or Porkbun works; just point the nameservers to Cloudflare’s free DNS
  • A free Cloudflare account (no credit card required)
  • A Linux server, VM, or Raspberry Pi running your services
  • Docker installed (optional but recommended — see our Docker beginner’s guide if you need to get started)

You do not need a Cloudflare Pro plan. The free tier is fully featured for homelab use, including Zero Trust Access.

Step 1: Install cloudflared

You can install cloudflared directly on the host or run it as a Docker container. I strongly prefer Docker — it keeps the daemon contained, makes updates a one-liner, and integrates naturally with whatever else you’re running.

Option A: Native Install (Debian/Ubuntu)

# Download the latest .deb for amd64
curl -L --output cloudflared.deb \
  https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared.deb

# Verify
cloudflared --version

For ARM (Raspberry Pi 4/5):

curl -L --output cloudflared.deb \
  https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64.deb
sudo dpkg -i cloudflared.deb

Option B: Docker (Recommended)

docker pull cloudflare/cloudflared:latest

We’ll wire up the Docker Compose configuration once we have a tunnel token from the dashboard.

Step 2: Create the Tunnel in Cloudflare Dashboard

Log into your Cloudflare dashboard and navigate to Zero Trust → Networks → Tunnels. If it’s your first time, you’ll be prompted to set up a Zero Trust account name — this is free and doesn’t require billing info.

Click Add a tunnel, choose Cloudflared, and give it a descriptive name (something like homelab, proxmox-host, or raspberrypi). Cloudflare will generate a tunnel token — copy it. This token is the only credential your server needs; it replaces the older certificate-based authentication flow.

You’ll see installation snippets for various platforms. We’ll use the Docker approach.

Step 3: Run cloudflared with Docker Compose

Create a directory and a docker-compose.yml:

mkdir -p /opt/cloudflared
cat > /opt/cloudflared/docker-compose.yml << 'EOF'
services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: cloudflared
    restart: unless-stopped
    command: tunnel --no-autoupdate run
    environment:
      - TUNNEL_TOKEN=your_token_here
    networks:
      - proxy

networks:
  proxy:
    external: true
EOF

I'm using an external Docker network called proxy — the same network my other containers (Nginx Proxy Manager, Nextcloud, Grafana) live on. This lets cloudflared reach them by container name rather than by IP address, which is more reliable since container IPs can change on restart.

# Create the network if it doesn't exist yet
docker network create proxy

# Start cloudflared
cd /opt/cloudflared
docker compose up -d

# Tail the logs to confirm a clean connection
docker logs cloudflared -f

A successful startup looks like this:

2026-09-17T10:00:00Z INF Starting tunnel tunnelID=abc123def456
2026-09-17T10:00:01Z INF Registered tunnel connection connIndex=0 location=SJC
2026-09-17T10:00:01Z INF Registered tunnel connection connIndex=1 location=LAX

Notice two connections — cloudflared maintains redundant connections to different Cloudflare PoPs for high availability. Back in the dashboard, your tunnel status should flip to HEALTHY within about 10 seconds.

Step 4: Configure Public Hostnames

This is where the real magic happens. In the tunnel config screen, click Public HostnamesAdd a public hostname. You'll configure:

  • Subdomain: e.g., nextcloud
  • Domain: your domain, e.g., example.com
  • Service Type: HTTP or HTTPS
  • Service URL: where cloudflared should forward traffic, e.g., http://nextcloud:80

If your service is on the same Docker network, use the container name. If it's running natively on the host, use http://localhost:PORT. If it's on a different machine on your LAN, use the local IP: http://192.168.1.50:8096.

Common homelab examples:

Public Hostname Service URL What It Exposes
nextcloud.example.com http://nextcloud:80 Nextcloud file sync
grafana.example.com http://grafana:3000 Monitoring dashboards
ha.example.com http://homeassistant:8123 Home Assistant
status.example.com http://uptime-kuma:3001 Uptime monitoring
jellyfin.example.com http://192.168.1.55:8096 Media server (different host)

Cloudflare automatically creates the DNS CNAME records pointing to your tunnel and provisions a TLS certificate from its own CA. Within about 30 seconds, https://nextcloud.example.com is live, with a valid cert, no port forwarding, and no ACME challenge dance.

Step 5: Lock It Down with Access Policies (Don't Skip This)

Putting services on the public internet without authentication is asking for trouble — especially something like a Nextcloud instance, a router admin panel, or a Proxmox dashboard. Cloudflare Access lets you bolt on an authentication layer in front of any tunnel endpoint, entirely separate from the application's own login.

Navigate to Zero Trust → Access → ApplicationsAdd an applicationSelf-hosted.

Set the application domain to match your public hostname (e.g., grafana.example.com), then configure an access policy. Your options include:

  • Email OTP: Cloudflare emails a one-time code to whitelisted addresses — zero infrastructure required
  • Google or GitHub login: Allow specific accounts or entire domains (e.g., *@yourcompany.com)
  • One-time PIN: Simpler version of OTP, useful for sharing with non-technical family members
  • Service tokens: Machine-to-machine access for scripts or APIs that need to call your service programmatically

For a personal homelab, Email OTP restricted to your own address is the simplest secure option. Even if someone discovers the URL, they'll hit a Cloudflare-hosted login page and get no further without access to your inbox. This authentication happens at Cloudflare's edge — your local service never sees the unauthenticated request at all.

Handling HTTPS Services Locally

Some services (Proxmox, pfSense, Synology DSM) only serve HTTPS with self-signed certificates. Pointing cloudflared at https://proxmox:8006 will fail TLS validation because the cert isn't signed by a public CA.

The fix is to configure noTLSVerify in a local config file. This tells cloudflared to skip cert validation on the origin side (your server) while still presenting a valid Cloudflare cert to the client side (the browser):

tunnel: your-tunnel-id-here
credentials-file: /etc/cloudflared/credentials.json

ingress:
  - hostname: proxmox.example.com
    service: https://proxmox:8006
    originRequest:
      noTLSVerify: true
  - hostname: synology.example.com
    service: https://192.168.1.100:5001
    originRequest:
      noTLSVerify: true
  - service: http_status:404

The final http_status:404 catch-all rule is required — cloudflared rejects configs without it.

Update your Docker Compose to mount the config:

services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: cloudflared
    restart: unless-stopped
    command: tunnel --no-autoupdate run --config /etc/cloudflared/config.yml
    volumes:
      - ./config.yml:/etc/cloudflared/config.yml:ro
      - ./credentials.json:/etc/cloudflared/credentials.json:ro

Get the credentials file by authenticating with the CLI:

cloudflared tunnel login
# Opens a browser — log in and authorize
cloudflared tunnel create homelab
# Outputs your tunnel ID and writes ~/.cloudflared/<tunnel-id>.json

Running Multiple Tunnels vs. One Tunnel with Multiple Hostnames

A common architectural question: should you run one cloudflared daemon exposing multiple services, or one per service? For most homelabs, one tunnel with multiple public hostnames is the right answer. It uses minimal RAM (cloudflared sits at about 30-50 MB), and each hostname gets its own Access policy configured independently.

The exception: if you have services spread across multiple physical machines — say a dedicated NAS running TrueNAS and a separate Proxmox box — run a separate cloudflared container on each machine. They can all share the same logical tunnel in the dashboard (just run the same token on both), or be separate named tunnels. Either way, Cloudflare routes traffic to whichever instance is currently connected.

Troubleshooting Common Issues

Tunnel shows DEGRADED or UNHEALTHY in the dashboard

docker logs cloudflared --tail 100 | grep -E "ERR|WARN|failed"

The most common cause is outbound connectivity. Cloudflared needs HTTPS to *.argotunnel.com and *.cloudflare.com on port 443. If your homelab firewall has a restrictive outbound policy, add those as allowed destinations. Also check for DNS resolution — cloudflared needs to resolve Cloudflare hostnames, so make sure it's not pointing to a broken resolver.

502 Bad Gateway when visiting your hostname

Cloudflared connected to Cloudflare's edge successfully, but couldn't reach your local service. Debug checklist:

  • Is the target container on the same Docker network? Run docker network inspect proxy and look for both containers in the list.
  • Is the service actually running? docker ps | grep <container-name>
  • Did you use the right port? Try curl -v http://<container-name>:PORT from inside the cloudflared container: docker exec -it cloudflared wget -qO- http://nextcloud:80

WebSocket connections dropping (Home Assistant, Jupyter, etc.)

Some apps depend heavily on WebSockets. In the public hostname settings, scroll down to Additional Application Settings and enable HTTP/2 Origin Connection and ensure WebSocket proxying isn't disabled. If you're using config.yml, add:

originRequest:
  connectTimeout: 30s
  noHappyEyeballs: false

Container restarts with buffer size warnings

docker inspect cloudflared --format '{{.State.ExitCode}}'

Exit code 0 or 1 with "failed to sufficiently increase receive buffer size" is a harmless Linux kernel message — cloudflared works fine despite it. Token errors (exit code 1, "tunnel not found" or "authentication failed") mean the token was revoked; regenerate it in the Cloudflare dashboard and update your environment variable.

Performance Considerations

Since traffic routes through Cloudflare's edge before reaching your server, there's an extra network hop. In practice, Cloudflare's anycast network is so well-distributed that the added latency is typically 5-20ms — imperceptible for web apps. For most homelab services — file managers, dashboards, password managers, home automation — you won't notice the difference at all.

Where you will notice it: high-throughput transfers or latency-sensitive protocols. If you're mirroring a 500 GB backup through a tunnel, it's slower than a direct LAN connection. The Cloudflare free tier also caps tunnel throughput in practice around 100 Mbps, which is plenty for typical homelab use but won't saturate a 2.5G NIC. For that kind of workload, a direct peer-to-peer VPN is a better fit.

A Real Homelab Setup

Here's how my current configuration looks — one cloudflared container on my Proxmox host, single tunnel, five public hostnames:

homelab.example.com    → http://nginx-proxy-manager:81   (NPM admin UI)
cloud.example.com      → http://nextcloud:80              (file sync)
monitor.example.com    → http://grafana:3000              (dashboards)
ha.example.com         → http://homeassistant:8123        (home automation)
status.example.com     → http://uptime-kuma:3001          (uptime tracking)

All five hostnames are protected by Cloudflare Access with Email OTP. If you're not yet using Uptime Kuma to monitor your services, it pairs extremely well with this setup — check out our guide on using Uptime Kuma to monitor your homelab.

The router has zero inbound ports forwarded. The entire cloudflared Docker Compose is about 20 lines. When my ISP rotates my dynamic IP, nothing breaks — cloudflared reconnects within seconds. The whole thing has been running for months with no intervention required.

What Cloudflare Tunnels Won't Do

Being clear about limitations saves headaches later:

  • Non-HTTP protocols: Tunnels handle HTTP, HTTPS, and SSH (via cloudflared access ssh). Raw TCP or UDP (game servers, RTSP streams, MQTT) need a different approach — Cloudflare's TCP tunnel feature exists but requires WARP on the client side.
  • Privacy from Cloudflare: Cloudflare terminates TLS at their edge, meaning they technically see plaintext traffic in transit. For a homelab running dashboards and file sync this is generally fine, but worth knowing if you're handling genuinely sensitive data.
  • Mass video delivery: Cloudflare's Terms of Service prohibit using the free tier to serve large volumes of video content (think a Plex server with 50 external users). Personal use is fine; running a CDN substitute is not.
  • Latency-sensitive real-time apps: Game servers, SIP/VoIP, and similar protocols will suffer from the extra hop. Use direct port forwarding or a peer-to-peer VPN for those.

Wrapping Up

Cloudflare Tunnels are one of those tools that genuinely change how you think about homelab networking. Once you've set it up, the old approach — dynamic DNS client, Let's Encrypt certbot cron job, port 443 forwarded on your router, hoping your ISP doesn't block inbound connections — starts to feel like a lot of unnecessary friction.

The combination of zero inbound ports, automatic TLS, and Cloudflare Access authentication gives you a security posture that's genuinely better than most port-forwarding setups, and you get it entirely for free. If you're already self-hosting services with Docker and haven't tried Cloudflare Tunnels yet, this weekend is the perfect time to start.

Got questions or running into a specific error? Drop a comment below — I respond to all of them.

Enjoying this post?

Get more guides like this delivered straight to your inbox. No spam, just tech and trails.