AI 生成的 Compose 为何冷启动失败
文章通过全新数据卷上的三次冷启动,复现编码 Agent 生成的 Compose 配置中的数据库就绪竞态。重点指出 depends_on 只保证启动顺序,并比较常见修补方式与有效修正。
AI 深度解读
AI 写出的 Docker Compose 文件,可能在本机测试通过,换到 CI 或同事电脑却连不上 Postgres。作者追到的原因很具体:旧教程里的启动竞态被复制进了生成配置,连常见的 healthcheck 补丁也可能误报就绪。
这篇值得读的地方,是把「容器启动了」「探针通过了」「应用能连接了」拆成三件事,再用冷启动日志验证它们的差距。对经常让 AI 搭建本地开发环境的程序员,这比多记一份 YAML 模板有用。
为什么本机通过,第一次部署却失败
Docker Compose 用一份 YAML 定义和运行多个容器。问题通常从短格式的 depends_on 开始:app 依赖 db,Compose 就先启动数据库容器,再启动应用容器。
但启动顺序不等于服务就绪。
Postgres 数据目录为空时,入口脚本要执行 initdb、启动临时服务器、运行 /docker-entrypoint-initdb.d/ 里的初始化脚本,最后才启动正式服务器,并让它成为 PID 1。应用如果在这个过程中连接 db:5432,就可能撞上 CONNECTION-REFUSED。
已经初始化过的数据卷会跳过前面的准备工作,时间差大幅缩小。因此,作者认为常见的验证方式漏掉了真正危险的条件:第一次启动。
作者在 2026-10-29 使用 Docker Engine 29.1.3、Docker Compose v5.1.4 做了三次新数据卷冷启动,应用是 alpine:3.20,数据库是 postgres:16-alpine。测试应用每隔 0.5 秒探测一次端口,最多尝试 30 次。
最初配置里,前四次连接失败,第五次成功,约有两秒的拒绝连接窗口。数据库日志中的就绪时间是 08:10:28,应用此前已经开始请求。
测试通过只能证明那次启动成功,不能证明首次初始化也成功。
restart: unless-stopped 还可能遮住问题:应用不断崩溃重启,直到数据库准备完成。最后所有容器都运行着,但启动竞态依然存在。
为什么加了 healthcheck,应用还是连不上
把依赖改成 condition: service_healthy,方向是对的。它会让 Compose 等数据库健康检查通过,再启动应用。
问题在于,健康检查可能测错了连接路径。
常见探针是 pg_isready -U postgres。没有指定 host 时,它检查的是 Unix socket。官方 Postgres 镜像在首次初始化期间启动的临时服务器,恰好能通过这个 socket 回应;应用访问的却是 TCP 地址 db:5432。
两边问的不是同一个问题。
作者记录到,数据库在启动后 +0.78 秒进入 starting,+3.02 秒变成 healthy,正式服务器直到 +7.06 秒才接受 TCP 连接。健康状态提前了约四秒,应用仍然连续七次连接失败,第八次才成功。
下面这段探针日志解释了误报的来源:
08:13:52.4 exit 0 /var/run/postgresql:5432 - accepting connections
08:13:54.5 exit 0 /var/run/postgresql:5432 - accepting connections
08:13:56.5 exit 2 /var/run/postgresql:5432 - no response
要看的是 /var/run/postgresql:5432:前两次成功来自 socket 上的临时服务器;临时服务器关闭后,第三次探测又失败了。

所以,不能只检查 YAML 有没有 healthcheck,还要检查它证明了什么。service_healthy 的等待机制正常工作,也可能因为探针选错而提前放行。
三处配置修改,分别解决什么问题
作者给出的完整修正如下,用同一份配置展示依赖等待、TCP 探测和连接结果:
services:
app:
image: alpine:3.20
depends_on:
db:
condition: service_healthy
restart: true
command: >
sh -c 'i=0;
while [ $$i -lt 30 ]; do
i=$$((i+1));
if nc -z -w 1 db 5432 2>/dev/null; then
echo "ATTEMPT $$i: CONNECTED";
exit 0;
else
echo "ATTEMPT $$i: CONNECTION-REFUSED";
fi;
sleep 0.5;
done;
echo "GAVE UP"'
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: example
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U postgres"]
interval: 2s
timeout: 3s
retries: 10
start_period: 10s
volumes:
pgdata:
最关键的一行是探针中的 -h 127.0.0.1:它强制使用 TCP,避免临时初始化服务器通过 socket 提前回答。
另外两处各有职责。condition: service_healthy 控制首次启动等待;依赖项下的 restart: true 用于 Compose 显式重启依赖的场景,作者以 docker compose restart db 为例。它和应用服务自身的 restart: unless-stopped 不是同一个配置,也不能理解成数据库以后每次异常退出,Compose 都会自动重新执行整套等待流程。
时间预算同样需要配合。这里每 2s 探测一次,单次超时 3s,允许 10 次失败,并设置 10s 的 start_period,给初始化留出宽限。作者的数据库冷启动约需七秒,因此选择十秒,并建议共享 CI runner 留更多余量。
修正后的测试结果是 ATTEMPT 1: CONNECTED。不过示例里的应用只是用 nc -z -w 1 db 5432 检查端口,成功后就退出,并没有验证真实业务查询。
启动等待负责让应用晚一点出发,应用重试负责处理运行期间的断连。
两者要同时保留。
我的判断
这篇最可靠的部分,是作者把短格式依赖、socket 探针和 TCP 就绪之间的差异落到了日志上。对依赖 Postgres 的 Node 服务,这是一条直接可用的排查路径:先确认测的是否是应用实际使用的连接方式,再讨论重试次数。
但实验范围需要看清。作者只报告了同一台主机上的三次冷启动,没有覆盖不同磁盘、不同初始化脚本或更多镜像版本。原文还说第三次运行加入了 sleep 4 的初始化脚本,后文又把修正输出称为 run 2,运行编号和配置对应关系交代得不够清楚。日志能说明机制,不能据此把四秒当成普遍延迟。
关于 AI 的解释则要更克制。作者引用 arXiv:2506.20495,讨论模型即使能访问当前文档,仍会生成过时 API;又引用 arXiv:2511.21022,称 GPT-3.5 的 API 预测中有 37.4% 已弃用。
这些结果支持「模型可能沿用旧知识」,却不能直接证明 Compose 错误来自哪批训练教程。原文没有给出编码模型、提示词及多轮生成对照,把问题归因于「2021 年语料形态」仍属于推断。
作者建议提供当前文档和明确要求,这个做法合理;但「模型不会反对贴进去的规范」说得太满。文档可以改善输入,生成结果仍需审核。端口就绪也只是一层信号,实际 Node 服务还可能需要确认认证、数据库和迁移状态。
能拿来做什么
- 让 AI 生成 Postgres 开发环境时,明确要求
condition: service_healthy和 TCP 探针,并逐项审核依赖条件、-h 127.0.0.1、时间预算及依赖重启配置,别只检查有没有 healthcheck。 - 合并前,在可丢弃的测试项目里执行
docker compose down -v && docker compose up --wait,覆盖新数据卷首次初始化。down -v会删除相关数据卷,不能直接用于要保留数据的环境;--wait的结果也依赖探针是否测对。 - 给 Node 数据库连接保留带退避的重试,并记录失败次数和最终结果。Compose 的启动等待不能替应用处理运行一小时后发生的断连。
- 将验证条件写进 AI 任务要求:除了生成 YAML,还要说明数据卷是否全新、使用什么连接路径、如何判定成功,让「测试过了」有明确边界。