Zero-Trust Access to Your Homelab Using Cloudflare Tunnel and Docker

We earn commissions when you shop through the links on this page, at no additional cost to you. Learn more.

Opening ports on your home router to expose homelab services has always felt like a calculated gamble — you gain convenience and lose sleep. Cloudflare Tunnel changes that equation entirely: your server initiates an outbound connection to Cloudflare's edge, and Cloudflare handles all the inbound traffic routing. No open ports, no exposed IP address, no certificate headaches. When I first set this up for my Jellyfin and Vaultwarden instances, I was genuinely surprised at how little complexity was involved compared to maintaining a WireGuard setup. This tutorial walks you through deploying the cloudflared daemon as a Docker container alongside your existing services, then layering on Cloudflare Access to add identity-aware authentication — real zero-trust, not just SSL.

What You Actually Need Before Starting

The prerequisites here are minimal but non-negotiable. You need a domain name added to Cloudflare — it must be on Cloudflare's nameservers, not just using Cloudflare as a DNS proxy. A free Cloudflare account is sufficient; you do not need a paid plan for tunnels or Cloudflare Access with up to 50 users. On your homelab side, you need Docker and Docker Compose installed. I'm running this on a Debian 12 mini PC, but the setup is identical on Ubuntu, a Raspberry Pi 5, or even a VPS.

If you're also looking for a VPS to run some of your services in the cloud rather than at home, DigitalOcean is worth a look — their Droplets start cheap and the interface is clean enough that you won't spend half your day clicking through dashboards.

Step 1 — Create the Tunnel in the Cloudflare Dashboard

Log into the Cloudflare Zero Trust dashboard at one.dash.cloudflare.com. Navigate to Networks → Tunnels and click Create a tunnel. Choose Cloudflared as the connector type and give the tunnel a sensible name — I use homelab-main. On the next screen, Cloudflare will show you an installation command. Don't run that command directly; instead, copy just the tunnel token. It looks like a long Base64 string and starts with eyJ. You'll paste this into your Docker Compose file shortly.

Still in the dashboard, go to the Public Hostnames tab for your tunnel. Add a hostname entry for each service you want to expose. For example:

The service URL uses the Docker container name because cloudflared will be on the same Docker network as your other services. Cloudflare creates the DNS records automatically — no manual CNAME entries needed.

Step 2 — The Docker Compose File

Here's my working docker-compose.yml that runs cloudflared alongside a Vaultwarden instance. The key principle is that all services share the same custom bridge network so cloudflared can reach them by container name.

version: "3.8"

services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    restart: unless-stopped
    command: tunnel --no-autoupdate run
    environment:
      - TUNNEL_TOKEN=${TUNNEL_TOKEN}
    networks:
      - tunnel-net

  vaultwarden:
    image: vaultwarden/server:latest
    restart: unless-stopped
    volumes:
      - vaultwarden-data:/data
    environment:
      - WEBSOCKET_ENABLED=true
      - SIGNUPS_ALLOWED=false
    networks:
      - tunnel-net

  jellyfin:
    image: jellyfin/jellyfin:latest
    restart: unless-stopped
    volumes:
      - jellyfin-config:/config
      - /mnt/media:/media:ro
    networks:
      - tunnel-net

networks:
  tunnel-net:
    driver: bridge

volumes:
  vaultwarden-data:
  jellyfin-config:

Create a .env file in the same directory as your Compose file and put your tunnel token in there:

# .env file — never commit this to git
TUNNEL_TOKEN=eyJhIjoiY...your_full_token_here

# Bring everything up
docker compose up -d

# Verify cloudflared connected successfully
docker compose logs cloudflared

You should see a line in the logs like Connection registered connIndex=0 location=AMS (or whichever Cloudflare PoP is closest to you). The tunnel is now live. At this point, your subdomains resolve publicly and Cloudflare proxies traffic to your home server — but there's no authentication yet. Don't stop here.

Watch out: Until you configure Cloudflare Access policies in the next step, your services are publicly reachable at those subdomains. If Vaultwarden allows signups or Jellyfin has no user accounts configured, anyone can hit those URLs right now. Set up Access policies immediately after confirming the tunnel works.

