signal online Asaqe Lee --:--:-- UTC reading mode
02 Asaqe.
System Record / Infrastructure Stack 4 min read ghost-edge-stale-upstream-502

Ghost 502 不是博客挂了,是 nginx 把容器 IP 解析死了

nginx 把容器名解析死在启动时,Ghost 一重建就会 502。这篇记录根因、修复和防再发。

Ghost 502 不是博客挂了,是 nginx 把容器 IP 解析死了

2026-08-21 12:58 之后,me.asaqe.orgasaqe.orgblog.asaqe.site 同时 502。Cloudflare 报 error code: 502,本机 curl 127.0.0.1:2368 拿到的是 nginx 的 502 HTML。看起来像博客宕机,其实 Ghost 进程一直在跑。

这篇文章记录一次完整的误判路径:先怀疑容器挂了,再发现是边缘代理把容器名解析死在了旧 IP 上。

症状

  • 外网三个域名都是 Cloudflare 502
  • 源站 ghost-edge(nginx:1.27,映射 0.0.0.0:2368)日志:connect() failed (111: Connection refused) while connecting to upstream ... http://172.23.0.27:2368/
  • Ghost 容器状态 Up,直连当前 IP 172.23.0.12:2368 返回 301/200
  • MySQL mysqladmin ping 正常,磁盘和内存都不是瓶颈

502 从 Ghost 重建那一秒开始,连续打了大约 37 小时,直到把 ghost-edge 重启。

时间线

  1. 07-30 ghost-edge 启动。nginx 在加载配置时把 ghost 解析成 172.23.0.27,并缓存这个结果。
  2. 08-21 12:58 Ghost 容器被重建(镜像 ghost:latest,启动后版本 6.59.0),新地址 172.23.0.12。旧地址立即拒绝连接。
  3. 08-21 12:58:33 边缘日志出现第一笔 Connection refused。同一时刻 Ghost 自己去拉 ActivityPub,也撞上这层 502。
  4. 08-23 01:50 重启 ghost-edge,解析回到 172.23.0.12,首页、文章、/ghost/ 恢复 200。

根因

nginx 的 proxy_pass http://ghost:2368; 如果主机名写死、不用变量,只会在 启动 / 加载配置 时向 Docker 内置 DNS 127.0.0.11 问一次。之后容器重建、IP 变更,nginx 不会再问。

Docker 里这是旧问题,个人博客这种「一个长期活着的反代 + 一个会升级的应用容器」特别容易中招。容器健康检查全绿,对外却是 502,就是这种错位。

修复

应急:

docker restart ghost-edge

防再发:让 proxy_pass 走变量,并显式使用 Docker DNS。

resolver 127.0.0.11 valid=10s ipv6=off;

location / {
  set $ghost_upstream ghost:2368;
  proxy_pass http://$ghost_upstream;
}

变量形式会在请求时重新解析。valid=10s 把缓存压到十秒量级,够用,也不会每请求都打 DNS。

同一台机器上的 Nginx Proxy Manager 也有一处写死的 proxy_pass http://new-api:3000;,已改成和它自己 proxy.conf 一样的 $forward_scheme://$server:$port$request_uri。当时 new-api 的 IP 碰巧没变,所以 API 没倒;不改的话,下次 new-api 重建就会把 /v1/ 打挂。

顺手清掉的同类故障

排查博客时把整机 Docker 扫了一遍,另外几处也不是「应用逻辑坏了」,而是运行时状态漂了:

  • pgAdmin:数据目录属主 1000,容器用户 5050,启动时 chmod 0600 失败,restart: always 空转约 18 万次。处理:chown -R 5050:5050 后重启。
  • 若干 compose 应用(stream-vault、syncthing、qwen2api、tavily、AutoTeam、Kiro-Go):项目网络被删,旧容器还挂着 *_default,状态是 Created / Exited。处理:按现有 compose 重建,接到 1panel-network
  • MySQL compose:挂载了主机 /etc/timezone,而这台机器上它是目录不是文件。当前容器能跑,下次 1Panel 重建会起不来。处理:去掉该挂载。

这些和 Ghost 502 是同一类问题:进程在,拓扑不在。

验收

curl -sS -o /dev/null -w '%{http_code}\n' -H 'Host: me.asaqe.org' -H 'X-Forwarded-Proto: https' http://127.0.0.1:2368/
curl -sS -o /dev/null -w '%{http_code}\n' https://me.asaqe.org/
curl -sS -o /dev/null -w '%{http_code}\n' https://me.asaqe.org/ghost/

三条都应 200。边缘日志里不应再出现 172.23.0.27

回滚

如果动态解析有问题,把 proxy_pass 改回 http://ghost:2368 并重启 ghost-edge,行为回到「启动时解析一次」。那是旧行为,能恢复站点,但不能抵抗下一次 Ghost 重建。

以后升级 Ghost 的检查单

  1. 重建应用容器之后,立刻 curl 边缘端口,不要只看 docker ps
  2. 反代如果用容器名当上游,必须是变量 + Docker DNS,或和被代理容器一起重启。
  3. 1Panel / Watchtower 自动拉 :latest 时,默认假设「边缘还记得人」。这个假设不成立。

个人 AI 和自建博客一样:模型或 CMS 没挂,不等于链路可交付。可靠性出在解析、权限、网络这些不性感的层。