部署环境的第一次启动失败了:服务起不来,日志里数据库连接指向一个只在我们某位同事本地存在过的容器主机名。代码里没有这个字符串,部署脚本没有,配置模板也没有——排查从这里开始。
时间线
| 阶段 | 观察 | 结论 |
|---|---|---|
| 现象 | 服务启动失败,连接错误指向陌生主机名 | 先当网络问题处理 |
| 假设一 | 网络/防火墙不通 | 排除:同网络其他服务正常 |
| 假设二 | 部署脚本传参有误 | 排除:脚本传的是正确主机 |
| 探针 | 打印运行时环境变量与最终配置 | 运行时读到的不是我们传的值 |
| 定位 | 干净环境只启动镜像,逐层打印 | 基础镜像烘焙了 ENV |
| 验证 | 清掉那个变量重跑 | 服务正常启动,根因确认 |
关键的探针命令只有一条——不信任任何"应该",直接打印运行时看到的东西:
# 用与生产相同的镜像,在干净环境里看它到底看到了什么
docker run --rm <image> sh -c 'printenv | grep -iE "db|host|postgres"'
输出里那个陌生的主机名,就是全部答案。
根因:被"已设置"绕过的默认值
问题出在入口脚本的写法。我们(和大多数模板)习惯这样:
# 修复前:未设置才用默认值
DB_HOST="${DB_HOST:-saas-db}"
在正常环境完全正确。但基础镜像构建时烘焙了一组 ENV(含一个内部主机名),对容器里的进程来说,这些变量永远"已设置"——:- 默认值从未生效,我们的显式传参也排在被烘焙的值之后。脚本没有一行日志提示"你传的值被忽略了",一切静默地走向错误的主机。
修复很直接——不再依赖"未设置则默认"的假设,需要什么就显式导出什么:
# 修复后:显式覆盖,并加启动自检
export DB_HOST="${DB_HOST:?DB_HOST is required}"
export DB_PORT="${DB_PORT:?DB_PORT is required}"
# 启动自检:关键依赖不可达就响亮地失败,不静默降级
为什么排查花了时间
三个因素叠加:
- 报错指向错误的方向:连接失败看起来像网络/防火墙,第一反应是查网络;
- "本地是好的":本地开发用另一套入口方式,恰好绕过了烘焙变量——本地永远无法复现部署环境的假设;
- 静默优先:入口脚本的写法在正常环境无可指摘,只在被劫持时静默选错。
留下的两道防线
修复是一次性的,防线是长期的:
- 启动自检:服务启动时主动验证关键依赖(数据库可达、必需配置非空),失败明确报错退出——让"配置错误"和"服务故障"在日志里是两种东西;
- 干净环境回归:部署验收必须包含"干净 clone + 非默认凭据"的一次完整启动。本地开发环境永远覆盖不了部署环境的假设,只有干净环境可以。
可复用的结论
- 容器时代最贵的一课往往和代码无关:你以为的环境,可能只是镜像作者以为的环境;
- 凡是入口脚本里的"未设置则默认",都值得问一句——如果它永远"已设置"呢?
- 排查环境类问题时,先打印事实(运行时环境、最终配置),再提出假设——顺序反过来,时间就花在验证错误的假设上。
