Traefik Docker 自动发现与本地入口工具手册
从不断变化的 Compose 服务开始
本地微服务联调经常从端口表开始:前端占 5173,订单服务占 8081,同一服务的候选版本又占 8082。每增加一个容器,开发者都要同步端口、反向代理和本地域名。服务停止后,旧入口仍可能留在配置里;一条错误 label 也可能把原本只应在容器网络内访问的组件暴露出来。
Traefik 的价值不是“少写几行反向代理”,而是把容器事件转换成运行时 router、middleware 和 service。代价也随之变化:谁能改 label,谁就能影响入口;谁能访问 Docker API,谁就接近宿主机控制面。下面从一个 v1 实例起步,再接入 HTTPS、v2、健康检查和灰度,最后把直接 Docker socket 收紧为受限 API 代理。
Traefik 可以作为二进制、系统服务或容器运行。Docker Provider 场景优先使用官方容器,示例固定为 traefik:v3.7.1:
docker pull traefik:v3.7.1
docker run --rm traefik:v3.7.1 version输出应包含 Traefik 版本、Go 版本和构建信息。镜像拉取失败先检查代理和镜像仓库访问,不要把不明来源镜像改成长期替代品。版本升级前应查看 Traefik v3.7.1 官方 release 和 发布与弃用策略,核对路由规则语法、Provider 配置和已弃用字段,并记录目标平台实际镜像 digest。
先分开启动配置与路由配置
Traefik 有两种生命周期不同的配置:entryPoint、Provider、日志、API 和 certificate resolver 属于启动配置,通常需要重启进程;router、middleware、service、TLS certificate 和 ServersTransport 属于路由配置,可由 Docker、File 等 Provider 动态更新。把两类配置混在同一堆 labels 中,会让“为何没有热更新”变得难以判断。
创建 gateway/traefik/traefik.yaml:
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
provider-sink:
address: "127.0.0.1:65535"
api:
dashboard: true
log:
level: INFO
accessLog:
format: json
providers:
docker:
exposedByDefault: false
allowEmptyServices: true
network: gateway
file:
directory: /etc/traefik/dynamic
watch: trueexposedByDefault 的官方默认值是 true。显式改为 false 后,只有带 traefik.enable=true 的容器才会进入生成配置。network: gateway 指定 Traefik 连接上游时优先使用的网络;如果 Compose 实际创建了带项目前缀的网络名,后面要通过 docker network inspect 核对,或者给网络设置固定 name。Docker Provider 配置项
先建立只含 v1 的 Compose。应用使用两个独立健康文件:Docker 检查 /livez,Traefik 检查 /readyz,这样后面可以分别制造故障。
services:
traefik:
image: ${TRAEFIK_IMAGE:-traefik:v3.7.1}
command: ["--configFile=/etc/traefik/traefik.yaml"]
ports:
- "80:80"
- "443:443"
volumes:
- ./gateway/traefik/traefik.yaml:/etc/traefik/traefik.yaml:ro
- ./gateway/traefik/dynamic:/etc/traefik/dynamic:ro
- ./gateway/traefik/certs:/etc/traefik/certs:ro
- ./gateway/traefik/secrets:/run/secrets:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
security_opt:
- no-new-privileges:true
networks: [gateway]
app-v1:
image: nginx:1.30.3-alpine
command:
- /bin/sh
- -ec
- |
printf 'version=v1\n' > /usr/share/nginx/html/index.html
printf 'live\n' > /usr/share/nginx/html/livez
printf 'ready\n' > /usr/share/nginx/html/readyz
exec nginx -g 'daemon off;'
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1/livez >/dev/null || exit 1"]
interval: 5s
timeout: 2s
retries: 2
labels:
traefik.enable: "true"
traefik.http.services.app-v1.loadbalancer.server.port: "80"
traefik.http.services.app-v1.loadbalancer.healthcheck.path: "/readyz"
traefik.http.services.app-v1.loadbalancer.healthcheck.interval: "5s"
traefik.http.services.app-v1.loadbalancer.healthcheck.timeout: "2s"
traefik.http.services.app-v1.loadbalancer.healthcheck.status: "200"
traefik.http.routers.app-v1-provider-sink.rule: "Host(`provider-sink.invalid`)"
traefik.http.routers.app-v1-provider-sink.entrypoints: "provider-sink"
traefik.http.routers.app-v1-provider-sink.service: "app-v1"
networks: [gateway]
networks:
gateway:
name: gateway用于模拟上游的镜像固定为 nginx:1.30.3-alpine。更换上游镜像时先核对 Nginx 官方镜像标签 并保存实际 digest;stable、alpine 等移动标签不应进入可重复的联调基线。
直接挂载 docker.sock:ro 适合先看清发现链路,但它不是最终权限方案。:ro 只限制容器对 socket 文件本身的挂载方式,并没有给 Docker API 增加只读授权;后面会把它替换掉。
用 File Provider 建立第一条 HTTP 路由
创建 gateway/traefik/dynamic/routes.yaml:
http:
routers:
app-http:
entryPoints: [web]
rule: Host(`app.localhost`)
service: app-v1@docker业务 router 来自 File Provider,实际上游 service 来自 Docker Provider,@docker 明确了跨 Provider 引用。Docker Provider 会为已启用容器自动推导 router,因此应用还显式声明了一个只绑定容器内回环 provider-sink 的占位 router,阻止默认 router 落到公开 entryPoint。
这只是仓库约定,不是 Traefik 的权限隔离:能修改容器 labels 的主体仍可创建新的公开 router、middleware,甚至把内部 API 暴露出去。CI 必须拒绝应用 Compose 中未批准的 traefik.http.routers.* 与 traefik.http.middlewares.* 标签,部署权限也要限制 labels 变更;File Provider 并不会自动获得路由独占权。
启动并观察两个 Provider:
docker compose config > ./gateway/traefik/compose.rendered.yaml
docker compose up -d
docker compose ps
docker compose logs --tail=200 traefik
curl -i http://app.localhost/预期响应为 200 和 version=v1。404 表示请求已到 Traefik,但没有 router 匹配 Host、Path 或 entryPoint;502 通常表示 router 已匹配但上游地址、端口、协议或网络错误;503 通常表示 service 存在但没有可用 server。不要把三者都归为“代理失败”。
应用不需要发布宿主端口。Traefik 应通过 gateway 网络访问容器内部端口:
docker network inspect gateway
docker compose exec app-v1 wget -qO- \
--header='Host: app.localhost' http://traefik/第二条命令从同一 Docker 网络经过 Traefik 再返回 version=v1,同时证明容器 DNS、入口端口、Host 路由和上游网络可达。官方 Traefik 镜像不保证包含 shell、curl 或 wget,因此不要把临时安装诊断工具写进运行容器。若容器在多个网络中,使用 traefik.docker.network=<运行时网络名> 指定目标网络,并以 inspect 输出为准;Compose 文件里的短名称不一定等于 Docker 运行时名称。
websecure 不会自动获得证书
与 Caddy 不同,声明 Host 规则和 websecure entryPoint 并不会让 Traefik 自动签发证书。router 必须启用 TLS,证书则来自 File Provider、certificate resolver 或外部证书控制器。
本地联调先使用一套受控开发 CA。可以用 mkcert 生成证书;它是独立工具,不是 Traefik 功能。安装并信任本地 CA 后执行:
mkcert -install
mkcert \
-cert-file ./gateway/traefik/certs/app.localhost.crt \
-key-file ./gateway/traefik/certs/app.localhost.key \
app.localhost gateway.localhost成功时会生成叶证书和私钥。私钥目录必须限制为当前用户和 Traefik 运行身份可读,不能提交仓库。CI、Java、Firefox 和移动设备可能不使用宿主系统信任库,需要分别导入开发根证书。
创建 gateway/traefik/dynamic/tls.yaml:
tls:
certificates:
- certFile: /etc/traefik/certs/app.localhost.crt
keyFile: /etc/traefik/certs/app.localhost.key再把 HTTP 重定向和 HTTPS router 写入 routes.yaml:
http:
routers:
app-http:
entryPoints: [web]
rule: Host(`app.localhost`)
middlewares: [redirect-https]
service: noop@internal
app-https:
entryPoints: [websecure]
rule: Host(`app.localhost`)
tls: {}
service: app-v1@docker
middlewares:
redirect-https:
redirectScheme:
scheme: https
permanent: trueFile Provider 会观察目录变更。等待日志出现新配置后复验:
docker compose logs --since=2m traefik
curl -I http://app.localhost/
curl -v https://app.localhost/
openssl s_client -connect app.localhost:443 -servername app.localhost </dev/nullHTTP 应重定向到 HTTPS,HTTPS 应在不使用 -k 的情况下返回 version=v1。出现 Traefik 默认自签名证书,说明 SNI 没有匹配已加载证书,或 File Provider 读取文件失败;出现证书链错误,说明客户端没有信任签发 CA。官方的文件证书结构与 SNI 选择规则见 TLS certificates。
公网证书需要 resolver 与 router 同时配置
共享公网环境可以在启动配置中定义 ACME resolver:
certificatesResolvers:
public:
acme:
email: ops@example.invalid
storage: /var/lib/traefik/acme.json
httpChallenge:
entryPoint: webCompose 还要为 /var/lib/traefik 挂载持久卷,并限制其中账户私钥和证书材料的读取权限。定义 resolver 不会自动让任何 router 使用它;目标 router 必须同时启用 TLS 并引用 resolver:
http:
routers:
app-public:
entryPoints: [websecure]
rule: Host(`app.example.com`)
tls:
certResolver: public
service: app-v1@dockerHTTP-01 要求 CA 能通过公网 80 端口访问 challenge,TLS-ALPN-01 要求公网 443 可达,DNS-01 则需要受限的 DNS API 凭证。签发失败时查看 ACME 日志、DNS 和端口证据,不能临时关闭 TLS 校验。Traefik ACME resolver
Traefik OSS 的 ACME 文件存储不是分布式证书状态。多个实例不能因为挂载了同名 acme.json 就被视为证书高可用;共享文件还会引入并发和损坏风险。多实例入口应把证书签发交给具备一致性保证的外部控制器、边缘 LB 或相应平台能力,再由每个实例加载证书。配置回滚也不应覆盖较新的 ACME 状态。
Dashboard 只能通过受保护的 router 访问
Dashboard 会读取 API 展示 routers、services、middlewares、错误和依赖关系,这些信息足以暴露内部拓扑。不要启用 api.insecure,也不要把 8080 管理端口发布到公网。
先创建 BasicAuth users 文件,示例中的账号和密码只用于本机实验:
htpasswd -nB gateway-admin > ./gateway/traefik/secrets/dashboard-users然后通过 File Provider 建立 router:
http:
routers:
dashboard:
entryPoints: [websecure]
rule: Host(`gateway.localhost`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))
service: api@internal
middlewares: [dashboard-auth]
tls: {}
middlewares:
dashboard-auth:
basicAuth:
usersFile: /run/secrets/dashboard-users访问入口必须带 /dashboard/ 尾斜杠;router 规则还必须覆盖 /api,否则页面能打开但数据请求失败。API 与 Dashboard 安全配置
curl -I https://gateway.localhost/dashboard/
curl -u gateway-admin https://gateway.localhost/api/rawdata \
-o ./gateway/traefik/rawdata.before.json未认证请求应得到 401。认证请求应保存当前动态对象、错误和依赖关系。密码、token、私钥和可直接使用的 BasicAuth 哈希不应放在 labels 中,因为 docker inspect、Compose 渲染结果和管理 API 都可能暴露它们。
让 v2 进入独立 service
在 Compose 中增加 app-v2,保留独立名称和健康状态:
app-v2:
image: nginx:1.30.3-alpine
command:
- /bin/sh
- -ec
- |
printf 'version=v2\n' > /usr/share/nginx/html/index.html
printf 'live\n' > /usr/share/nginx/html/livez
printf 'ready\n' > /usr/share/nginx/html/readyz
exec nginx -g 'daemon off;'
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1/livez >/dev/null || exit 1"]
interval: 5s
timeout: 2s
retries: 2
labels:
traefik.enable: "true"
traefik.http.services.app-v2.loadbalancer.server.port: "80"
traefik.http.services.app-v2.loadbalancer.healthcheck.path: "/readyz"
traefik.http.services.app-v2.loadbalancer.healthcheck.interval: "5s"
traefik.http.services.app-v2.loadbalancer.healthcheck.timeout: "2s"
traefik.http.services.app-v2.loadbalancer.healthcheck.status: "200"
traefik.http.routers.app-v2-provider-sink.rule: "Host(`provider-sink.invalid`)"
traefik.http.routers.app-v2-provider-sink.entrypoints: "provider-sink"
traefik.http.routers.app-v2-provider-sink.service: "app-v2"
networks: [gateway]docker compose up -d app-v2
docker compose ps
curl -u gateway-admin https://gateway.localhost/api/http/servicesAPI 输出中应同时存在 app-v1@docker 和 app-v2@docker,并显示各自 server。没有 v2 时先检查 traefik.enable、容器健康状态、目标网络和 Provider 日志。
三种健康实验不能互相替代
Docker 事件决定对象是否存在
停止 v1:
docker compose stop app-v1
docker compose logs --since=1m traefik
curl -u gateway-admin https://gateway.localhost/api/rawdata \
-o ./gateway/traefik/rawdata.app-v1-stopped.jsonDocker Provider 收到 stop 事件后,app-v1@docker 应从生成配置中消失,引用它的 router 会留下依赖错误。这证明的是事件收敛,不是 HTTP health check。重新启动后对象应再次出现:
docker compose start app-v1
docker compose psTraefik 主动检查决定 server 是否接流量
保持容器和 Docker health 为正常,只删除 readiness 文件:
docker compose exec app-v1 rm /usr/share/nginx/html/readyz
docker compose exec app-v1 wget -qO- http://127.0.0.1/livez
sleep 7
curl -u gateway-admin https://gateway.localhost/api/http/services/app-v1@docker/livez 仍成功,说明容器存活;Traefik 对 /readyz 得到 404,应把 server 标记为不可用。恢复文件后等待下一轮检查:
docker compose exec app-v1 sh -c "printf 'ready\n' > /usr/share/nginx/html/readyz"
sleep 7
curl -u gateway-admin https://gateway.localhost/api/http/services/app-v1@docker状态应恢复。Traefik 默认把 health check 的 2xx、3xx 或显式 status 视为成功;路径、Host、scheme、interval、timeout 和重定向策略必须与应用 readiness 契约一致。Service health check
Docker HEALTHCHECK 决定 Provider 是否接纳容器
这次保留 /readyz,删除 /livez:
docker compose exec app-v1 rm /usr/share/nginx/html/livez
sleep 12
docker compose ps
curl -u gateway-admin https://gateway.localhost/api/rawdata \
-o ./gateway/traefik/rawdata.app-v1-unhealthy.jsonCompose 应把 app-v1 标记为 unhealthy。启动配置已经显式启用 allowEmptyServices: true,因此 app-v1@docker 保留为一个空 service,直接路由到它时返回 503,而不是让对象从 Provider 中消失。这个稳定对象身份是后面 weighted 健康传播的前提。恢复后等待 Docker health 重新变为 healthy:
docker compose exec app-v1 sh -c "printf 'live\n' > /usr/share/nginx/html/livez"
docker compose ps这三个实验分别回答“对象是否存在”“Traefik 是否认为上游可接流量”“Docker 是否认为容器健康”。只有逐项留证,才能判断请求为什么从 200 变成 404、502 或 503。
weighted service 必须放在支持它的 Provider
组合型 weighted service 在 Traefik OSS 中由 File Provider或 IngressRoute Provider 定义,不能假定任意 Docker label 都能创建。它分配的是多个 service 的权重,与单个 load balancer 内多个 server 的选择不是同一层。
创建 gateway/traefik/dynamic/canary.yaml:
http:
services:
app-weighted:
weighted:
healthCheck: {}
services:
- name: app-v1@docker
weight: 9
- name: app-v2@docker
weight: 1把 app-https router 的 service 改为 app-weighted@file。父级 healthCheck: {} 让子 service 的健康变化向上传播;子 service 自身也必须配置健康检查,否则组合 service 创建会失败。支持边界与传播规则见 Weighted Round Robin。
for i in $(seq 1 40); do curl -sS https://app.localhost/; done \
| sort | uniq -c输出应以 v1 为主并包含 v2。权重是长期流量倾向,不保证 40 次请求恰好 36:4;上线判断应结合版本维度的请求数、错误率和延迟。每个版本必须有独立 service 名和 access log 字段,否则出现问题时无法确认请求落在哪一版。
保持 v1 容器运行,只删除 readiness 文件。Docker Provider 对象仍存在,而主动健康检查把 v1 server 移出可用集合;父级 weighted service 应停止选择 v1,只把新请求交给健康的 v2:
docker compose exec app-v1 rm /usr/share/nginx/html/readyz
sleep 7
for i in $(seq 1 20); do curl -fsS https://app.localhost/; done
docker compose exec app-v1 sh -c "printf 'ready\n' > /usr/share/nginx/html/readyz"
sleep 720 次响应都应为 version=v2。如果父 service 整体变成 503,检查父级 healthCheck: {} 与两个子 service 的主动健康检查是否同时存在;如果仍有请求命中 v1,检查 Provider rawdata 中 v1 server 的健康状态。docker compose stop app-v1 会让单容器 service 从 Docker Provider 消失,父 service 的固定引用可能整体失效;需要容器消失时仍保持子 service 身份,应把稳定 service 定义放到 File、Kubernetes CRD 等不会随单个容器事件消失的 Provider。
中间件按 router 中声明的顺序执行。StripPrefix 后再做签名验证、先限流再鉴权、先重试再写入都会改变语义。retry 只能覆盖明确可安全重放的请求;非幂等写请求需要业务幂等键。单实例 rate limit 也不能自动成为多实例全局配额,跨实例总量需要共享状态或上层限流能力。
回退候选版本只修改一个事实源:从 app-weighted 删除 v2 子 service,或让 router 直接重新引用 app-v1@docker。不要依赖权重 0 表达停流,除非目标版本已经明确支持并完成过行为验证。变更后保存 API rawdata,并连续请求确认不再出现 version=v2。
HTTPS upstream 使用 ServersTransport 建立信任
当 app-v2 改为 HTTPS 并由企业 CA 为 api.internal.example 签发证书时,Docker service 需要使用 HTTPS scheme,File Provider 则保存 CA 与 SNI 规则:
# app-v2 labels
traefik.http.services.app-v2.loadbalancer.server.scheme: "https"
traefik.http.services.app-v2.loadbalancer.serverstransport: "app-tls@file"# dynamic/upstream-tls.yaml
http:
serversTransports:
app-tls:
serverName: api.internal.example
rootCAs:
- /etc/traefik/certs/upstream-ca.pem从 Traefik 所在网络验证证书链和名称:
docker run --rm --network gateway \
-v "$PWD/gateway/traefik/certs/upstream-ca.pem:/ca.pem:ro" \
alpine:3.23 sh -ec \
'apk add --no-cache openssl >/dev/null &&
openssl s_client -connect api.internal.example:443 \
-servername api.internal.example -CAfile /ca.pem </dev/null'预期为 Verify return code: 0 (ok)。出现 x509: certificate signed by unknown authority 时检查 CA 文件及挂载;出现名称错误时检查证书 SAN 和 serverName。insecureSkipVerify 会让中间人证书也被接受,不能作为生产修复。ServersTransport 的连接池、超时和客户端证书还会影响容量与 mTLS,应与应用端配置一起压测。
用受限 socket proxy 替换直接挂载
Docker socket 即使只读挂载,也允许客户端调用 socket 暴露的 API。Traefik 被入侵后,攻击者至少可能枚举容器、网络、环境和 labels;若代理错误开放写 API,还可能创建高权限容器。更稳妥的结构是让独立 socket proxy 挂载 socket,只向 Traefik 开放发现所需的 GET 和事件接口。
下面以 docker-socket-proxy v0.4.2 的权限模型表示所需能力。升级时先检查 项目官方 release 和镜像仓库,再记录实际 digest并重新核对环境变量对应的 Docker API 路径:
services:
socket-proxy:
image: tecnativa/docker-socket-proxy:v0.4.2
environment:
CONTAINERS: "1"
EVENTS: "1"
NETWORKS: "1"
PING: "1"
VERSION: "1"
POST: "0"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks: [docker-api]
security_opt:
- no-new-privileges:true
traefik:
# 删除 docker.sock 挂载
networks: [gateway, docker-api]
networks:
docker-api:
internal: true把启动配置中的 endpoint 改为内部代理:
providers:
docker:
endpoint: tcp://socket-proxy:2375
exposedByDefault: false
network: gateway
constraints: Label(`com.example.ingress`, `true`)每个允许发现的应用还要增加 com.example.ingress=true。constraints 只决定哪些容器进入发现结果,不替代 traefik.enable=true,也不限制入选容器可以通过 labels 声明哪类 router、middleware 或 service;对象类型仍要由 CI 策略和部署权限约束。socket proxy 不发布宿主端口,docker-api 网络只允许 Traefik 和 proxy 加入;开启 POST、容器创建或 exec 等接口会重新扩大控制面。
docker compose up -d --force-recreate socket-proxy traefik
docker compose logs --since=2m socket-proxy traefik
curl -fsS https://app.localhost/成功证据是 Traefik 仍能收到容器事件并生成允许的 service,而未带约束标签的容器不会出现。更高隔离要求可把 Docker API 放到独立主机,通过 SSH 或 mTLS endpoint 访问;这又引入主机身份、客户端证书轮换、失效和网络 ACL,不能只把 URL 从 Unix socket 换成 TCP。
分层回滚静态配置、动态文件和 labels
热更新并不意味着所有变更使用同一种回滚动作。发布前保存四类证据:
BACKUP_ID=$(date -u +%Y%m%dT%H%M%SZ)
BACKUP_DIR="$PWD/.traefik-backups/$BACKUP_ID"
mkdir -p "$BACKUP_DIR"
cp ./gateway/traefik/traefik.yaml "$BACKUP_DIR/traefik.yaml"
tar -czf "$BACKUP_DIR/dynamic.tgz" \
-C ./gateway/traefik dynamic
cp ./compose.yaml "$BACKUP_DIR/compose.yaml"
docker compose config > "$BACKUP_DIR/compose.rendered.yaml"
curl -u gateway-admin https://gateway.localhost/api/rawdata \
-o "$BACKUP_DIR/rawdata.json"
OLD_TRAEFIK_IMAGE=$(docker image inspect traefik:v3.7.1 \
--format '{{index .RepoDigests 0}}')
printf '%s\n' "$OLD_TRAEFIK_IMAGE" > "$BACKUP_DIR/image.txt"
printf '%s\n' "$BACKUP_DIR" > ./.traefik-last-backup同一轮的六类证据全部进入唯一 ${BACKUP_ID} 目录,后续恢复先读取 BACKUP_DIR=$(cat ./.traefik-last-backup),不能把不同轮次的 previous 文件混在一起。
动态文件错误先恢复对应文件,再观察 File Provider 日志和 API 是否收敛:
BACKUP_DIR=$(cat ./.traefik-last-backup)
BACKUP_ID=$(basename "$BACKUP_DIR")
RESTORE_ID=$(date -u +%Y%m%dT%H%M%SZ)
mv ./gateway/traefik/dynamic \
"./gateway/traefik/dynamic.failed.${BACKUP_ID}.${RESTORE_ID}"
tar -xzf "$BACKUP_DIR/dynamic.tgz" \
-C ./gateway/traefik
docker compose up -d --force-recreate traefik
docker compose logs --since=1m traefik
curl -fsS https://app.localhost/先移走当前目录再解压,才能删除备份后新增的错误文件。bind mount 仍可能指向被移动目录的旧 inode,因此解压后必须重建 Traefik,让新容器挂载恢复后的目录;随后在 rawdata 中确认新增 router、middleware 和 service 已消失。直接覆盖解压会留下多余对象,只移动目录而不重建容器则可能继续读取旧内容。
Docker labels 属于容器定义,恢复 Compose 后需要重新创建相关服务:
BACKUP_DIR=$(cat ./.traefik-last-backup)
OLD_TRAEFIK_IMAGE=$(cat "$BACKUP_DIR/image.txt")
docker pull "$OLD_TRAEFIK_IMAGE"
TRAEFIK_IMAGE="$OLD_TRAEFIK_IMAGE" \
docker compose --project-directory "$PWD" -f "$BACKUP_DIR/compose.yaml" \
up -d --force-recreate app-v1 app-v2
docker compose logs --since=1m traefikentryPoint、Provider endpoint、API 和 resolver 属于启动配置,恢复旧文件后必须重建或重启 Traefik:
BACKUP_DIR=$(cat ./.traefik-last-backup)
cp "$BACKUP_DIR/traefik.yaml" ./gateway/traefik/traefik.yaml
OLD_TRAEFIK_IMAGE=$(cat "$BACKUP_DIR/image.txt")
docker pull "$OLD_TRAEFIK_IMAGE"
TRAEFIK_IMAGE="$OLD_TRAEFIK_IMAGE" \
docker compose --project-directory "$PWD" -f "$BACKUP_DIR/compose.yaml" \
up -d --force-recreate traefik app-v1 app-v2
docker compose ps
curl -fsS https://app.localhost/回滚 Compose 的 image 通过 TRAEFIK_IMAGE 直接引用已记录 digest,实际恢复的是旧镜像内容,不是继续使用会移动的 v3 tag。ACME 存储和业务数据卷不随配置回滚覆盖;证书状态回退可能重新引入旧账户密钥或过期材料。
每次恢复都应保存 api/rawdata、Provider 日志和连续请求结果。路由恢复的证据不是“Compose 能解析”,而是目标 router、middleware、service 和 server 已在运行 API 中出现,v1 请求恢复,v2 流量归零,TLS 链仍通过验证,WebSocket 或长轮询也完成真实往返。
把动态入口变成团队可维护能力
能修改 labels、File Provider 目录或 Docker API 的主体都拥有不同程度的入口执行权。Docker constraints 只筛容器,不能限制入选容器声明的动态对象;仓库评审、label 策略、部署权限、宿主机权限和 Dashboard 访问必须一起建模。数据库、缓存和管理组件不加入 ingress 网络;应用如果还连接数据网络,应避免让入口容器同时加入数据网络。
路由冲突要用实际 priority 和匹配规则解释,不能依赖文件顺序。CORS 只保留一个权威层:Traefik Headers middleware 直接响应预检后,应用日志不会出现 OPTIONS;网关和应用同时写 CORS 头会产生冲突和缓存误判。WebSocket 要验证握手后的真实消息往返,单看 101 还不足以证明长连接稳定。
长期保留的证据应包括:Traefik 版本和镜像 digest、渲染后的 Compose、静态配置、各 Provider 生成的最终对象、证书链、三类健康实验、灰度版本指标、socket proxy 拒绝日志以及三种配置层的回滚结果。动态发现只有同时做到可观察、可限制和可恢复,才真正减少联调成本。
