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
@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