Docker 容器一直 Restarting 怎么排查:日志、退出码、Healthcheck 与 OOM 定位

Docker 容器反复进入 Restarting 状态,通常不是 Docker 本身“卡住了”,而是容器主进程退出后,重启策略又将它拉起。常见原因包括启动参数错误、依赖不可用、健康检查配置不当、进程被信号终止,以及容器或宿主机内存不足。

排查时不要急着删除并重建容器。删除操作可能丢失现场信息,甚至误删未持久化的数据。更有效的顺序是:

  1. 确认容器是否真的在重启;
  2. 保存日志、退出码和事件;
  3. 检查重启策略;
  4. 根据退出码定位进程退出原因;
  5. 分别检查 Healthcheck、OOM、挂载、权限及外部依赖;
  6. 修复后验证容器是否稳定运行。

一、先确认是“进程退出”,还是仅仅“不健康”

执行:

docker ps -a

典型状态可能是:

Restarting (1) 5 seconds ago

这表示容器主进程已经退出,Docker 正根据重启策略再次启动它。

如果显示的是:

Up 10 minutes (unhealthy)

则容器主进程仍在运行,只是健康检查失败。二者不是同一个问题。

需要特别注意:

在普通 Docker 或 Docker Compose 环境中,容器变成 unhealthy 并不会仅凭 Healthcheck 自动重启。

如果一个不健康的容器同时反复重启,通常还有其他原因:

  • 应用发现自身异常后主动退出;
  • 配置了外部自动修复工具;
  • 容器由 Swarm 等编排系统管理;
  • 主进程崩溃,Healthcheck 只是更早暴露了问题;
  • 运维脚本或监控平台正在执行重启。

二、第一时间保存现场信息

先指定容器名称或 ID:

c=my-container

1. 查看完整运行状态

docker inspect -f \
'status={{.State.Status}} running={{.State.Running}} restarting={{.State.Restarting}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} error={{printf "%q" .State.Error}} started={{.State.StartedAt}} finished={{.State.FinishedAt}} restarts={{.RestartCount}}' \
"$c"

重点关注:

  • status:当前状态;
  • exit:最近一次退出码;
  • oom:是否被记录为 OOMKilled;
  • error:Docker 启动容器时遇到的错误;
  • restarts:该容器的累计重启次数;
  • started 与 finished:每次是否只运行了几秒。

如果容器被 Docker Compose 重新创建过,旧容器的 RestartCount 不会自动继承。因此还要结合容器 ID、创建时间和事件判断。

2. 查看应用日志

docker logs --tail 200 --timestamps "$c"

持续观察:

docker logs -f --tail 200 --timestamps "$c"

只查看最近 30 分钟:

docker logs --since 30m --timestamps "$c"

Docker Compose 服务可使用:

docker compose logs --tail 200 --timestamps <服务名>

重点搜索以下信息:

  • permission denied
  • connection refused
  • timeout
  • no such file or directory
  • address already in use
  • out of memory
  • killed
  • segmentation fault
  • 数据库认证失败
  • 配置文件语法错误
  • 证书过期或主机名不匹配

如果 docker logs 没有内容,不代表应用没有报错。还要检查日志驱动:

docker inspect -f '{{.HostConfig.LogConfig.Type}}' "$c"

应用也可能将日志写入容器内文件或挂载目录,而不是标准输出。此时需要检查镜像配置、挂载路径和应用日志目录。

3. 查看 Docker 事件

Docker 事件能够补充日志中没有记录的信息:

docker events \
  --since 30m \
  --filter type=container \
  --filter container="$c"

常见事件包括:

  • start
  • die
  • restart
  • oom
  • health_status: unhealthy
  • kill

如果需要单独检查近期 OOM 事件:

docker events \
  --since 1h \
  --filter type=container \
  --filter event=oom

docker events 默认持续等待新事件,可以按 Ctrl+C 结束。

三、先看重启策略,判断为什么会被反复拉起

查看容器的重启策略:

docker inspect -f \
'policy={{.HostConfig.RestartPolicy.Name}} max_retry={{.HostConfig.RestartPolicy.MaximumRetryCount}}' \
"$c"

