DEV Community

Cover image for Cloudflare Tunnel Advanced Guide: Docker, Zero Trust and HA Setup
Rahul Ravindran
Rahul Ravindran

Posted on • Originally published at rahulr.cc on

Cloudflare Tunnel Advanced Guide: Docker, Zero Trust and HA Setup

In Part 1, we built a Cloudflare Tunnel capable of exposing multiple local services through a single secure connection.

We successfully created:

app.example.com
api.example.com
grafana.example.com

Enter fullscreen mode Exit fullscreen mode

without opening a single port on our router.

That's already a significant improvement over traditional port forwarding. However, most developers and homelab operators quickly discover that exposing a few applications is only the beginning.

Questions start appearing:

  • How do I expose Docker containers?

  • Can I secure services behind authentication?

  • What happens if my server reboots?

  • How do I scale beyond a few applications?

  • Can multiple machines share a tunnel?

  • How do production teams manage this?

This article answers those questions and takes Cloudflare Tunnel from a useful tool to a complete infrastructure component.


Why Most Tunnel Setups Eventually Become Difficult to Manage

Let's look at a realistic self-hosted environment.

Many developers start with:

Frontend
Backend API
Database Admin Tool

Enter fullscreen mode Exit fullscreen mode

A few months later:

Frontend
Backend API
Grafana
Prometheus
Portainer
Jenkins
Home Assistant
Gitea
MinIO
Uptime Kuma

Enter fullscreen mode Exit fullscreen mode

Now you're managing:

  • Multiple ports

  • Multiple containers

  • Multiple services

  • Different authentication requirements

  • Internal-only applications

  • Public-facing applications

Without structure, your configuration becomes difficult to maintain.

The goal is not simply exposing services.

The goal is building an architecture that scales.


Designing a Multi-Service Gateway

A common mistake is assigning random names.

For example:

app1.example.com
app2.example.com
app3.example.com

Enter fullscreen mode Exit fullscreen mode

This quickly becomes confusing.

Instead, use service-oriented naming.

api.example.com
grafana.example.com
portainer.example.com
git.example.com
status.example.com
storage.example.com

Enter fullscreen mode Exit fullscreen mode

Anyone can immediately understand the purpose of each endpoint.

For development environments:

dev-api.example.com
dev-web.example.com
staging-api.example.com
staging-web.example.com

Enter fullscreen mode Exit fullscreen mode

This naming convention scales much better.


Advanced Ingress Routing

Part 1 introduced hostname routing. Cloudflared also supports more advanced patterns.

Basic hostname rule:

ingress:

  - hostname: api.example.com
    service: http://localhost:8080

  - service: http_status:404

Enter fullscreen mode Exit fullscreen mode

Simple. But what if multiple applications share the same hostname?


Path-Based Routing

Suppose you want:

example.com/api
example.com/admin
example.com/dashboard

Enter fullscreen mode Exit fullscreen mode

instead of separate subdomains.

Configuration:

