Cloudflare Tutorial Advanced

How to Run Cloudflare Tunnel in Front of Traefik

Expose Docker services through Cloudflare with no inbound ports open — cloudflared plus Traefik, the forwarded-header setting that trips people up, and how to verify the firewall is closed.

An outbound tunnel from a Docker host to Cloudflare with all inbound ports closed

Tested with

OperatingSystem
Ubuntu 24.04 LTS
Docker
27.x
DockerCompose
v2.29
Cloudflared
2026.6.x
Traefik
v3.3

Before you start

  • A domain already active on Cloudflare
  • Docker and Docker Compose on the host
  • Basic familiarity with Traefik routers and labels
On this page

Cloudflare Tunnel removes inbound ports from the equation. cloudflared dials out to Cloudflare, requests arrive back through that connection, and your firewall can drop everything except SSH.

Putting Traefik behind it means you configure routing once, with labels, instead of adding a public hostname to the tunnel for every service.

Everything below runs on one Ubuntu 24.04 host with Docker 27.

What this replaces

Without the tunnel you open 80 and 443, allowlist Cloudflare’s IP ranges, and manage an origin certificate. With it you open nothing, and the origin is not reachable by IP at all.

The mechanism is straightforward: cloudflared opens outbound connections to Cloudflare’s edge and keeps them alive. When a request arrives for one of your public hostnames, Cloudflare sends it down an existing connection rather than dialling your server. Because the connections are outbound, a NAT gateway, a dynamic IP address or a default-deny firewall makes no difference — which is also why this works on a home server or inside a private network.

The trade is a dependency on cloudflared staying connected, and one more moving part in the request path.

Not covered here: routing non-HTTP protocols such as SSH or RDP through the tunnel (possible, uses cloudflared access), or Cloudflare Access policies beyond a note at the end.

Create the tunnel

Use a remotely-managed tunnel. The connector gets a token, and the hostname routing lives in the dashboard rather than in a file on the server — which means you can change routes without touching the host.

  1. In the Cloudflare dashboard, open Zero Trust → Networks → Tunnels and create a tunnel. Name it after the host, not the application.
  2. Choose the Docker connector. Cloudflare shows a docker run command with a long token in it.
  3. Copy only the token. You will put it in an env file, not in a shell command that lands in your history.
/opt/edge/.env
TUNNEL_TOKEN=eyJhIjoiN2Y...
bash
chmod 600 /opt/edge/.env

The Compose stack

/opt/edge/compose.yaml
networks:
  edge:
    name: edge

services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    restart: unless-stopped
    command: tunnel --no-autoupdate --metrics 0.0.0.0:2000 run
    environment:
      TUNNEL_TOKEN: ${TUNNEL_TOKEN}
    ports:
      # Metrics on loopback only — never publish this.
      - "127.0.0.1:2000:2000"
    networks:
      - edge
    depends_on:
      - traefik

  traefik:
    image: traefik:v3.3
    restart: unless-stopped
    command:
      - --providers.docker=true
      - --providers.docker.exposedByDefault=false
      - --providers.docker.network=edge
      - --entrypoints.web.address=:80
      - --entrypoints.web.forwardedHeaders.trustedIPs=172.18.0.0/16
      - --accesslog=true
      - --log.level=INFO
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - edge

  whoami:
    image: traefik/whoami
    restart: unless-stopped
    networks:
      - edge
    labels:
      - traefik.enable=true
      - traefik.http.routers.whoami.rule=Host(`app.example.com`)
      - traefik.http.routers.whoami.entrypoints=web
      - traefik.http.services.whoami.loadbalancer.server.port=80

Traefik publishes no ports. cloudflared reaches it as traefik:80 over the edge network, and nothing else can.

Find the real subnet before copying 172.18.0.0/16 — Docker assigns it when it creates the network:

bash
docker compose up -d
docker network inspect edge --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'
Expected output
bash
172.18.0.0/16

If it differs, update trustedIPs and restart Traefik.

Why trustedIPs matters

Traefik strips X-Forwarded-Proto, X-Forwarded-For and X-Forwarded-Host from requests unless they arrive from an address in trustedIPs. That default is correct — otherwise any client could claim any source IP — but it means an untrusted cloudflared container produces two visible symptoms:

  • Your application sees http and generates http:// links or redirects to HTTPS, causing a loop.
  • Every access log line shows the cloudflared container’s internal address instead of the visitor’s.

With the subnet trusted, Cloudflare’s CF-Connecting-IP and the standard forwarded headers reach your app intact.

Route the hostname to Traefik

Back in the tunnel’s configuration, add a public hostname:

FieldValue
Subdomainapp
Domainexample.com
Service typeHTTP
URLtraefik:80

Cloudflare creates the DNS record for you — a proxied CNAME pointing at the tunnel. You do not add an A record, and there is no origin IP in DNS at all.

Every additional service needs a Traefik label and a public hostname entry pointing at the same traefik:80. Traefik picks them apart by Host header.

