技术指南
Docker 启动失败排查:保留退出证据再重建
先冻结重启循环并保存状态、日志、事件与解析配置,再按创建、启动、运行和就绪四层定位 Docker 容器失败。
容器启动失败时,最容易丢失的不是运行时间,而是第一现场:实际应用的 Compose 模型、镜像身份、退出状态、时间窗日志、生命周期事件、挂载与端口。先暂停自动重启和人工重试,保存这些证据,再判断失败发生在创建、启动、运行还是就绪阶段。一次只改一个边界,修复后用同一组探针验收。
先定位 create、start、running 还是 ready
| 阶段 | 可观察证据 | 常见边界 |
|---|---|---|
| 配置解析 | docker compose config 的退出状态和解析模型 | 变量缺失、文件合并、路径或字段错误 |
| 创建 | CLI/daemon 错误、create/die 事件 | 镜像、端口、挂载、权限或运行参数 |
| 进程启动 | State.ExitCode、Error、FinishedAt、日志 | 入口命令、配置、依赖或信号 |
| 持续运行 | RestartCount、OOMKilled、事件和资源指标 | 崩溃循环、内存、磁盘或外部依赖 |
| 业务就绪 | Health、真实端口和应用请求 | 进程存活但尚未可服务 |
`docker ps` 默认只列运行中的容器,故障排查要保留停止实例。容器处于 running 只表示主进程尚未退出,不等于数据库迁移完成、依赖可用或应用能够处理真实请求;健康检查和外部探针必须回答不同层的问题。
用一个 UTC 时间窗保存第一现场
incident=$(date --utc +%Y%m%dT%H%M%SZ)
umask 077
mkdir "incident-$incident"
docker container ls --all --no-trunc >"incident-$incident/containers.txt"
docker container inspect APP >"incident-$incident/inspect.json"
docker container logs --timestamps --since 30m --until "$(date --utc --iso-8601=seconds)" APP \
>"incident-$incident/stdout.log" 2>"incident-$incident/logs.stderr"
docker system events --since 30m --until "$(date --utc --iso-8601=seconds)" \
--filter type=container --filter container=APP --format json \
>"incident-$incident/events.jsonl"State 字段要联合解释,不靠一个退出码猜根因
docker container inspect --format '{{json .State}}' APP
docker container inspect --format '{{json .HostConfig.RestartPolicy}}' APP
docker container inspect --format '{{json .Config.Healthcheck}}' APP
docker container inspect --format '{{json .NetworkSettings.Ports}}' APP
docker container inspect --format '{{json .Mounts}}' APP| 字段 | 能说明什么 | 不能单独证明 |
|---|---|---|
| ExitCode / Error | 主进程结果与 daemon 启动错误 | 应用内部根因 |
| OOMKilled | 该容器状态是否记录 OOM kill | 宿主所有内存压力来源 |
| FinishedAt | 与日志、事件对齐的退出时间 | 失败从何时开始 |
| RestartCount | 当前容器累计自动重启次数 | 每次退出原因都相同 |
| Health.Status / Log | 健康命令的最近结果 | 真实用户路径必然正常 |
137 常见于 SIGKILL 的 shell 风格结果,但不能脱离 `OOMKilled`、内核/daemon 日志、cgroup 内存事件和当时资源指标直接写成 OOM。143 通常对应 SIGTERM,同样要确认是谁发出信号、停止超时和应用是否完成优雅退出。125/126/127 等值也要结合 CLI 与镜像入口语义查证,不能维护一张脱离版本和上下文的万能退出码表。
先审阅 Compose 实际应用的模型
umask 077
docker compose --env-file .env.production config --format json \
> resolved-compose.json
docker compose --env-file .env.production config --images
docker compose --env-file .env.production config --hash '*'`docker compose config` 展示将提交给 Engine 的解析模型,可发现覆盖顺序、插值、相对路径和短语法展开问题。它不证明镜像内入口命令可用,也不证明目标端口、挂载权限或依赖已就绪,因此配置门禁通过后仍需运行时证据。
重启次数上升时先证明循环条件
- 把 RestartCount、die/start 事件和每次日志末尾按时间对齐。
- 确认当前 restart policy、最大尝试和停止来源;不要把无限重启当可用性。
- 若每次同样退出,继续重启只制造噪声;修正候选配置或镜像后再受控重建。
- 若退出原因变化,分别保存每轮证据,避免把后续端口冲突覆盖最初配置错误。
- 回滚使用已知镜像摘要和已审阅配置,不依赖会漂移的 `latest`。
日志必须带时间边界并与事件对齐
start='2026-08-16T20:00:00Z'
end='2026-08-16T20:10:00Z'
docker container logs --timestamps --since "$start" --until "$end" APP
docker system events --since "$start" --until "$end" \
--filter type=container --filter container=APP --format jsonDocker 官方 CLI 支持用 RFC 3339 或相对时间筛选日志,并可用 JSON 格式保存事件。将 create、start、health_status、die 与应用日志对齐,才能区分应用主动退出、健康检查失败、daemon 操作和外部停止。
入口命令失败要复现参数和文件存在性
| 检查 | 证据 | 修复边界 |
|---|---|---|
| 镜像身份 | Image ID、RepoDigests、构建/部署版本 | 固定摘要后再比较 |
| 入口和参数 | Config.Entrypoint、Config.Cmd | 确认 exec/shell 形式与信号传递 |
| 文件与架构 | 候选镜像内路径、mode、CPU 架构 | 不要在失败容器可写层临时补文件 |
| 应用配置 | 脱敏配置校验命令及退出状态 | 用同一镜像做只读预检 |
`pull`、`up --force-recreate` 或在容器内手工修改会更换证据对象。先记录当前容器与镜像,再把修复放进版本化 Dockerfile、Compose 或配置来源;候选验证通过后重建,失败则能回到原摘要。
端口冲突、发布和监听是三个问题
docker container inspect --format '{{json .NetworkSettings.Ports}}' APP
ss --listening --tcp --numeric --process
# 只从宿主验证已发布的本地端口
curl --fail --silent --show-error --max-time 3 http://127.0.0.1:HOST_PORT/health- 先确认端口由哪个进程或容器占用,不先停止未知监听者。
- 用 inspect 区分 declared/exposed 与实际 published 端口。
- 在容器网络、宿主回环和真实入口分别探测,定位故障跨越哪一层。
- 同时检查 IPv4/IPv6、主机防火墙和反向代理,但不要用关闭防火墙作为诊断结论。
挂载问题先核对对象身份,再核对应用 UID
| 挂载证据 | 要确认 |
|---|---|
| Type / Source / Destination | 命名卷还是 daemon 主机上的 bind 路径 |
| RW / propagation | 容器是否应该写入及传播边界 |
| 宿主 inode 与 owner | 路径是否存在、是不是预期对象 |
| 容器应用 UID/GID | 实际进程身份是否具备最小读写权限 |
| SELinux/AppArmor | 拒绝是否来自强制访问控制 |
docker container inspect --format '{{json .Mounts}}' APP
namei --mountpoints --long /verified/host/path
getfacl --absolute-names /verified/host/path
# 服务能启动时,以应用身份验证指定路径
docker compose exec --user APP_UID:APP_GID SERVICE \
sh -c 'id; test -r /app/config.yaml; test -w /app/data'depends_on 只解决顺序时,应用仍可能未就绪
Compose 按依赖顺序创建服务,但短写法只保证依赖已经启动。需要 readiness 时,为依赖定义能验证实际服务的 healthcheck,并在 `depends_on` 使用 `condition: service_healthy`;应用本身仍应对短暂依赖失败做有界重试。健康命令必须能在镜像内运行,并设置 start_period、interval、timeout 与 retries。
services:
app:
depends_on:
db:
condition: service_healthy
db:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $POSTGRES_USER -d $POSTGRES_DB"]
start_period: 30s
interval: 10s
timeout: 5s
retries: 5“进程已监听”也不等于 ready。本批次隔离探针在同一进程监听端口时先返回 503,切换就绪后才返回 200。健康检查应覆盖最小关键依赖,外部探针再验证发布端口、反向代理、TLS 和代表性内容。
资源故障要同时看容器、cgroup 和宿主
| 信号 | 进一步证据 |
|---|---|
| OOMKilled=true | 容器限制、memory.events、daemon/内核日志、工作集 |
| ExitCode=137 | OOMKilled、停止来源、SIGKILL、超时和事件 |
| 写入失败 | df bytes、df inodes、配额、只读挂载和应用 UID |
| CPU/延迟上升 | docker stats 样本、宿主 load/steal、应用请求延迟 |
Docker 默认不会自动给容器设置 CPU/内存限制。先测量应用工作集和峰值,再设置硬限制与告警;不要为了阻止退出而禁用 OOM killer,也不要在生产机故意制造 OOM。磁盘容量、inode、日志增长、镜像层和卷需要分别核对。
候选修复一次只改变一个层
- 保存旧容器 inspect、镜像摘要、解析配置、挂载身份和回滚命令。
- 在隔离环境对候选运行配置校验、入口命令、应用 UID 路径探针和 healthcheck。
- 在维护窗口只重建目标服务;不使用会删除卷或全局资源的清理命令。
- 观察至少一个完整启动窗口,确认 RestartCount 不再增长且事件序列稳定。
- 从容器内、宿主回环和真实入口逐层验收状态、内容与依赖。
- 失败时恢复旧摘要和配置并重建,再验证数据与入口;保留失败候选证据。
隔离演练证明了先分层再修复
VPScope 没有安装或启动 Docker daemon,而是在一次性目录中验证底层证据边界:同一缺失配置连续三次都以 78 退出并留下相同 stderr,证明重启没有增加信息;第二个回环监听得到 EADDRINUSE 98;nobody 对 mode 0500 的 root 目录写入得到 EACCES 13;SIGTERM 对应 Python returncode -15 / shell 风格 143。
同一回环服务已经监听时,健康端点先返回 503,设置 ready 后才返回 200,停止后端口关闭。演练没有运行 Docker/Compose、拉镜像、访问 registry、挂载宿主目录、触碰 daemon socket、诱发 OOM 或使用生产数据,因此只能证明诊断顺序和采集边界,不能证明 Docker 的 OOMKilled、restart policy、bind mount 或发布规则行为。
恢复完成要有一组可反证的验收项
- 解析后的 Compose 模型、镜像摘要和运行参数与批准候选一致。
- 目标容器在完整观察窗内不再增加 RestartCount,State.Error 为空。
- 健康状态稳定,健康日志没有持续超时或假阳性。
- 应用日志时间窗没有重复启动、迁移失败或未处理异常。
- 挂载对象正确,应用 UID 的最小读写探针通过,原数据仍可读取。
- 目标端口只发布到需要的宿主地址,内部/回环/真实入口探针逐层通过。
- 内存、CPU、容量、inode 和日志增长保有余量,没有新的 OOM/资源事件。
- 回滚使用固定镜像摘要和旧配置演练过,不依赖删除卷或全局清理。
- 事故包已脱敏,记录根因、单一修复、证据缺口和复查触发。