常见策略如下:

重启策略 行为
no 容器退出后不自动重启
always 主进程退出后自动重启
unless-stopped 除非被人工停止,否则自动重启
on-failure 仅在退出码非 0 时重启
on-failure:N 非 0 退出时重启,最多尝试 N 次

例如,应用正常执行完任务并以退出码 0 结束,但容器配置了 restart: always,也会形成持续重启。这类问题不是应用崩溃,而是容器用途与重启策略不匹配。

查看 Docker Compose 最终生效配置:

docker compose config

不要只查看原始 Compose 文件,因为变量替换、多个配置文件合并和命令行参数可能改变最终结果。

临时停止重启循环

为了保留现场并避免日志被不断刷屏,可以先记录原重启策略,再临时关闭:

docker update --restart=no "$c"
docker stop -t 30 "$c"

完成排查后,再按实际需求恢复:

docker update --restart=unless-stopped "$c"

如果容器由 Docker Compose 管理,最终仍应修改 Compose 文件中的 restart 配置并重新部署,避免下次重建后恢复旧配置。

如果容器是 Docker Swarm 的任务实例,不应只修改底层容器,因为 Swarm 会重新创建任务。应改为检查服务:

docker service ps --no-trunc <服务名>
docker service logs --timestamps <服务名>
docker service inspect <服务名>

四、根据退出码缩小排查范围

查询最近一次退出码:

docker inspect -f '{{.State.ExitCode}}' "$c"

常见退出码及含义如下:

退出码 常见含义 优先检查
0 进程正常结束 是否误用了 always;程序是否本来就是一次性任务
1 通用应用错误 应用日志、配置文件、环境变量、依赖服务
2 命令参数或程序使用错误 启动参数、命令行选项、脚本语法
126 命令存在但不能执行 执行权限、挂载选项、文件格式、架构
127 找不到命令 ENTRYPOINT、CMD、PATH、镜像内容
137 通常表示收到 SIGKILL OOM、人工 docker kill、宿主机强制终止
139 通常为段错误 SIGSEGV 原生程序崩溃、动态库、架构或硬件问题
143 通常表示收到 SIGTERM 正常停止、编排平台滚动更新、超时终止

Linux 中,退出码大于 128 时,常见计算方式是:

退出码 = 128 + 信号编号

例如:

  • 137 = 128 + 9,对应 SIGKILL;
  • 143 = 128 + 15,对应 SIGTERM。

但退出码只能作为线索,不能单独作为结论。特别是:

退出码 137 不等于一定发生了 OOM。

人工执行 docker kill、宿主机脚本发送 SIGKILL,也可能得到 137。必须结合 .State.OOMKilled、Docker 事件和内核日志确认。

五、检查实际启动命令和入口脚本

镜像默认命令、Compose 覆盖项和运行参数可能共同决定最终启动方式。

查看容器实际配置:

docker inspect -f 'path={{.Path}} args={{json .Args}}' "$c"

查看镜像层面的入口和命令:

docker inspect -f \
'entrypoint={{json .Config.Entrypoint}} cmd={{json .Config.Cmd}}' \
"$c"

常见问题包括:

1. 启动脚本不存在

日志可能出现:

exec: "/app/start.sh": stat /app/start.sh: no such file or directory

检查:

  • Dockerfile 是否正确 COPY;
  • .dockerignore 是否意外排除了脚本;
  • 卷挂载是否覆盖了镜像中的 /app;
  • 文件路径和大小写是否正确。

2. 脚本没有执行权限

典型日志:

permission denied

Dockerfile 中应显式设置权限:

COPY start.sh /usr/local/bin/start.sh
RUN chmod +x /usr/local/bin/start.sh

也可以使用支持该语法的构建器:

COPY --chmod=755 start.sh /usr/local/bin/start.sh

3. Windows 换行符导致脚本无法运行

脚本使用 CRLF 时,可能出现:

/bin/sh^M: bad interpreter

应将脚本转换为 LF,并在版本库中统一换行规则。

4. 卷挂载覆盖镜像文件

