Your home server’s got a service you want to share with the world, but opening ports feels like leaving your front door wide open. You don’t have to. Cloudflare Tunnels let you expose anything, dev server, self-hosted app, media box, without a single inbound firewall rule. The tunnel does the heavy lifting, and you sleep better at 2 AM knowing your attack surface is basically zero.
What’s a Cloudflare Tunnel?
A tunnel is an outbound-only connection from your machine to Cloudflare’s edge network. Instead of listening for incoming traffic on your public IP (which screams “exploit me”), your local service quietly reaches out to Cloudflare and says “route traffic through me.” Cloudflare handles the public-facing part, you keep your ports locked down. It’s like having a security guard in front of your door instead of leaving it wide open.
Prerequisites
- A Cloudflare account with a domain already pointed at Cloudflare’s nameservers
- A server or machine where your service runs (Linux, macOS, Windows, doesn’t matter)
- Basic comfort with a terminal
Step 1: Create the tunnel in the dashboard
You need to set up the tunnel connection first, before installing anything locally.
- Log in to the Cloudflare dashboard (https://dash.cloudflare.com)
- Go to Networking → Tunnels
- Click Create a tunnel
- Give it a descriptive name (e.g.,
homelab-tunnelordev-server) - Click Create Tunnel
- Pick your operating system (or Docker) and copy the tunnel token from the install command: you’ll need this in a second
Don’t close this window yet. You’ll come back here in a moment.
Step 2: Install cloudflared
The cloudflared daemon creates and maintains your tunnel connection.
Ubuntu / Debian:
curl -L --output cloudflared.deb https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.debsudo dpkg -i cloudflared.debRHEL / Fedora:
curl -L --output cloudflared.rpm https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-x86_64.rpmsudo yum localinstall -y cloudflared.rpmmacOS:
brew install cloudflaredWindows:
Download the installer from GitHub (choose 64-bit or 32-bit .msi), run it, and follow the prompts.
Step 3: Deploy via Docker Compose
This is the cleanest approach if you’re already running containers. Create a Docker network first, then deploy cloudflared.
First, create the isolated network:
docker network create cloudflareThen create your docker-compose.yml:
services: cloudflared: image: cloudflare/cloudflared:latest container_name: cloudflared restart: unless-stopped command: tunnel run environment: - TUNNEL_TOKEN=${TUNNEL_TOKEN} networks: - cloudflare
networks: cloudflare: external: trueCreate a .env file in the same directory:
TUNNEL_TOKEN=your_actual_token_from_step_1Never commit
.envto git, add it to.gitignoreimmediately.
Start the tunnel:
docker compose up -ddocker compose logs -fYou should see “Connected to Cloudflare” in the logs. Good sign.
Step 4: Add services to the Cloudflare network
Here’s where the security magic happens. Only containers connected to the cloudflare network are visible to the tunnel. Everything else stays hidden.
Take your existing docker-compose.yml (Caddy, your app, whatever) and add it to that network:
services: caddy: image: caddy:latest container_name: caddy restart: unless-stopped networks: - cloudflare
networks: cloudflare: external: trueNow Caddy (or your service) is reachable by the tunnel, but not directly from the internet. Note there is no ports: mapping on purpose: publishing 80 and 443 on the host would reopen the inbound ports the tunnel exists to avoid. This is the isolation you want.
Step 5: Configure routes in the dashboard
Back to the Cloudflare dashboard. Go to Networking → Tunnels and select your tunnel. On the Routes tab, click Add route, then choose Published application and fill in:
- Subdomain:
blog(or whatever you want) - Domain: Click the dropdown and select your domain (typing it doesn’t work, you have to click)
- Service: Select
http://caddy:80(or the IP and port of your service)
For a server at 10.10.10.222:21004, you’d enter:
- Service type:
HTTP - URL:
http://10.10.10.222:21004
Save the route.
Your subdomain is now routed through the tunnel. Test it by visiting https://blog.example.com in a browser.
Step 6: Lock it down with Cloudflare Access (Zero Trust)
This is the part that makes this actually secure, not just “no open ports.” Without this, anyone who knows your subdomain can hit your service. You want to gate-keep it.
- Go to Zero Trust → Access controls → Applications
- Click Create new application
- Select Self-hosted and private
- Click Add public hostname and pick the domain and subdomain you routed in Step 5
- Under Access policies, create a policy with a rule:
- Action:
Allow - Rule type:
Emails(orEmail domainif you want all @company.com) - Value: Your email (or domain)
- Save the application
Now only you (or your approved users) can access the service. Everyone else gets a login page. This is the “secure” part the title promised.
Want something lighter? Use a one-time PIN instead, anyone you share the link with gets emailed a code to verify. In the Access policy, set Action: Allow, Selector: Emails, and list the addresses you want to let in. No password manager required, no user accounts to manage.
Keep your token safe
Never commit TUNNEL_TOKEN to git. Use .env files. Even better, rotate the token in the Cloudflare dashboard if it ever leaks. Your .gitignore should have:
.env.env.localUse cases
- Dev server showcase: Share your localhost with a client without deploying to staging
- Home NAS from the road: Access your Nextcloud, Jellyfin, or file server securely from anywhere
- Self-hosted apps: Caddy, Uptime Kuma, wiki.js, whatever, all hidden behind Cloudflare Access
Wrap-up
Cloudflare Tunnels trade one outbound connection for zero inbound risk. Combine that with Access policies and you’ve got a zero-trust setup that makes sense for a home lab. No open ports, no complicated VPN config, no 2 AM regrets.