Skip to content
Go back

How to securely deploy Cloudflare Tunnels

· Updated:
By KingPin 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. 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 the Cloudflare dashboard (https://dash.cloudflare.com)
  2. Go to Networking → Tunnels
  3. Click Create a tunnel
  4. Give it a descriptive name (e.g., homelab-tunnel or dev-server)
  5. Click Create Tunnel
  6. 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:

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
networks:
- cloudflare
networks:
cloudflare:
external: true

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

  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:

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.

  1. Go to Zero Trust → Access controls → Applications
  2. Click Create new application
  3. Select Self-hosted and private
  4. Click Add public hostname and pick the domain and subdomain you routed in Step 5
  5. Under Access policies, create a policy with a rule:
  1. 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.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
Understanding and Optimizing Performance in Proxmox VE

Discussion

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

Related Posts