例如镜像内已有 /app/start.sh,但运行时挂载了空目录:

volumes:
  - ./app:/app

此时镜像原有 /app 内容会被挂载目录遮盖。使用以下命令检查挂载:

docker inspect -f '{{json .Mounts}}' "$c"

如已安装 jq,可以更清晰地显示:

docker inspect "$c" | jq '.[0].Mounts'

5. 镜像与宿主机架构不兼容

常见错误包括:

exec format error

检查宿主机架构:

uname -m

检查镜像信息:

docker image inspect <镜像名> \
  -f 'os={{.Os}} arch={{.Architecture}}'

多架构镜像还可以查看清单:

docker buildx imagetools inspect <镜像名>

六、Healthcheck 失败怎么排查

1. 查看健康状态和失败输出

docker inspect -f '{{json .State.Health}}' "$c"

如有 jq:

docker inspect "$c" | jq '.[0].State.Health'

只查看每次检查的时间、退出码和输出:

docker inspect -f \
'{{range .State.Health.Log}}{{println .Start "exit=" .ExitCode .Output}}{{end}}' \
"$c"

查看健康检查配置:

docker inspect -f '{{json .Config.Healthcheck}}' "$c"

Healthcheck 的结果通常为:

  • 0:健康;
  • 1:不健康;
  • 2:保留值,不应作为普通检查结果使用。

2. 在容器内手工执行同一条命令

如果容器能维持运行一段时间,可以执行:

docker exec "$c" sh -c '<健康检查命令>'
echo $?

例如:

docker exec "$c" sh -c 'curl -fsS http://127.0.0.1:8080/health'

但不能默认镜像里存在 sh、curl 或 wget。精简镜像、distroless 镜像可能没有这些工具。应根据镜像实际内容设计检查命令,或者让应用自身提供轻量的健康检查程序。

3. 常见 Healthcheck 配置错误

检查工具根本没有安装

例如配置使用了:

HEALTHCHECK CMD curl -f http://localhost:8080/health || exit 1

但镜像中没有 curl,检查会持续失败。

检查地址或端口错误

容器内的 localhost 指向容器自身,不是宿主机,也不是其他容器。

如果要检查同一 Compose 网络中的其他服务,应使用服务名,例如:

http://api:8080/health

不过健康检查通常应该反映当前容器自身是否可服务,不宜把所有外部依赖都设为强制条件,否则数据库短暂抖动可能导致整条服务链都变成不健康。

启动时间不足

应用需要较长时间加载数据或执行迁移时,应设置合理的启动宽限期:

