Skip to content

Securely Publishing Docker Services with Cloudflare Tunnel + Portainer (Zero Trust Without Opening Ports)

Published:
β€’ 4 min read

Hello! πŸ‘‹

After deploying Clawdbot securely on my Mac Mini using Cloudflare Tunnel (previous post), I decided to take the Zero Trust approach a step further: integrating the tunnel directly into my Docker stack managed with Portainer.

The goal: expose services like Portainer itself, Jellyfin, Homepage, or any web app without opening a single port on the router, without a fixed public IP, and with strong authentication via Cloudflare Access. All from just another container in my NAS/homelab.

Why This Setup in 2026?

Step 1: Create the Tunnel in Zero Trust

  1. Go to one.dash.cloudflare.com β†’ Networks β†’ Tunnels β†’ Create a tunnel
  2. Name: e.g. homelab-tunnel
  3. Connector: Cloudflared (recommended) β†’ copy the token from the command they provide (the long part after --token)

Create tunnel options in Cloudflare Zero Trust

Go to Portainer β†’ Stacks β†’ Add stack β†’ paste this (use Web editor):

version: "3.9"

services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: cloudflared
    restart: unless-stopped
    environment:
      - TUNNEL_TOKEN=eyJhIjoi...PASTE_YOUR_COMPLETE_TOKEN_HERE...
    command: tunnel run
    # If you prefer command instead of env (less secure but simpler):
    # command: tunnel --no-autoupdate run --token eyJhIjoi...TOKEN...

πŸ’‘ Tip: Store the token as an environment variable or Portainer secret for better security.

Step 3: Configure Public Hostnames

Back in the Cloudflare dashboard, add the services you want to expose:

  1. Go to your tunnel β†’ Public Hostname tab
  2. Add hostname:
    • Subdomain: e.g. portainer
    • Domain: select your domain
    • Service: http://portainer:9000 (use the container name and internal port)

Repeat for each service:

Step 4: Configure CIDR Routes (Optional - For Private Network Access)

If you want to access your entire home network (not just specific services) through WARP:

  1. Go to Networks β†’ Routes β†’ Add CIDR route
  2. Add your local network range: e.g. 192.168.1.0/24
  3. Give it a description like β€œHome LAN”

CIDR routes configuration in Cloudflare

Step 5: Connect via WARP Client

On your devices (Mac, Windows, iOS, Android):

  1. Install the Cloudflare WARP client
  2. Go to Preferences β†’ Account
  3. Log in with your Zero Trust team name

WARP client preferences showing Zero Trust connection

Now you can access:

Step 6: Secure with Access Policies

Don’t forget to add authentication! Go to Access β†’ Applications:

  1. Create an application for each hostname
  2. Add a policy:
    • Email: your personal email(s)
    • Or: Require WARP device posture
    • Or: One-time PIN via email

Example policy:

Allow if:
  - Email ends with @yourdomain.com
  OR
  - Device posture: WARP is connected

Network Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Your Homelab                         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”‚
β”‚  β”‚  Portainer  β”‚  β”‚  Jellyfin   β”‚  β”‚  Nextcloud  β”‚     β”‚
β”‚  β”‚   :9000     β”‚  β”‚   :8096     β”‚  β”‚    :80      β”‚     β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜     β”‚
β”‚         β”‚                β”‚                β”‚            β”‚
β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜            β”‚
β”‚                          β”‚                             β”‚
β”‚                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”                    β”‚
β”‚                 β”‚   cloudflared   β”‚                    β”‚
β”‚                 β”‚   (container)   β”‚                    β”‚
β”‚                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜                    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚ Outbound only (no open ports!)
                           β–Ό
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚   Cloudflare Network   β”‚
              β”‚     Zero Trust Edge    β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚                        β”‚
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚  Public Access    β”‚    β”‚   WARP Client   β”‚
    β”‚ (with CF Access)  β”‚    β”‚ (private IPs)   β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Troubleshooting

Container won’t start?

Can’t reach services?

WARP not connecting to private IPs?

Conclusion

With this setup, you get:

βœ… No open ports on your router
βœ… No public IP required
βœ… Strong authentication via Cloudflare Access
βœ… Encrypted traffic end-to-end
βœ… Centralised management via Portainer
βœ… Free for personal use

The best part? If your ISP changes your IP or you move house, everything keeps working. The tunnel is outbound-only, so your homelab finds Cloudflare, not the other way around.

Happy self-hosting! πŸ πŸ”


Edit on GitHub

πŸ‘‹ Β‘Hola! PregΓΊntame lo que quieras sobre el blog
πŸ€– AI Assistant