Verify

Check the connector is registered:

bash
curl -s localhost:2000/ready
Expected output
bash
{"status":200,"readyConnections":4}

Four connections is normal — cloudflared establishes redundant links to two Cloudflare data centres.

Then check the route end to end:

bash
curl -sS https://app.example.com/ | head -5
Expected output
bash
Hostname: 9f3c2b1a4d77
IP: 127.0.0.1
IP: 172.18.0.4
RemoteAddr: 172.18.0.3:41522

Now close the firewall and confirm nothing else answers:

bash
sudo ufw default deny incoming
sudo ufw allow OpenSSH
sudo ufw enable
bash
# from another machine
nmap -Pn -p 80,443,2000 203.0.113.42
Expected output
bash
PORT     STATE    SERVICE
80/tcp   filtered http
443/tcp  filtered https
2000/tcp filtered cisco-sccp

The site still works. That is the point of the exercise.

Common errors

ERR Couldn't start tunnel error="Unauthorized: Failed to get tunnel" — the token is wrong, truncated, or belongs to a deleted tunnel. Regenerate it from the dashboard.

Cloudflare Error 1033. DNS points at a tunnel with no connector attached. docker compose logs cloudflared will show why it is not registering.

Error 502 from Cloudflare. cloudflared connected but could not reach the origin service. Usually the public hostname URL is localhost:80 instead of traefik:80 — inside a container, localhost is that container.

Traefik returns 404 for a service that is running. exposedByDefault=false requires traefik.enable=true on the container, and the router needs an entrypoints label matching a defined entrypoint. Check docker compose logs traefik for the router it actually built.

Infinite redirect loop. Your app redirects HTTP to HTTPS and does not see X-Forwarded-Proto: https. Fix trustedIPs, or remove the redirect and let Cloudflare’s Always Use HTTPS handle it at the edge.

The tunnel drops every few minutes. Outbound UDP on port 7844 is blocked, so cloudflared falls back and reconnects repeatedly. Allow it outbound, or force HTTP/2 with --protocol http2.

Production notes

Run two connectors on different hosts against the same tunnel once the service matters. Cloudflare load balances across registered connectors, so this is a config-free HA improvement.

Pin the cloudflared image to a dated tag rather than latest. Connector releases are frequent and an unattended pull can restart your edge at an unhelpful moment.

Put Cloudflare Access in front of anything internal — dashboards, admin panels, the Traefik dashboard if you expose it. It authenticates at the edge, so unauthenticated requests never reach your host at all. That is a stronger position than any application-level login, and it takes about five minutes per hostname.

Scrape localhost:2000/metrics from your monitoring. The connection count dropping to zero is the signal you want to page on, and it happens well before users notice.

Frequently asked questions

Do I still need ports 80 and 443 open on the server?

No. cloudflared makes outbound connections to Cloudflare and traffic arrives back down those. A tunnelled host can run with every inbound port closed except SSH, and you can move SSH into the tunnel too.

Why does my application think every request is HTTP, not HTTPS?

Traefik is discarding `X-Forwarded-Proto` because the cloudflared container's IP is not in the entrypoint's `forwardedHeaders.trustedIPs` list. Add the Docker network subnet.

Can I use Cloudflare Tunnel with Let's Encrypt in Traefik?

You can, but there is little reason to. Cloudflare terminates TLS at the edge and the hop to your origin is inside the tunnel. Serving plain HTTP on an internal-only entrypoint keeps the config smaller and removes the ACME challenge problem you would otherwise have with no open ports.

What does Error 1033 mean?

Cloudflare has a DNS record for the hostname pointing at a tunnel, but no connector is currently registered for it. Either cloudflared is not running, or it is running with a token for a different tunnel.

How many cloudflared replicas should I run?

Two, on different hosts, once the service matters. Multiple connectors on the same tunnel are load balanced automatically, so a second replica removes cloudflared as a single point of failure without any extra config.

Sources

  1. Create a remotely-managed tunnel Cloudflare Primary source Accessed 26 Jun 2026
  2. Cloudflare Tunnel origin configuration Cloudflare Primary source Accessed 26 Jun 2026
  3. Traefik Docker provider Traefik Labs Primary source Accessed 26 Jun 2026
  4. Traefik entrypoints — forwarded headers Traefik Labs Primary source Accessed 26 Jun 2026
  5. Cloudflare Access policies Cloudflare Primary source Accessed 26 Jun 2026
4 min read

Part of a learning path

  • Cloudflare beginner

    How to Point a Cloudflare Domain at Your VPS

    Create the A record, pick the right proxy status, set the SSL mode to Full (strict) and lock the origin so only Cloudflare can reach it — plus what errors 521, 522, 525 and 526 actually mean.

    4 min
  • Security intermediate

    How to Secure a VPS That Runs Docker Containers

    SSH hardening, a default-deny firewall, and the fix for the problem that catches almost everyone — Docker publishing container ports straight past UFW into the public internet.

    4 min