Skip to content

Instantly share code, notes, and snippets.

@mattdy
Last active October 29, 2025 22:15
Show Gist options
  • Save mattdy/d741344366a4fbb86f5034adfd1ad191 to your computer and use it in GitHub Desktop.
Save mattdy/d741344366a4fbb86f5034adfd1ad191 to your computer and use it in GitHub Desktop.
Traefik on Docker Swarm accessed via Cloudflare Tunnel
Please see https://mattdyson.org/blog/2024/02/using-traefik-with-cloudflare-tunnels for a detailed write-up of this configuration
ROOT_DOMAIN=yourdomain.com
HTTP_TIMEOUT=60
POLLING_INTERVAL=10
PROPAGATION_TIMEOUT=3600
TTL=300
PROVIDERS_GOOGLE_CLIENT_ID=<GOOGLE CLIENT ID>
PROVIDERS_GOOGLE_CLIENT_SECRET=<GOOGLE CLIENT SECRET>
SECRET=RandomTextGoesHere
WHITELIST=<YOUR GOOGLE ACCOUNT EMAIL>
LOG_LEVEL=INFO
ZONE_ID=<YOUR CLOUDFLARE ZONE ID>
TUNNEL_TOKEN=<YOUR CLOUDFLARE TUNNEL TOKEN>
version: '3.7'
services:
whoami:
image: traefik/whoami
command:
- --name=externalapp
deploy:
labels:
- "traefik.enable=true"
- "traefik.docker.network=traefik"
- "traefik.http.routers.external.rule=Host(`external.yourdomain.com`)"
- "traefik.http.routers.external.entrypoints=websecure"
- "traefik.http.routers.external.tls=true"
- "traefik.http.routers.external.middlewares=forward-auth"
- "traefik.http.services.external.loadbalancer.server.port=80"
- "traefik.constraint=proxy-public"
networks:
traefik:
external: true
version: '3.7'
services:
whoami:
image: traefik/whoami
command:
- --name=internalapp
deploy:
labels:
- "traefik.enable=true"
- "traefik.docker.network=traefik"
- "traefik.http.routers.internal.rule=Host(`internal.yourdomain.com`)"
- "traefik.http.routers.internal.entrypoints=websecure"
- "traefik.http.routers.internal.tls=true"
- "traefik.http.services.internal.loadbalancer.server.port=80"
networks:
traefik:
external: true
version: '3.7'
services:
reverse-proxy:
image: traefik:v2.10
command:
- "--log"
- "--log.level=${LOG_LEVEL:-INFO}"
- "--log.format=json"
- "--api.insecure=true"
- "--providers.docker"
- "--providers.docker.swarmMode=true"
- "--providers.docker.exposedbydefault=false"
- "--providers.file.directory=/config"
- "--providers.file.watch=true"
- "--serversTransport.insecureSkipVerify=true" # Allow self-signed certificates for target hosts - https://doc.traefik.io/traefik/routing/overview/#insecureskipverify
- "--metrics"
- "--metrics.prometheus.buckets=0.1,0.3,1.2,5.0"
- "--entrypoints.web.address=:80"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
- "--entrypoints.websecure.address=:443"
- "--entrypoints.websecure.http.tls=true"
- "--entrypoints.websecure.http.tls.certresolver=letsencrypt"
- "--entrypoints.webinternal.address=:82"
- "--certificatesresolvers.letsencrypt.acme.email=<YOUR EMAIL>"
- "--certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/letsencrypt.json"
- "--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare"
- "--certificatesresolvers.letsencrypt.acme.dnschallenge.delaybeforecheck=300"
- "--certificatesresolvers.letsencrypt.acme.dnschallenge.resolvers=8.8.8.8:53"
secrets:
- cf_token
environment:
- CLOUDFLARE_DNS_API_TOKEN_FILE=/run/secrets/cf_token
- CLOUDFLARE_HTTP_TIMEOUT=${HTTP_TIMEOUT}
- CLOUDFLARE_POLLING_INTERVAL=${POLLING_INTERVAL}
- CLOUDFLARE_PROPAGATION_TIMEOUT=${PROPAGATION_TIMEOUT}
- CLOUDFLARE_TTL=${TTL}
deploy:
restart_policy:
condition: any
delay: 5s
max_attempts: 3
window: 120s
update_config: # Start new instance before stopping existing one
delay: 10s
order: start-first
parallelism: 1
rollback_config:
parallelism: 0
order: stop-first
placement:
constraints:
- node.role == manager
labels:
- traefik.enable=true
- traefik.http.routers.api.rule=Host(`traefik.${ROOT_DOMAIN}`)
- traefik.http.routers.api.service=api@internal
- traefik.http.routers.api.entrypoints=websecure
- traefik.http.routers.api.tls=true
- traefik.http.services.api.loadbalancer.server.port=8080
ports:
# HTTP
- target: 80
published: 80
# HTTPS
- target: 443
published: 443
# Web UI (enabled by --api.insecure=true)
- target: 8080
published: 8080
networks:
- traefik
- internal
volumes:
# So that Traefik can listen to the Docker events
- /var/run/docker.sock:/var/run/docker.sock
- acme:/etc/traefik/acme
- traefik:/config
- cloudflare:/cloudflare
traefik-forward-auth:
image: thomseddon/traefik-forward-auth:2.1.0
networks:
- traefik
environment:
- PROVIDERS_GOOGLE_CLIENT_ID=${PROVIDERS_GOOGLE_CLIENT_ID}
- PROVIDERS_GOOGLE_CLIENT_SECRET=${PROVIDERS_GOOGLE_CLIENT_SECRET}
- SECRET=${SECRET}
- AUTH_HOST=auth.${ROOT_DOMAIN}
- COOKIE_DOMAIN=${ROOT_DOMAIN}
- WHITELIST=${WHITELIST}
deploy:
labels:
- traefik.enable=true
- traefik.docker.network=traefik
- traefik.http.routers.auth.rule=Host(`auth.${ROOT_DOMAIN}`)
- traefik.http.routers.auth.entrypoints=websecure
- traefik.http.routers.auth.tls=true
- traefik.http.routers.auth.tls.domains[0].main=${ROOT_DOMAIN}
- traefik.http.routers.auth.tls.domains[0].sans=*.${ROOT_DOMAIN}
- traefik.http.routers.auth.tls.certresolver=letsencrypt
- traefik.http.routers.auth.service=auth@docker
- traefik.http.services.auth.loadbalancer.server.port=4181
- traefik.http.middlewares.forward-auth.forwardauth.address=http://traefik-forward-auth:4181
- traefik.http.middlewares.forward-auth.forwardauth.trustForwardHeader=true
- traefik.http.middlewares.forward-auth.forwardauth.authResponseHeaders=X-Forwarded-User
- traefik.http.routers.auth.middlewares=forward-auth
- traefik.constraint=proxy-public
tunnel:
container_name: cloudflared-tunnel
image: cloudflare/cloudflared
restart: unless-stopped
command: tunnel run
deploy:
mode: replicated
replicas: 3
update_config:
delay: 30s
order: start-first
monitor: 20s
networks:
- traefik
environment:
- TUNNEL_TOKEN=${TUNNEL_TOKEN}
error-pages:
image: tarampampam/error-pages:2.26.0
environment:
TEMPLATE_NAME: l7-dark
networks:
- traefik
deploy:
mode: replicated
replicas: 2
update_config:
delay: 20s
order: start-first
monitor: 10s
labels:
- traefik.enable=true
- traefik.docker.network=traefik
# use as "fallback" for any non-registered services (with priority below normal)
- traefik.http.routers.error-pages.rule=HostRegexp(`{host:.+}`)
- traefik.http.routers.error-pages.priority=10
# should say that all of your services work on https
- traefik.http.routers.error-pages.tls='true'
- traefik.http.routers.error-pages.entrypoints=websecure
- traefik.http.routers.error-pages.middlewares=error-pages
- traefik.http.services.error-pages.loadbalancer.server.port=8080
# "errors" middleware settings
- traefik.http.middlewares.error-pages.errors.status=400-599
- traefik.http.middlewares.error-pages.errors.service=error-pages
- traefik.http.middlewares.error-pages.errors.query=/{status}.html
cloudflare-companion:
image: ghcr.io/tiredofit/docker-traefik-cloudflare-companion:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
deploy:
placement:
constraints:
- node.role == manager
environment:
- TIMEZONE=Europe/London
- LOG_TYPE=CONSOLE
- LOG_LEVEL=INFO
- TRAEFIK_VERSION=2
- RC_TYPE=CNAME
- TARGET_DOMAIN=${ROOT_DOMAIN}
- REFRESH_ENTRIES=TRUE
- DOCKER_SWARM_MODE=TRUE
- ENABLE_TRAEFIK_POLL=TRUE
- TRAEFIK_POLL_URL=https://traefik.${ROOT_DOMAIN}/api
- TRAEFIK_FILTER_LABEL=traefik.constraint
- TRAEFIK_FILTER=proxy-public
- DOMAIN1=${ROOT_DOMAIN}
- DOMAIN1_ZONE_ID=${ZONE_ID}
- DOMAIN1_PROXIED=TRUE
restart: always
networks:
- internal
secrets:
- cf_token
networks:
traefik:
external: true
internal:
volumes:
acme:
traefik:
cloudflare:
secrets:
cf_token:
external: true
@mattdy
Copy link
Author

