-
-
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 |
Hello. Thank you for this write up. Port forwarding is not an option. Cloudflare tunnel is. I've mentioned to got this setup and an almost ready to convert over from nginx proxy manager. I have question. I have 2 domains I am wanting to use use. How would I setup another tunnel in docker? I know I will need to create another tunnel id in cloudflare but am unsure what I need to edit in docker compose file.
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.
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
Maybe try without
/apiat the end of TRAEFIK_POLL_URL? Other than that, I'm afraid I don't know!