> ## Content Index
> Fetch the complete content index at: https://isoziyuan.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Docker 容器一直 Restarting 怎么排查：日志、退出码、Healthcheck 与 OOM 定位
- URL: https://isoziyuan.com/p/100108/
- Published: 2026-08-28T00:04:04.000Z
- Updated: 2026-08-28T00:04:03.000Z
- Author: Isoziyuan
- Tags: Docker, 故障排查, 容器运维, 性能优化

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

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

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

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

执行：

```bash
docker ps -a

```

典型状态可能是：

```text
Restarting (1) 5 seconds ago

```

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

如果显示的是：

```text
Up 10 minutes (unhealthy)

```

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

需要特别注意：

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

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

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

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

先指定容器名称或 ID：

```bash
c=my-container

```

### 1\. 查看完整运行状态

```bash
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\. 查看应用日志

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

```

持续观察：

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

```

只查看最近 30 分钟：

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

```

Docker Compose 服务可使用：

```bash
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` 没有内容，不代表应用没有报错。还要检查日志驱动：

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

```

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

### 3\. 查看 Docker 事件

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

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

```

常见事件包括：

- `start`
- `die`
- `restart`
- `oom`
- `health_status: unhealthy`
- `kill`

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

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

```

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

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

查看容器的重启策略：

```bash
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 最终生效配置：

```bash
docker compose config

```

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

### 临时停止重启循环

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

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

```

完成排查后，再按实际需求恢复：

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

```

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

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

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

```

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

查询最近一次退出码：

```bash
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 时，常见计算方式是：

```text
退出码 = 128 + 信号编号

```

例如：

- `137 = 128 + 9`，对应 `SIGKILL`；
- `143 = 128 + 15`，对应 `SIGTERM`。

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

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

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

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

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

查看容器实际配置：

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

```

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

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

```

常见问题包括：

### 1\. 启动脚本不存在

日志可能出现：

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

```

检查：

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

### 2\. 脚本没有执行权限

典型日志：

```text
permission denied

```

Dockerfile 中应显式设置权限：

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

```

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

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

```

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

脚本使用 CRLF 时，可能出现：

```text
/bin/sh^M: bad interpreter

```

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

### 4\. 卷挂载覆盖镜像文件

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

```yaml
volumes:
  - ./app:/app

```

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

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

```

如已安装 `jq`，可以更清晰地显示：

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

```

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

常见错误包括：

```text
exec format error

```

检查宿主机架构：

```bash
uname -m

```

检查镜像信息：

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

```

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

```bash
docker buildx imagetools inspect <镜像名>

```

## 六、Healthcheck 失败怎么排查

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

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

```

如有 `jq`：

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

```

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

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

```

查看健康检查配置：

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

```

Healthcheck 的结果通常为：

- `0`：健康；
- `1`：不健康；
- `2`：保留值，不应作为普通检查结果使用。

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

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

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

```

例如：

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

```

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

### 3\. 常见 Healthcheck 配置错误

#### 检查工具根本没有安装

例如配置使用了：

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

```

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

#### 检查地址或端口错误

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

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

```text
http://api:8080/health

```

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

#### 启动时间不足

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

```yaml
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 形式不会自动处理管道、变量和 `||`：

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

```

需要 shell 语法时应使用：

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

```

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

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

### 1\. 检查 Docker 是否记录了 OOM

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

```

查看容器内存限制：

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

```

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

实时查看资源使用：

```bash
docker stats "$c"

```

重点关注：

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

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

### 2\. 检查宿主机内核日志

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

使用 systemd 的 Linux：

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

```

或者：

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

```

可能看到：

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

```

需要区分：

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

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

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

```bash
docker update --memory 2g "$c"

```

使用 Compose 时，应写入服务配置，例如：

```yaml
services:
  app:
    mem_limit: 2g

```

随后验证最终配置：

```bash
docker compose config
docker compose up -d

```

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

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

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

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

### 1\. 环境变量与配置文件

查看容器环境变量：

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

```

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

重点检查：

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

### 2\. 外部依赖不可用

常见依赖包括：

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

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

```text
127.0.0.1:3306

```

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

```text
mysql:3306

```

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

### 3\. 端口或 Unix Socket 冲突

如果日志出现：

```text
address already in use

```

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

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

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

```

### 4\. 磁盘空间或 inode 用尽

```bash
df -h
df -i
docker system df

```

磁盘空间不足可能导致：

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

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

```bash
docker system prune -a

```

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

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

检查挂载及只读设置：

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

```

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

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

```

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

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

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

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

```

前提是镜像中存在 `/bin/sh`。如果没有 shell，可以：

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

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

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

```

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

```bash
docker diff "$c"

```

输出标识包括：

- `A`：新增；
- `C`：修改；
- `D`：删除。

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

## 十、修复后的验证标准

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

### 1\. 确认状态稳定

```bash
docker ps --filter name="$c"

```

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

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

```

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

连续执行两次：

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

```

如果容器被重新创建，应同时记录新容器 ID：

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

```

### 3\. 检查近期日志

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

```

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

### 4\. 检查健康状态

```bash
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 输出以及宿主机内核记录。按照这一顺序保留现场并逐层缩小范围，通常可以避免反复重建容器却始终找不到真正问题。