Step 3 — Adding Zero-Trust Authentication with Cloudflare Access

This is where "tunnel" becomes "zero-trust." In the Cloudflare Zero Trust dashboard, go to Access → Applications and click Add an application. Choose Self-hosted. Fill in the application domain — for example, vault.yourdomain.com — and set a session duration (I use 24 hours for convenience, 1 hour if I'm being security-conscious).

On the next screen you define policies. I use one-time PIN (OTP) email authentication for personal use — Cloudflare sends a 6-digit code to your email and that's your login. No external identity provider needed. My policy looks like this:

If you have family members who need access to Jellyfin, add their email addresses to the same include rule. Cloudflare Access sits in front of every request — before anything reaches your home server, a user must authenticate via the email OTP flow. The session is maintained by a CF_Authorization JWT cookie, so they only log in once per session duration, not on every page load.

Tip: For services that have their own robust authentication (like Vaultwarden, which uses end-to-end encryption), Cloudflare Access acts as an extra layer — even if someone finds your subdomain, they need to prove identity before they see a login screen. For services with weaker or no auth (like a local Grafana instance with anonymous access enabled), Access is your primary gate.

Step 4 — Handling WebSocket and Long-Lived Connections

Some services — Vaultwarden's WebSocket sync, Jellyfin's streaming, Uptime Kuma's realtime updates — use WebSocket connections or long HTTP requests that Cloudflare can sometimes interrupt. In the Cloudflare Zero Trust dashboard, under your tunnel's public hostname settings, make sure the HTTP2 connection option is enabled and check the No TLS Verify box only if your backend service uses a self-signed cert on a non-standard port. For Vaultwarden specifically, I add a second hostname entry pointing to the /notifications/hub path to ensure WebSocket traffic routes correctly.

Jellyfin video streaming works fine in practice — I've streamed 4K HEVC content through the tunnel without issues. The bottleneck is your home upload bandwidth, not Cloudflare's routing. If you want to move heavy workloads off your home connection entirely, running a Jellyfin transcoding node on a DigitalOcean Droplet with Block Storage for your media library is a workable hybrid approach.

Step 5 — Keeping the Tunnel Container Updated

The cloudflare/cloudflared:latest image updates frequently. I use Watchtower in a monitoring-only mode to alert me when new images are available, but update manually rather than automatically — a tunnel outage during working hours is inconvenient. If you prefer automatic updates, add Watchtower to your Compose file:

  watchtower:
    image: containrrr/watchtower:latest
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    command: --interval 86400 --cleanup cloudflared
    networks:
      - tunnel-net

The --interval 86400 flag checks once per day. Omit the cloudflared argument at the end if you want Watchtower to update all containers, not just the tunnel daemon.

What This Setup Doesn't Replace

Cloudflare Tunnel is not a VPN. You can't route arbitrary TCP/UDP traffic through it using this configuration — only HTTP and HTTPS services work with public hostnames. If you need SSH access to your homelab or need to reach non-HTTP services remotely, pair this with Tailscale (which I run simultaneously on the same machine with zero conflicts). Tailscale handles my SSH and database access; Cloudflare Tunnel handles anything with a web UI.

Also worth noting: your traffic does pass through Cloudflare's infrastructure. For most homelab services this is a reasonable trade-off — you get DDOS protection, TLS termination, and identity-gating for free. For a self-hosted password manager, some people prefer end-to-end encrypted traffic that Cloudflare cannot inspect; Vaultwarden's encryption model means Cloudflare sees ciphertext regardless, but if that architecture concerns you, WireGuard with a VPS exit node is the more privacy-preserving alternative.

Wrapping Up

In about 20 minutes you can go from "I have Docker services running locally" to "those services are securely accessible from anywhere, gated by identity verification, with no open firewall ports." That's a genuinely excellent security posture for a homelab. My next step after getting this running was adding Uptime Kuma behind the same tunnel to monitor all my services from outside my network — which exposed a few Docker restart loops I hadn't noticed. Start with one non-critical service to verify the tunnel works, then migrate your others. Once you've confirmed the setup, check out our guide on self-hosting Authentik for SSO if you want to replace Cloudflare Access's email OTP with a richer identity provider that you fully control.

Discussion

```