ingress:

  - hostname: example.com
    path: /api/*
    service: http://localhost:8080

  - hostname: example.com
    path: /admin/*
    service: http://localhost:5000

  - hostname: example.com
    path: /dashboard/*
    service: http://localhost:3001

  - service: http_status:404

Enter fullscreen mode Exit fullscreen mode

Requests are routed based on both hostname and URL path. This can simplify DNS management when dozens of services exist.


Wildcard Routing

Sometimes development environments change frequently. Instead of creating many DNS entries:

feature1.example.com
feature2.example.com
feature3.example.com

Enter fullscreen mode Exit fullscreen mode

use a wildcard.

DNS:

*.example.com

Enter fullscreen mode Exit fullscreen mode

Configuration:

ingress:

  - hostname: "*.example.com"
    service: http://localhost:3000

  - service: http_status:404

Enter fullscreen mode Exit fullscreen mode

Useful for preview deployments and temporary environments.


Running Cloudflared as a System Service

Production deployments should never rely on manually running:

cloudflared tunnel run homelab

Enter fullscreen mode Exit fullscreen mode

Use systemd instead.

Install service:

sudo cloudflared service install

Enter fullscreen mode Exit fullscreen mode

Enable automatic startup:

sudo systemctl enable cloudflared

Enter fullscreen mode Exit fullscreen mode

Start immediately:

sudo systemctl start cloudflared

Enter fullscreen mode Exit fullscreen mode

Verify:

sudo systemctl status cloudflared

Enter fullscreen mode Exit fullscreen mode

Example output:

Active: active (running)

Enter fullscreen mode Exit fullscreen mode

Now the tunnel automatically recovers after:

  • Reboots

  • Crashes

  • Power failures

  • Maintenance windows

This is essential in production.


Docker Integration

Most modern self-hosted services run inside containers. Cloudflared works exceptionally well with Docker.

Consider this stack:

React Frontend
ExpressJS Backend
Grafana
Prometheus

Enter fullscreen mode Exit fullscreen mode

All running in containers.


Running Cloudflared Inside Docker

Create a dedicated network.

docker network create cloudflare

Enter fullscreen mode Exit fullscreen mode

Create docker-compose.yml:

version: "3.9"

services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    command: tunnel --config /etc/cloudflared/config.yml run
    volumes:
      - ./cloudflared:/etc/cloudflared
    restart: unless-stopped
    networks:
      - cloudflare

networks:
  cloudflare:
    external: true

Enter fullscreen mode Exit fullscreen mode

Start:

docker compose up -d

Enter fullscreen mode Exit fullscreen mode

Cloudflared now runs entirely inside Docker.


Routing to Containers Directly

Instead of localhost:

service: http://localhost:3000

Enter fullscreen mode Exit fullscreen mode

use container names.

Example:

ingress:

  - hostname: app.example.com
    service: http://frontend:3000

  - hostname: api.example.com
    service: http://backend:8080

  - hostname: grafana.example.com
    service: http://grafana:3000

  - service: http_status:404

Enter fullscreen mode Exit fullscreen mode

Docker DNS automatically resolves container names. This creates a clean architecture.


Example Production Docker Stack

A realistic setup:

services:

  frontend:
    image: myapp/frontend

  backend:
    image: myapp/api

  postgres:
    image: postgres:16

  grafana:
    image: grafana/grafana

  prometheus:
    image: prom/prometheus

  cloudflared:
    image: cloudflare/cloudflared

Enter fullscreen mode Exit fullscreen mode

Everything communicates internally. Only Cloudflare exposes services externally.


Protecting Internal Applications with Zero Trust

This is arguably Cloudflare Tunnel's strongest feature.

Consider:

grafana.example.com

Enter fullscreen mode Exit fullscreen mode

Should Grafana be publicly accessible?

Probably not.

Instead:

Internet
↓
Cloudflare Access
↓
Authenticated User
↓
Grafana

Enter fullscreen mode Exit fullscreen mode

Setting Up Cloudflare Access

Navigate to:

Zero Trust Dashboard
→ Access
→ Applications
→ Add Application

Enter fullscreen mode Exit fullscreen mode

Select:

Self Hosted

Enter fullscreen mode Exit fullscreen mode

Enter:

grafana.example.com

Enter fullscreen mode Exit fullscreen mode

Restrict Access by Email

Example policy:

Allow
Email ends with:
@company.com

Enter fullscreen mode Exit fullscreen mode

Only approved users gain access.

Everyone else receives:

Access Denied

Enter fullscreen mode Exit fullscreen mode

before traffic reaches Grafana.


Restrict Access to Specific Users

Example:

alice@example.com
bob@example.com
charlie@example.com

Enter fullscreen mode Exit fullscreen mode

Only these users can authenticate. Excellent for administrative dashboards. Even if someone discovers the URL, they cannot reach the application.


Exposing Grafana Securely

Without Access:

Internet
↓
Grafana Login Screen

Enter fullscreen mode Exit fullscreen mode

Attackers can see the application.

With Access:

Internet
↓
Cloudflare Access Login
↓
Grafana

Enter fullscreen mode Exit fullscreen mode

Grafana itself becomes invisible to unauthorized users. This significantly improves security.


High Availability Tunnels

A common question:

What happens if cloudflared crashes?

Cloudflare supports multiple tunnel connectors.

One tunnel.

Several cloudflared instances.

Example:

Server A
   |
Tunnel

Server B
   |
Tunnel

Server C
   |
Tunnel

Enter fullscreen mode Exit fullscreen mode

Cloudflare automatically balances traffic. If one connector disappears:

Traffic continues

Enter fullscreen mode Exit fullscreen mode

No downtime.


Multi-Host Architecture

Imagine:

VM1 → Grafana
VM2 → Prometheus
VM3 → Jenkins

Enter fullscreen mode Exit fullscreen mode

All can participate in the same tunnel.

This enables distributed homelab architectures.


Monitoring Tunnel Health

Cloudflare provides metrics inside the dashboard.

Useful indicators:

  • Active connectors

  • Tunnel availability

  • Request volume

  • Errors

  • Traffic trends

Monitor regularly.


Viewing Logs

Systemd:

journalctl -u cloudflared -f

Enter fullscreen mode Exit fullscreen mode

Docker:

docker logs -f cloudflared

Enter fullscreen mode Exit fullscreen mode

Common troubleshooting begins here.


Detecting Misconfigured Services

Example symptom:

502 Bad Gateway

Enter fullscreen mode Exit fullscreen mode

Often means:

Cloudflared works
Service doesn't

Enter fullscreen mode Exit fullscreen mode

Verify local connectivity first.

curl http://localhost:8080

Enter fullscreen mode Exit fullscreen mode

or

curl http://backend:8080

Enter fullscreen mode Exit fullscreen mode

depending on deployment style.


Real-World Developer Workstation Setup

Many engineers expose local applications during development.

Example:

web-dev.example.com
api-dev.example.com

Enter fullscreen mode Exit fullscreen mode

Cloudflared configuration:

ingress:

  - hostname: web-dev.example.com
    service: http://localhost:3000

  - hostname: api-dev.example.com
    service: http://localhost:8080

  - service: http_status:404

Enter fullscreen mode Exit fullscreen mode

Useful for:

  • QA reviews/Webhook testing

  • Mobile testing

  • Client demonstrations

  • Remote collaboration

No VPN required.


Real-World Homelab Setup

A common self-hosting stack:

Jellyfin
Grafana
Prometheus
Portainer
Home Assistant
Uptime Kuma
Gitea

Enter fullscreen mode Exit fullscreen mode

Configuration:

ingress:

  - hostname: media.example.com
    service: http://localhost:8096

  - hostname: grafana.example.com
    service: http://localhost:3000

  - hostname: prometheus.example.com
    service: http://localhost:9090

  - hostname: portainer.example.com
    service: https://localhost:9443

  - hostname: home.example.com
    service: http://localhost:8123

  - hostname: status.example.com
    service: http://localhost:3001

  - hostname: git.example.com
    service: http://localhost:3005

  - service: http_status:404

Enter fullscreen mode Exit fullscreen mode

One tunnel.

Many services.

No port forwarding.


Common Mistakes

Exposing Everything Publicly

Avoid:

Public Grafana
Public Portainer
Public Jenkins

Enter fullscreen mode Exit fullscreen mode

Use Zero Trust.

Always.


Missing Catch-All Rule

Incorrect:

ingress:

  - hostname: api.example.com
    service: http://localhost:8080

Enter fullscreen mode Exit fullscreen mode

Correct:

ingress:

  - hostname: api.example.com
    service: http://localhost:8080

  - service: http_status:404

Enter fullscreen mode Exit fullscreen mode

Wrong Service Port

Example:

service: http://localhost:8080

Enter fullscreen mode Exit fullscreen mode

when the application actually runs on:

localhost:8000

Enter fullscreen mode Exit fullscreen mode

Always verify locally first.


Mixing Container and Host Networking

Bad:

service: http://localhost:3000

Enter fullscreen mode Exit fullscreen mode

inside Docker when service exists on another container.

Use:

service: http://frontend:3000

Enter fullscreen mode Exit fullscreen mode

instead.


Cloudflared vs Alternatives

Cloudflared

Best for:

  • Homelabs

  • Internal applications

  • Zero Trust

  • Long-running services


ngrok

Best for:

  • Temporary demos

  • Short-lived testing

  • Quick sharing


Reverse Proxy + Port Forwarding

Best for:

  • Full infrastructure control

  • Existing network expertise

But requires:

  • Open ports

  • Firewall management

  • SSL management

Cloudflared removes most of that complexity.


Recommended Production Architecture

For most teams and homelab operators:

Docker Containers
↓
Cloudflared
↓
Cloudflare Tunnel
↓
Cloudflare Access
↓
Internet

Enter fullscreen mode Exit fullscreen mode

Benefits:

  • No inbound ports

  • Automatic TLS

  • Identity-based authentication

  • Centralized access control

  • Easy scaling

  • Reduced attack surface

This architecture has become increasingly popular because it combines simplicity with strong security.


Final Thoughts

Cloudflare Tunnel starts as a convenient way to expose a local application. But its real value appears when you begin managing many services.

Instead of configuring:

  • Routers

  • Firewalls

  • Reverse proxies

  • Certificates

  • Public IPs

you create a secure outbound connection and let Cloudflare handle the rest.

The combination of:

  • Named tunnels

  • Ingress routing

  • Docker integration

  • Zero Trust Access

  • Automatic HTTPS

  • High availability connectors

creates a surprisingly powerful platform for self-hosting, development environments, internal tooling, and homelab infrastructure.

Whether you're exposing a single application for testing or managing dozens of internal services, cloudflared provides a clean, scalable, and secure solution that avoids many of the operational headaches traditionally associated with publishing local services to the internet.

Top comments (0)