services:
  app:
    healthcheck:
      test: ["CMD", "app-healthcheck"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 60s

各参数作用:

  • interval:检查间隔;
  • timeout:单次检查超时时间;
  • retries:连续失败次数;
  • start_period:启动宽限期。

不要仅为了让状态变绿而无限增大 retries 或 start_period。如果应用始终无法就绪,仍应修复应用或依赖问题。

CMD 与 CMD-SHELL 使用错误

Exec 形式不会自动处理管道、变量和 ||:

test: ["CMD", "curl", "-f", "http://127.0.0.1:8080/health"]

需要 shell 语法时应使用:

test:
  - CMD-SHELL
  - curl -fsS http://127.0.0.1:8080/health || exit 1

前提是镜像中确实存在相应 shell。

七、退出码 137 或 OOMKilled 的处理方法

1. 检查 Docker 是否记录了 OOM

docker inspect -f \
'oom={{.State.OOMKilled}} exit={{.State.ExitCode}} error={{printf "%q" .State.Error}}' \
"$c"

查看容器内存限制:

docker inspect -f \
'memory={{.HostConfig.Memory}} memory_swap={{.HostConfig.MemorySwap}}' \
"$c"

其中 memory 通常以字节表示;值为 0 一般表示未通过该项设置显式内存上限。

实时查看资源使用:

docker stats "$c"

重点关注:

  • MEM USAGE / LIMIT
  • 内存是否持续增长;
  • 重启前是否逼近限制;
  • CPU 是否持续满载;
  • 进程数是否异常增加。

由于重启发生很快,人工观察可能来不及。生产环境最好通过监控系统持续采集容器内存、工作集、重启次数和 OOM 事件。

2. 检查宿主机内核日志

即使 .State.OOMKilled 没有给出明确结果,也要检查宿主机是否发生系统级 OOM。

使用 systemd 的 Linux:

sudo journalctl -k --since "1 hour ago" |
  grep -Ei 'oom|out of memory|killed process'

或者:

sudo dmesg -T |
  grep -Ei 'oom|out of memory|killed process'

可能看到:

Out of memory: Killed process ...
Memory cgroup out of memory: Killed process ...

需要区分:

  • 容器达到 cgroup 内存限制:通常只影响该容器或其中的进程;
  • 宿主机整体内存耗尽:可能随机杀死某个高内存进程,影响范围更大;
  • 应用主动申请超大内存后崩溃:日志中可能出现运行时自己的内存错误。

3. 不要只靠提高内存限制

临时提高限制可用于验证,但不是最终修复方案:

docker update --memory 2g "$c"

使用 Compose 时,应写入服务配置,例如:

services:
  app:
    mem_limit: 2g

随后验证最终配置:

docker compose config
docker compose up -d

更重要的是定位内存为什么增长:

  • JVM 最大堆设置是否接近甚至超过容器限制;
  • Node.js 堆上限是否合理;
  • PHP-FPM、Gunicorn、数据库等进程数是否过多;
  • 单次请求是否读取了超大文件;
  • 是否存在缓存无上限、队列堆积或内存泄漏;
  • 应用堆之外是否还有直接内存、线程栈、共享内存和本地库开销。

例如,容器限制为 1 GiB 时,不应直接把应用堆也设置为 1 GiB。还必须为运行时、线程栈、本地内存和系统库保留空间。

八、日志正常但仍然退出时,还要检查这些项目

1. 环境变量与配置文件

查看容器环境变量:

docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' "$c"

注意输出中可能包含密码、令牌和连接串,不要直接粘贴到工单或公开聊天中。

重点检查:

  • 必填变量是否为空;
  • 变量名是否拼错;
  • 布尔值和数字格式是否正确;
  • 数据库地址是否仍写成 localhost;
  • 配置文件是否被挂载到错误路径;
  • 密钥或证书文件权限是否正确。

2. 外部依赖不可用

常见依赖包括:

  • 数据库;
  • Redis;
  • 消息队列;
  • DNS;
  • 对象存储;
  • 第三方 API;
  • 证书和时间同步服务。

典型错误是把其他容器写成:

127.0.0.1:3306

在容器中,127.0.0.1 只表示当前容器。Compose 网络内通常应使用服务名:

mysql:3306

depends_on 可以帮助控制启动顺序,但不能保证依赖服务永远可用。应用仍应实现连接重试、超时和退避机制。

3. 端口或 Unix Socket 冲突

如果日志出现:

address already in use

检查应用是否重复启动多个实例,或者旧进程、PID 文件、Socket 文件未清理。

宿主机端口映射冲突通常会让容器启动直接失败,可查看:

docker ps -a
docker inspect -f '{{printf "%q" .State.Error}}' "$c"

4. 磁盘空间或 inode 用尽

df -h
df -i
docker system df

磁盘空间不足可能导致:

  • 应用无法写日志;
  • 数据库无法写入;
  • 临时文件创建失败;
  • Docker 无法创建容器层;
  • 日志驱动异常。

不要在未确认影响范围时直接执行:

docker system prune -a

该操作可能删除仍有用途的未运行容器、网络、构建缓存和镜像。应先使用 docker system df 确认占用,再有针对性地清理。

5. 文件权限或只读文件系统

检查挂载及只读设置:

docker inspect "$c" | jq '.[0] | {
  ReadonlyRootfs: .HostConfig.ReadonlyRootfs,
  Mounts: .Mounts
}'

还要确认容器内运行用户:

docker inspect -f 'user={{printf "%q" .Config.User}}' "$c"

