- 
      
- 
        Save mattdy/d741344366a4fbb86f5034adfd1ad191 to your computer and use it in GitHub Desktop. 
| 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 | 
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?
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
You should be able to create another instance of the
cloudflared-tunnelcontainer if that's your desired approach, although I don't think there's anything stopping you running both domains over the same tunnel.