--- title: Traefik на Windows Docker Desktop — нюансы type: concept tags: [traefik, docker, windows, letsencrypt, reverse-proxy] sources: [../sources/nas-recovery-session-2026-05-18.md] updated: 2026-05-19 --- # Traefik на Windows Docker Desktop Запуск traefik 2.6.6 на Windows Docker Desktop (WSL2 backend) с импортированным production-конфигом и сертификатами — пара ловушек. ## Pitfall 1: traefik.yml не загружается автоматом Symptom: traefik стартует, но routers из `data/custom/*.yml` ругаются на "non-existent resolver: letsEncrypt", сертификаты не отдаются. Reason: traefik 2.x при отсутствии `--configFile=` ищет в дефолтных путях (`/etc/traefik/`, `./traefik.yml`), но Linux-контейнер с CWD=`/` и bind-mount `./data/traefik.yml:/traefik.yml` — почему-то не подхватывает. **Fix:** явно указать в compose `command:`: ```yaml command: - "--configFile=/traefik.yml" - "--log.level=DEBUG" ``` Подтверждение в логах: `Configuration loaded from file: /traefik.yml`. ## Pitfall 2: acme.json permissions через bind-mount Symptom: traefik ругается `permissions 777 for /letsencrypt/acme.json are too open, please use 600` и **отключает** letsEncrypt resolver (даже если у него есть валидные certs). Reason: bind-mount Windows-файла в Linux-контейнере **всегда** показывает permissions `0777`. Изменить через `chmod` нельзя — bind-mount не транслирует POSIX-perms на Windows-side. **Fix:** named volume вместо bind для `/letsencrypt`: ```yaml volumes: - "traefik_letsencrypt:/letsencrypt" # вместо ./letsencrypt:/letsencrypt - "./data/traefik.yml:/traefik.yml:ro" - "./data/custom/:/custom/:ro" # и снизу: volumes: traefik_letsencrypt: ``` Population volume единоразово: ```powershell docker volume create traefik_traefik_letsencrypt docker run --rm \ -v traefik_traefik_letsencrypt:/dest \ -v /traefik/letsencrypt:/src:ro \ alpine sh -c "cp /src/acme.json /dest/acme.json && cp /src/acme.old.json /dest/acme.old.json && chmod 600 /dest/*" ``` ## Pitfall 3: docker.sock provider не работает Symptom: ``` Failed to retrieve information of the docker client and server host: Error response from daemon: providerName=docker ``` С `-v /var/run/docker.sock:/var/run/docker.sock:ro` сокет монтируется (видно `srw-rw---- 1 root root` внутри контейнера), но соединение с daemon обрывается. Reason: Docker Desktop на Windows транслирует docker.sock через WSL2 layer. Иногда permission-моэль клиента (libdocker) не принимает то, что предоставляет Desktop's proxy. **Workaround:** **полностью отключить docker-provider, использовать только file-provider** для всех routes. Маршруты, которые в производстве были как traefik labels на контейнерах (minio, elasticsearch, imgproxy с `traefik.http.routers...labels`), переписываются в `data/custom/.yml` руками: ```yaml # data/custom/minio.yml http: routers: minio: entryPoints: [https] rule: Host(`minio.kzntsv.site`) tls: certResolver: letsEncrypt service: minio services: minio: loadBalancer: servers: - url: http://minio:9000 # docker DNS name (работает потому что все на одной network=proxy) ``` Подобно для elasticsearch (с basicAuth middleware), imgproxy (→ imgproxy-nginx:80), etc. ## Pitfall 4: file-provider не подхватывает изменения через bind-mount `traefik.yml` имеет `providers.file.watch: true`, но на Windows bind-mount inotify не работает через WSL2-слой. Изменения в `data/custom/*.yml` не подхватываются автоматически. **Workaround:** `docker compose restart traefik` после изменений в custom/. Несколько секунд downtime, ничего страшного. ## Production порты vs нашa конфигурация Production traefik compose биндил на host: ```yaml ports: - "8000:80" # router forwards public 80 → host 8000 → container 80 - "4443:443" - "8080:8080" ``` То есть **роутер делает port-translation** 80→8000, 443→4443. Не стандартные порты, но работает. На Windows-хосте оставили те же порты (8000/4443) — не конфликтуют с локальным IIS на 80 (который не используется, но не выключен). И не требуют admin для bind. ## host.docker.internal Из traefik-контейнера достучаться до VM (которая в VBox NAT, не в docker network) — через **`host.docker.internal`**: ```yaml servers: - url: http://host.docker.internal:18080/ # 18080 = VBox NAT-forwarded port → VM:80 ``` Docker Desktop резолвит `host.docker.internal` в IP хоста (обычно 172.x.x.1 из docker bridge perspective). Дальше Windows-host обрабатывает 18080 → VBox NAT → VM:80 → IIS → CMS. ## Pitfall 5: X-Forwarded не используется ASP.NET — port utечка в admin URLs (RESOLVED 2026-05-19) **Симптом:** CMS-admin генерирует URLs с internal IIS portом (`:8089` после attempt 2; ранее `:4443` от traefik) → browser HSTS auto-upgrade ломает TLS на нестандартном портe → admin SPA broken. **Root cause:** Traefik 2.x **шлёт** `X-Forwarded-Proto: https` / `X-Forwarded-Host` / `X-Forwarded-Port` по-default (когда entrypoint https). НО CMS-код (`MoreThenCms.Admin\Mis\Web\Mvc\UrlHelpers\UrlHelpers.cs`) читает **socket-level** `SERVER_PORT` / `SERVER_PORT_SECURE`, **игнорируя** X-Forwarded-* headers. На VM работало случайно потому что IIS binding `:80` → `SERVER_PORT=80` стрипалось whitelist'ом `{80,443}` в коде. **Fix:** URL Rewrite 2.1 + `` rule на host IIS — переписывает `HTTPS`/`SERVER_PORT`/`SERVER_PORT_SECURE` на основе `X-Forwarded-Proto`. Детально в [[cms-server-port-leak-fix]]. Path B (поменять traefik http entrypoint :80 inside container чтобы IIS-binding :80 не получал loop от Docker NAT) — **не работает** на Windows Docker Desktop из-за WSL2 NAT quirk, см. там же. ## DNS-01 challenge через REGRU `traefik.yml` имеет HTTP-01 challenge: ```yaml certificatesResolvers: letsEncrypt: acme: email: vitya.kuznetsov@gmail.com storage: /letsencrypt/acme.json httpChallenge: entryPoint: http ``` В compose env уже: ``` REGRU_USERNAME=OpeItcLoc03 REGRU_PASSWORD=ytyYqC%u%QAJ ``` Эти креды для **DNS-01** через REGRU API. Просто закомментировать `httpChallenge` и активировать `dnsChallenge` в traefik.yml: ```yaml certificatesResolvers: letsEncrypt: acme: email: ... storage: /letsencrypt/acme.json dnsChallenge: provider: regru ``` DNS-01 более надёжный (не требует public:80 reachable для validation) и работает даже если HTTP-01 challenge не пройдёт (например, домен временно на другой хостинге). **Текущие 40 LE-сертификатов из acme.json валидны ~3 месяца** (LE default). Когда подойдут к истечению — пора переключать на DNS-01. Связано: [[snolla-recovery-vm]], [[windows-recovery-host]], [[recovery-architecture-snapshot]].