应用以非 root 用户运行时,挂载目录必须允许对应 UID/GID 读写。不要为了快速解决问题就长期使用 chmod 777 或直接改为 root,应修正目录属主和最小必要权限。

九、容器重启太快,无法进入内部怎么办

docker exec 只能对正在运行的容器使用。对于启动后立即退出的容器,可以基于同一镜像启动临时调试容器,并覆盖入口:

docker run --rm -it \
  --entrypoint /bin/sh \
  <镜像名>

前提是镜像中存在 /bin/sh。如果没有 shell,可以:

  • 使用镜像自带的调试命令;
  • 重新构建临时调试镜像;
  • 使用相同网络和必要挂载启动专门的工具容器;
  • 从停止的容器中复制文件进行分析。

例如复制配置或崩溃文件:

docker cp "$c":/app/logs ./container-logs

查看容器可写层发生过哪些变化:

docker diff "$c"

输出标识包括:

  • A:新增;
  • C:修改;
  • D:删除。

启动调试容器时不要无条件复制生产环境的全部密钥、数据卷和网络权限。应仅添加定位问题所需的最小配置。

十、修复后的验证标准

修改配置或镜像后,不要只看到一次 Up 就认为问题已经解决。至少完成以下验证。

1. 确认状态稳定

docker ps --filter name="$c"

等待一段时间后再次检查:

docker inspect -f \
'status={{.State.Status}} health={{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}} restarts={{.RestartCount}} started={{.State.StartedAt}}' \
"$c"

2. 确认重启次数不再增加

连续执行两次:

docker inspect -f '{{.RestartCount}}' "$c"

如果容器被重新创建,应同时记录新容器 ID:

docker inspect -f 'id={{.Id}} created={{.Created}} restarts={{.RestartCount}}' "$c"

3. 检查近期日志

docker logs --since 10m --timestamps "$c"

确认没有重复的初始化、连接失败、迁移失败或内存告警。

4. 检查健康状态

docker inspect -f \
'{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}' \
"$c"

5. 进行实际业务验证

健康检查成功不代表所有功能正常,还应验证:

  • 核心接口是否返回正确结果;
  • 数据库读写是否正常;
  • 消息是否能够消费;
  • 重启后数据是否保留;
  • 优雅停止是否生效;
  • 内存是否在合理区间内保持稳定。

十一、快速判断路径

遇到 Docker 容器一直重启时,可以按下面的顺序快速定位:

  1. 退出码是 0
    检查是否配置了 always 或 unless-stopped,以及该程序是否本来就是一次性任务。

  2. 退出码是 1 或 2
    优先查看应用日志、配置、启动参数和外部依赖。

  3. 退出码是 126 或 127
    检查入口命令、文件路径、执行权限、换行符、PATH 和卷挂载覆盖。

  4. 退出码是 137
    同时检查 OOMKilled、Docker oom 事件、内核日志和人工强制终止记录。

  5. 退出码是 139
    检查原生程序崩溃、动态库、镜像架构,并保留 core dump 进行分析。

  6. 状态为 unhealthy,但进程仍在运行
    检查 Healthcheck 命令、工具是否存在、端口、超时和启动宽限期;不要默认认为 Docker 会自动重启。

  7. 没有应用日志
    检查日志驱动、应用日志文件、Docker 事件以及宿主机内核和 Docker 服务日志。

  8. 修复后仍周期性复发
    增加持续监控,重点观察内存、磁盘、依赖延迟、重启次数和 OOM 事件,不要只依赖某一次人工检查。

Docker 的 Restarting 只是结果,不是根因。最有价值的证据通常是最近一次退出码、退出前日志、Docker 事件、Healthcheck 输出以及宿主机内核记录。按照这一顺序保留现场并逐层缩小范围,通常可以避免反复重建容器却始终找不到真正问题。

⚡ 极客核心要点提炼 可供 AI 智能体与搜索引擎引用检索

本文主题:Docker 容器一直 Restarting 怎么排查:日志、退出码、Healthcheck 与 OOM 定位

引用出处:https://isoziyuan.com/p/100108/(作者:Isoziyuan · 发布于爱搜资源网)