Skip to content
Go back

How to securely deploy Cloudflare Tunnels

By SumGuy 5 min read
How to securely deploy Cloudflare Tunnels

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

Step 1: Create the tunnel in the dashboard

You need to set up the tunnel connection first, before installing anything locally.

  1. Log in to Cloudflare Zero Trust (https://dash.teams.cloudflare.com)
  2. Go to NetworksTunnels
  3. Click Create a tunnel
  4. Select Cloudflared as the connector type
  5. Give it a descriptive name (e.g., homelab-tunnel or dev-server)
  6. Click Save tunnel
  7. 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:

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

RHEL / Fedora:

Terminal window
curl -L --output cloudflared.rpm https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-x86_64.rpm
sudo yum localinstall -y cloudflared.rpm

macOS:

Terminal window
brew install cloudflared

Windows:

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:

Terminal window
docker network create cloudflare

Then create your docker-compose.yml:

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: true

Create a .env file in the same directory:

.env
TUNNEL_TOKEN=your_actual_token_from_step_1

Never commit .env to git — add it to .gitignore immediately.

Start the tunnel:

Terminal window
docker compose up -d
docker compose logs -f

You 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:

docker-compose.yml
services:
caddy:
image: caddy:latest
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
networks:
- cloudflare
networks:
cloudflare:
external: true

Now 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:

  1. Subdomain: blog (or whatever you want)
  2. Domain: Click the dropdown and select your domain (typing it doesn’t work—you have to click)
  3. 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:

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.

  1. Go to AccessApplications
  2. Click Add an application
  3. Select Self-hosted
  4. Fill in the app name and subdomain
  5. Click Next
  6. Under Policies, add a rule:
    • Action: Allow
    • Rule type: Emails (or Email domain if you want all @company.com)
    • Value: Your email (or domain)
  7. 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.local

Use cases

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.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
Optimizing Ansible for Faster Playbook Execution
Next Post
Uptime Monitoring with Uptime Kuma

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts