Docker 排错最有效的顺序是:先看声明配置,再看对象状态,然后看日志,最后进入容器做针对性检查。不要一开始就删除重建,那会清掉最有价值的现场信息。

docker compose config
↓
docker compose ps -a
↓
docker compose logs <service>
↓
docker inspect <container>
↓
docker compose exec <service> <command>每一步回答一个问题:配置是否如预期、容器处于什么状态、进程说了什么、Docker 实际应用了什么参数、容器内部能否观察到依赖。
容器问题经常表现为同一句“访问不了”,但可能属于完全不同的层:
从声明和对象开始,能先排除范围大的错误;带着假设进入容器,能减少无目的修改。若一开始就安装工具、改权限或重建容器,现场会变化,错误反而更难复现。
至少记录失败命令、时间、完整错误、docker compose ps -a、相关服务的少量前后日志和最终配置。日志很长时先用 --since、--tail 缩小时间窗,再根据线索扩展。不要只截最后一行,因为根因常出现在连锁报错之前。
主进程正常完成通常返回 0,非零值表示程序选择报告失败。进程被信号终止时,常见 shell 约定会显示 128 + 信号编号,例如 137 常与 SIGKILL 有关,143 常与 SIGTERM 有关。但退出码只能提示方向,还要结合 Docker 状态字段确认。
$ docker inspect docker-task-api-api-1 \
--format 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}} error={{json .State.Error}} started={{.State.StartedAt}} finished={{.State.FinishedAt}}'OOMKilled=true 是内存限制或系统内存压力的重要证据;ExitCode=137 单独存在则还可能是人为 docker kill。不要根据一个数字直接下结论。
把 API 的地址临时改成错误端口:
environment:
REDIS_ADDR: redis:6399应用配置变化后重新创建 API:
$ docker compose up -d --force-recreate api
$ docker compose ps -a
NAME SERVICE STATUS
docker-task-api-api-1 api Up 20 seconds (unhealthy)
docker-task-api-redis-1 redis Up 2 minutes (healthy)Redis 健康、API 不健康,范围已经缩小到 API 自身或 API 到 Redis 的连接。
先做一次“错误类型预测”:名字 redis 没变、网络没变,只有端口从 6379 改成 6399,所以 DNS 应该仍然成功,而 TCP 连接会被拒绝或超时。带着这条预测检查日志,比看到 unhealthy 就重建整套应用更有效。
先看 API 日志:
$ docker compose logs --tail 20 api
api-1 | task-api listening on :8080; redis=redis:6399
api-1 | method=GET path=/health duration=1s再确认 Compose 解析后的环境变量:
$ docker compose config | grep -A2 REDIS_ADDR
REDIS_ADDR: redis:6399查看健康检查失败记录:
$ docker inspect docker-task-api-api-1 \
--format '{{range .State.Health.Log}}{{.ExitCode}} {{.Output}}{{end}}'
1 wget: server returned error: HTTP/1.1 503 Service Unavailable三处证据指向同一个错误端口。把 REDIS_ADDR 改回 redis:6379,重新创建 API:
$ docker compose up -d --force-recreate api
$ docker compose ps
NAME SERVICE STATUS
docker-task-api-api-1 api Up 8 seconds (healthy)
docker-task-api-redis-1 redis Up 3 minutes (healthy)修复后的验收要重放最初失败的业务路径,而不只是看绿色状态:
$ curl -i http://localhost:8080/health
HTTP/1.1 200 OK
...
{"status":"ok"}这完成了一条闭环:制造故障 → 缩小范围 → 用三处证据验证假设 → 修改唯一错误 → 重放失败请求。以后处理真实问题,也应记录这条因果链。
exec 在正在运行的容器中执行新进程。先确认 API 身份:
$ docker compose exec api id
uid=100(app) gid=101(app) groups=101(app)再检查服务名解析:
$ docker compose exec api getent hosts redis
172.21.0.2 redis redis身份不是 root,DNS 能解析 redis。如果名字解析失败,优先检查两个服务是否接入同一网络;不要把临时 IP 写进配置。
极简镜像可能没有 shell、curl 或 getent。不要为了排错长期把大量工具装进生产镜像。可以读取日志与 inspect 信息,或使用专门的临时诊断容器接入同一网络。
假设 API 镜像没有网络工具,可以启动一个用完即删的 Alpine 容器,加入 Compose 项目网络:
$ docker run --rm --network docker-task-api_default \
alpine:3.22 getent hosts redis
172.21.0.2 redis redis这能验证网络与 DNS,但要明确它不完全等同于 API 容器:诊断镜像的用户、证书、代理和文件系统都可能不同。它适合判断共享网络层,API 自身配置仍要通过 inspect 和应用日志验证。
Compose 也支持运行一次性服务进程:
$ docker compose run --rm --no-deps api id
uid=100(app) gid=101(app) groups=101(app)run 会按服务配置创建新容器,不是在现有容器里执行;exec 才是在已有运行容器中增加进程。排错时要分清你观察的是原现场还是新实例。
$ docker stats --no-stream \
docker-task-api-api-1 docker-task-api-redis-1
CONTAINER CPU % MEM USAGE / LIMIT NET I/O
docker-task-api-api-1 0.00% 4.8MiB / 7.7GiB 2.1kB / 1.7kB
docker-task-api-redis-1 0.11% 14.2MiB / 7.7GiB 1.8kB / 2.2kB数值会变化。stats 适合发现 CPU 持续满载或内存不断增长,但一次快照不能代替持续监控。
各列要结合趋势解释:
MEM USAGE / LIMIT 要看限制是否显式设置;没有业务级限制时,分母可能是引擎可用内存。NET I/O 与 BLOCK I/O 是累计量,单次变大不等于此刻仍然繁忙。PIDS 异常增长可能提示子进程泄漏或并发失控。资源问题还应与应用日志、请求延迟和 Docker 事件对齐。一个时间点的快照无法证明内存泄漏,至少需要连续样本。
Docker 会记录容器启动、停止、健康状态变化等事件。先理解用途:当“服务刚才短暂重启过”但当前已经恢复时,事件时间线能补足 ps 只显示当前状态的不足。
在一个终端监听项目事件:
$ docker events \
--filter type=container \
--filter label=com.docker.compose.project=docker-task-api另一个终端重启 API:
$ docker compose restart api事件输出会依次出现停止、退出、启动以及后续 health status 变化,时间戳对应真实发生顺序。按 Ctrl+C 结束监听。事件不是长期监控存储,但非常适合捕捉正在复现的生命周期问题。
如果给 Redis 盲目添加 cap_drop: [ALL],新数据卷可能无法完成权限准备,日志会出现:
Can't open or create append-only dir appendonlydir: Permission denied这不是“放弃安全”的理由,而是提醒我们:安全限制要结合镜像入口脚本和写入路径。API 可以移除全部权能;Redis 官方镜像启动时需要准备 /data,本课程保留它的默认权能,同时不发布 Redis 端口,并启用 no-new-privileges。
看到 Permission denied 时依次问:进程数字 UID/GID 是什么、目标路径由谁拥有、挂载是否只读、根文件系统是否只读、Linux capability 是否相关、MAC 策略是否阻止访问。直接改成 root 会掩盖是哪条边界不匹配,也可能扩大攻击面。
$ docker compose exec redis id
$ docker inspect docker-task-api-redis-1 \
--format 'readonly={{.HostConfig.ReadonlyRootfs}} mounts={{json .Mounts}}'先读取,不改动;确认是卷所有权初始化确实需要镜像入口逻辑后,再设计最小例外。
先用 compose ps -a 保留退出对象,再看退出码和第一段日志。若日志是“文件不存在”,检查镜像内容和工作目录;若是“permission denied”,检查用户与挂载;若退出码为 0,主进程可能本来就是一次性命令。
先确认服务进程和健康状态,再看 docker compose port api 8080 的实际发布地址。容器内服务必须监听 0.0.0.0:8080 或合适接口;若只监听容器自己的 127.0.0.1,端口转发到容器网卡时仍可能无法访问。
先确认当前项目名和卷名,再检查 .Mounts 中目标路径。常见原因是换了 Compose 项目名创建了新卷、把卷挂到错误目录,或执行了 down --volumes。不要在未确认旧卷前向新卷反复写入。
config:变量是否插值成预期值,端口和卷是否正确。ps -a:状态是 created、running、exited、restarting 还是 unhealthy。logs:先看错误发生前后的少量日志,再扩大范围。inspect:检查退出码、健康记录、挂载、网络和运行用户。exec:只验证已经形成的假设,不进行无目的浏览。最后给每次排错留下一个简短记录:现象、影响范围、关键证据、根因、唯一修改、回归结果。这会把一次救火变成下一次可以复用的知识。
成熟的排错不是命令多,而是每条命令都在验证一个明确假设;证据与预测不一致时,更新假设,而不是继续堆修改。