mattdy commented Sep 16, 2025

I know I will need to create another tunnel id in cloudflare but am unsure what I need to edit in docker compose file.

You should be able to create another instance of the cloudflared-tunnel container if that's your desired approach, although I don't think there's anything stopping you running both domains over the same tunnel.

@layydback
Copy link

layydback commented Oct 11, 2025

UPDATE: Got it to work using application routes and using origin TLS setting.

I have a few self hosted apps and I have my tunnel UP, but I'm stuck on routing the tunnel to each one of my Traefik routers since the ports are not exposed other than to the docker containers.

example I have status.mydomain.com running Uptime Kuma. Traefik handles the hand off of that service. But how to I set up the tunnel to use it?

@frntman
Copy link

frntman commented Oct 12, 2025

I think this seems to be the missing piece for me as well, I know I have to be missing a config, or setup on the cloudflare side, I have a healthy tunnel, I just have not been able to figure out how to setup the "Published application routes" section so I believe that's why the cloudflare-companion app will not start.

Published application routes - Basic Information:

Published hostname = *.mydomain.com
Path = *
Service = https//:container_name
Origin configurations = 0

cloudflare-companion log: # This just scrolls until app stopped

2025-10-11.23:58:20 [STARTING] ** [traefik-cloudflare-companion] [32] Starting Traefik Cloudflare Companion
Traceback (most recent call last):
  File "/usr/lib/python3.12/site-packages/requests/models.py", line 974, in json
    return complexjson.loads(self.text, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/json/__init__.py", line 346, in loads
    return _default_decoder.decode(s)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/json/decoder.py", line 338, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/json/decoder.py", line 356, in raw_decode
    raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
  File "/usr/sbin/cloudflare-companion", line 545, in <module>
    sync_mappings(get_initial_mappings(traefik_included_hosts, traefik_excluded_hosts), doms)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/sbin/cloudflare-companion", line 426, in get_initial_mappings
    add_to_mappings(mappings, check_traefik(included_hosts, excluded_hosts))
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/sbin/cloudflare-companion", line 352, in check_traefik
    for router in r.json():
                  ^^^^^^^^
  File "/usr/lib/python3.12/site-packages/requests/models.py", line 978, in json
    raise RequestsJSONDecodeError(e.msg, e.doc, e.pos)
requests.exceptions.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

TEST Application.yml

services:
  # Overseerr - Media Requests and Discovery for Plex
  overseerr:
    image: lscr.io/linuxserver/overseerr:latest
    container_name: overseerr
    security_opt:
      - no-new-privileges:true
    restart: unless-stopped
    profiles: ["apps", "all"]
    networks:
      - default
    ports:
      - "$OVERSEERR_PORT:5055"
    volumes:
      - $DOCKERDIR/appdata/overseerr:/config
    environment:
      PUID: $PUID
      PGID: $PGID
      TZ: $TZ
      traefik.constraint: proxy-public

Test Traefik Rule:

http:
  routers:
    overseerr-rtr-bypass:
      rule: "Host(`overseerr.{{env "DOMAINNAME_1"}}`) && Header(`traefik-auth-bypass-key`, `{{env "TRAEFIK_AUTH_BYPASS_KEY" }}`)"
      entryPoints:
        - websecure-external
        - websecure-internal
      middlewares:
        - chain-no-auth
      service: overseerr-svc
      priority: 100
    overseerr-rtr:
      rule: "Host(`overseerr.{{env "DOMAINNAME_1"}}`)" 
      entryPoints:
        - websecure-external
        - websecure-internal
      middlewares:
        - chain-authelia
      service: overseerr-svc
      traefik.constraint: public-proxy
      priority: 99
      tls:
        certResolver: dns-cloudflare
        options: tls-opts@file
  services:
    overseerr-svc:
      loadBalancer:
        servers:
          - url: "http://10.10.0.55:5055" # http://IP-ADDRESS:PORT

TEST cloudflare-companion.yml

services:
  cloudflare-companion:
    container_name: cloudflare-companion
    image: tiredofit/traefik-cloudflare-companion:latest
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - $DOCKERDIR/logs/$HOSTNAME/cloudflare-companion:/logs
    networks:
      - socket_proxy
    #security_opt:
    #  - no-new-privileges:true
    restart: always
    profiles: ["apps", "all"]
    deploy:
      placement:
        constraints:
          - node.role == manager
    environment:
      - TIMEZONE=$TZ
      - LOG_TYPE=BOTH
      - LOG_LEVEL=INFO
      - TRAEFIK_VERSION=2
      - RC_TYPE=CNAME
      - TARGET_DOMAIN=host.mydomain.com
      - REFRESH_ENTRIES=TRUE
      - ENABLE_TRAEFIK_POLL=TRUE
      - TRAEFIK_POLL_URL=https://traefik.$DOMAINNAME_1/api
      - TRAEFIK_FILTER_LABEL=traefik.constraint
      - TRAEFIK_FILTER=proxy-public
      # - DOCKER_HOST=tcp://socket-proxy:2375
      - TRAEFIK_FILTER=proxy-public
      - DOMAIN1=$DOMAINNAME_1
      - DOMAIN1_ZONE_ID=$CLOUDFLARE_ZONE_ID
      - DOMAIN1_PROXIED=TRUE
      - CF_TOKEN=qwelrfhjerfjpewifjpewijfjedfsdfsdfwfrfj 
      - DEFAULT_TTL=1
      - ENABLE_TRAEFIK_POLL=TRUE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment