Traefik 一直 404:labels 全对,问题出在健康检查
新服务部署完,Traefik 怎么都不给它路由:域名访问返回 404 page not found,而同一台机器上其他服务全部正常。
从怀疑隧道查到怀疑 provider 配置,折腾了一个多小时。真正的原因是我给容器写了一条错误的健康检查命令。
一、症状
走域名进来是 404:
curl -s -o /dev/null -w "%{http_code}\n" -H "Host: myapp.example.com" \ http://192.168.1.10:8888/api/v1/something# 404直连容器端口是正常的:
curl -s -o /dev/null -w "%{http_code}\n" http://192.168.1.10:8130/api/v1/something# 401 ← 路径对,只是没带 API Key同一个后端,两条路径,一个通一个不通。
二、错的排查顺序:从外到内查了六轮
每一步的判断都是对的,只是顺序错了,六轮下来一个多小时。
先排掉最外面那一层
一、怀疑 Cloudflare 隧道配错了。返回的 404 page not found 是纯文本、没有 HTML,这是 Go 程序的默认 404,而 Traefik 就是 Go 写的。说明请求已经到了 Traefik,只是没匹配上路由。隧道是通的。
再往里,labels、网络、入口点、provider
二、怀疑 labels 没生效:
docker inspect <容器名> --format '{{json .Config.Labels}}' \ | python3 -m json.tool | grep -i traefiktraefik.enable=true、rule、entrypoints、service 全在。labels 没问题。
三、怀疑容器不在 Traefik 的网络上:
docker inspect <容器名> \ --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}'traefik-net 在里面。网络没问题。
四、怀疑入口点名字对不上:
docker inspect <proxy容器> --format '{{json .Config.Cmd}}' | python3 -m json.tool--entrypoints.web-cf.address=:8888 确实存在。入口点没问题。
五、怀疑 provider 上挂了 constraints。完整启动参数里只有 exposedbydefault=false 和 providers.docker.network,一条 constraint 都没有。也没问题。
六、重启 Traefik 强制重新扫描。还是 404。
三、直接问 Traefik 注册了什么
不再猜,去拿它自己的 rawdata:
docker exec <proxy容器> wget -qO- http://localhost:8080/api/rawdata 2>/dev/null \ | python3 -c "import sys,json;d=json.load(sys.stdin);print(json.dumps({k:v for k,v in d.get('routers',{}).items() if 'myapp' in k},indent=2))"# {}空的。 不是「注册了但报错」,是根本没看见这个容器。
再查容器状态:
docker inspect <容器名> --format 'State={{.State.Status}} Health={{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}'# State=running Health=unhealthyrunning 但 unhealthy。Traefik 的 docker provider 会跳过健康检查失败的容器。
服务其实活得好好的,直连端口返回 401 就是证明。只是我给它写的探针命令在那个镜像里不成立。
四、原因:一条跑不通的探针
给一个 nginx 前端容器写了这条健康检查:
healthcheck: test: ["CMD-SHELL", "wget -qO- http://localhost/ >/dev/null || exit 1"]看着无害,实际在那个镜像里跑不通——可能是 busybox 版 wget 的行为不同,也可能是根路径本身不返回 200。总之探针永远失败,容器永远 unhealthy。
改法是删掉这段。官方 compose 本来就没给这个服务配健康检查,是我按「每个服务都要有 healthcheck」的习惯自己加上去的,而这个习惯写进过在 NAS 上用 Coolify 部署的十条约定。
五、三个教训
没把握的探针不如不写
一个永远失败的健康检查比没有健康检查危害大得多:它不报警,却会让 Traefik 跳过路由,还会让 depends_on: condition: service_healthy 的下游服务永远卡在 Created。
这两种症状都不指向健康检查本身,这是它难查的地方。
不同镜像里的工具不一样
alpine 系有 wget 没 curl,distroless 两个都没有,nginx 官方镜像有 curl 但未必是你要的行为。加探针之前先进容器验一次:
docker exec <容器名> wget -qO- http://127.0.0.1:<端口>/docker exec <容器名> curl -sf http://127.0.0.1:<端口>/跑得通再往 compose 里写。
排查顺序应该反过来
我从最外层的隧道查到最内层的 provider 配置,但成本最低、信息量最大的两条命令其实是:
# Traefik 到底注册了哪些 routerdocker exec <proxy容器> wget -qO- http://localhost:8080/api/rawdata
# 容器自己是什么状态docker ps --format '{{.Names}}\t{{.Status}}'第二条尤其便宜——docker ps 的 Status 列直接写着 (unhealthy)。我看了很多遍那个输出,注意力全在 Up 上,没看括号里的字。
六、一份更实用的排查清单
路由不通时按这个顺序,从便宜到贵:
# 1. 容器在跑吗?健康吗?(注意看 Up 后面的括号)docker ps --format '{{.Names}}\t{{.Status}}' | grep <关键词>
# 2. 直连容器端口通吗?(绕开所有网关)curl -s -o /dev/null -w "%{http_code}\n" http://192.168.1.10:<映射端口>/
# 3. Traefik 注册这条路由了吗?docker exec <proxy容器> wget -qO- http://localhost:8080/api/http/routers 2>/dev/null \ | python3 -c "import sys,json;[print(r.get('name'),'|',r.get('status'),'|',r.get('error','')) for r in json.load(sys.stdin)]"
# 4. 打 Traefik 入口,带 Host 头(绕开 DNS 和隧道)curl -s -o /dev/null -w "%{http_code}\n" -H "Host: myapp.example.com" http://192.168.1.10:8888/
# 5. 以上都对,再去查 labels、网络、入口点名字第 1 步和第 2 步加起来不到十秒,能排掉一大半情况。
注意第 3 步列出来的 router 是按字母排序的,用 head -40 之类截断很容易把要找的那条切掉——我也在这儿浪费了一轮。
七、为什么这个坑藏得深
根因很小,但症状(404)和原因(探针写错)之间隔着两层不相关的东西:Traefik 的服务发现,和 Docker 的健康状态。
换一句话说:labels 写对了,只保证「规则没错」,不保证「Traefik 看得见这个容器」。这两件事之间还夹着一道健康检查。
Traefik 跳过 unhealthy 容器这个行为是合理的,它不想把流量导给一个自己声称不健康的后端。问题在于「不健康」这个信号是我自己伪造的。
同一套网关此前还踩过五层别的问题,分层验证的顺序写在自建 Traefik 网关:HTTPS 打不开的完整排查路径里,这次的教训正好补上那份清单最前面缺的一步。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!


