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. Here’s the thing: 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 Cloudflare Zero Trust (https://dash.teams.cloudflare.com)
- Go to Networks → Tunnels
- Click Create a tunnel
- Select Cloudflared as the connector type
- Give it a descriptive name (e.g.,
homelab-tunnelordev-server) - Click Save tunnel
- Copy the tunnel token — 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 ports: - "80:80" - "443:443" networks: - cloudflare
networks: cloudflare: external: trueNow Caddy (or your service) is reachable by the tunnel, but not directly from the internet. This is the isolation you want.
Step 5: Configure routes in the dashboard
Back to the Cloudflare dashboard. You’re still in that Tunnels page from Step 1.
Click Configure and add a public hostname:
- 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
Click Save.
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)
Here’s 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 Access → Applications
- Click Add an application
- Select Self-hosted
- Fill in the app name and subdomain
- Click Next
- Under Policies, add a rule:
- Action:
Allow - Rule type:
Emails(orEmail domainif you want all @company.com) - Value: Your email (or domain)
- Action:
- Click Save
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.