直觉跳进去的那条路
当时 NAS 上已经跑了不少容器:Gitea、金融日报后台、Python 教程站、健康评估工具、还有一两个实验性质的前端页面。每个服务一个端口,8080、8765、9092、9095,越来越多,完全超出了能记住的范围。需要一个统一的入口。
选 nginx 做反向代理是几乎不用过脑子的决定。一台机器上做反向代理,nginx 就是那个最自然的选择:定义一个 upstream,写一条 proxy_pass,重启,通。我以为一个小时内连配置带验证就结束了。
配了,不通。再调,还不通。
第一条规则写上去:proxy_pass http://gitea:3000,reload nginx,浏览器里 502。
当时的第一反应是「是不是 upstream 名字写错了」。查了 compose 文件,确认了容器名,确认了端口,重新 reload——还是 502。
然后开始怀疑 location 匹配顺序。加了 rewrite,换了 location /gitea/ 改成正则,加了 trailing slash,去了 trailing slash。每次都 reload,每次都 502。
折腾了将近两个小时,中间短暂地想过「是不是 nginx 根本连不到容器」,但这个念头一闪就过去了。docker ps 显示所有容器都在跑,端口也能正常访问,下意识里就觉得「网络应该没问题」。
开始怀疑 nginx 本身
当 nginx 反复返回 502 而 docker 端口映射一切正常时,注意力自然地从「配置」滑到了「软件」。
检查 nginx 版本是不是有 bug;看 error log 找线索;确认 nginx 用户有没有权限;甚至查了 SELinux 和 apparmor 的状态。一条条排,一条条都不对。
比较荒唐的是,中间真的认真考虑过换掉 nginx。Caddy 的自动 HTTPS 看起来很干净,Traefik 跟 Docker 的集成很顺滑。但每次想放弃的时候,都会有一个声音说:「反向代理是最基础的功能,怎么可能搞不定」,然后又回到 error log 里翻。
偶然发现真正的线索
忘了是排查什么的间隙,随手跑了一个 docker exec nginx ping gitea。
返回的不是 timeout,也不是 network unreachable。返回的是 Name or service not known.——DNS 解析失败。
这不是网络不通,这是 nginx 容器根本不知道「gitea」这个名字对应什么 IP。
顺着这条线查下去,很快发现了问题:容器内的 /etc/resolv.conf 文件权限被设成了 600。正常应该是 644,容器内的 DNS 客户端需要读取这个文件来知道 DNS 服务器地址。权限设成 600 之后,只有 root 能读,其他进程(包括 nginx worker)全部无法解析内部主机名。
修复和后续
临时解决很简单:docker exec -u root nginx chmod 644 /etc/resolv.conf。立刻通了。
但更重要的不是修好这件事本身,而是意识到两个问题:
第一,nginx 配置完全没有问题。那条最基础的 proxy_pass 从头到尾都是对的。真正阻断它的是一个跟 nginx 毫无关系的权限问题。当时我把所有精力和注意力都给了 nginx 配置,并不是因为那里的问题概率最大,而是因为那是我最熟悉的诊断方向。我习惯在「配置层」找答案,而不是下钻到「运行时环境」里排查。
第二,容器的网络故障排查需要一个固定的检查起点。后来我在部署新项目的检查清单里加了几条:容器间 hostname 能否互 ping、resolv.conf 权限是否正常、DNS 解析是否生效。不是每次都会出问题,但出问题的时候,先检查这几条,能省掉很多在配置层面的无效折腾。
这件事教给我的
这次排查的问题最后只改了三个字母(600 → 644),但花了整个下午。
真正消耗时间的是那几个转换方向太慢的瞬间:比如在 nginx 配置里转了十几圈才停下来想「是不是根本不是 nginx 的问题」;比如 ping 了一下才意识到是 DNS 崩了。这些判断卡点不是技术能力的问题,是在某个时刻没有主动从当前的假设里退出来。
好的复盘不是「第一步做什么第二步做什么」。好的复盘是记住你在哪个节点选错了方向,以及以后碰到类似情况怎么更快地意识到「可能不是我以为的那个层面」。