这是运维里最常见、也最容易被「看日志」误导的故障:容器状态一直是 Restarting,但 docker logs 要么没输出、要么只有一段启动成功的日志。本文给一条固定的排查路径,四步走完基本能定位。
先记住一个判断:日志为空不等于容器没启动。很多启动失败发生在容器内进程还没写任何输出之前(权限、路径、依赖缺失),这时候日志必然是空的,继续看日志会浪费大量时间。
第一步:看 exit code,它比日志信息量大
docker ps -a 的 STATUS 列里,Restarting (1) 5 seconds ago 括号中的数字就是 exit code。
# 看容器的退出码与重启次数
docker inspect <容器名> --format '{{.State.ExitCode}} {{.RestartCount}} {{.State.Status}}'
1. 看事件流(每次 exit 的准确时间)
docker events --filter container=<容器名> --since 10m| exit code | 含义 | 常见原因 |
|---|---|---|
| 0 | 正常退出 | 容器内是短命进程,不该配 restart:always |
| 1 | 应用错误 | 配置语法错、依赖缺失、启动脚本第一行就失败 |
| 2 | shell 用法错误 | entrypoint 用了不存在的命令 |
| 125 | docker 本身拒绝 | 镜像不存在、参数错 |
| 126 | 命令不可执行 | 没执行权限,或 shebang 解释器不存在 |
| 127 | 命令找不到 | entrypoint 的命令不在 PATH |
| 137 | 被 SIGKILL | OOM 或被手动 kill |
| 139 | 段错误 | 二进制不兼容(glibc 版本不符) |
| 143 | 被 SIGTERM | 收到停止信号(常因健康检查失败被重启) |
137 最容易被误判:看着像「程序崩了」,其实是内存超限被内核杀掉。用
docker inspect --format "{{.State.OOMKilled}}"确认。
第二步:日志为空时的四种原因
原因 1:权限不对(最常见,且最难猜)
容器内进程以非 root 用户运行(如 uid 999、33),但挂载进来的配置文件属主是 root 且权限 600,进程读不到配置就退出,连错误都没机会写。
# 现象:反复重启,日志为空
1. Restarting (1) 3 seconds ago
1. 验证权限与容器内 uid
ls -l /path/to/conf/file.conf
1. -rw------- 1 root root ← 600 root,容器内 uid 999 读不到
docker inspect <容器名> --format '{{.Config.User}}'
1. 修复
chmod 644 /path/to/conf/file.conf- Redis 的经典坑:
redis.conf必须 644 且文件必须存在。属主不是容器内 uid 999 时,600 会 Permission denied 并无限重启 - Nginx 的坑:证书私钥通常 600 root,若 nginx 以非 root 跑,读不到私钥直接退出
- MySQL 的坑:数据目录权限必须是 999(mysql 用户),改成 root 会启动失败
原因 2:entrypoint 命令不存在
# 看镜像定义的 entrypoint 与 cmd
docker inspect <镜像> --format 'ENTRYPOINT={{.Config.Entrypoint}} CMD={{.Config.Cmd}}'
1. 手动跑一次,看报错
docker run --rm -it --entrypoint /bin/sh <镜像> -c "which your-command"原因 3:配置语法错误但进程静默退出
有些服务解析配置失败时直接 exit 而不打印错误(或只写 syslog 不写 stderr)。
# 临时去掉 restart 策略,前台跑,看真实输出
docker run --rm -it <镜像> <启动命令>
1. 确认日志驱动(none/syslog 时 docker logs 拿不到内容)
docker inspect <容器名> --format '{{.HostConfig.LogConfig.Type}}原因 4:健康检查一直失败
容器其实「活着」,但被健康检查判死,编排层反复重启。状态显示 unhealthy,配合 restart:always 就表现成循环重启。
# 看健康检查的最近输出
docker inspect <容器名> --format '{{json .State.Health}}' | python -m json.tool
1. 手动跑健康检查命令,看为什么失败
docker exec <容器名> <healthcheck 里的命令>第三步:判断是不是依赖没就绪
容器启动时数据库还没起来,应用连不上就退出——这是 compose 编排里第二常见的原因。
# 错误:只声明 depends_on,不管对方是否就绪
services:
app:
depends_on:
- mysql # 只保证容器启动了,不保证能连上
1. 正确:用 service_healthy
services:
app:
depends_on:
mysql:
condition: service_healthy
mysql:
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1"]
interval: 5s
timeout: 3s
retries: 10
start_period: 30s # 给初始化留时间,首次启动要建库- start_period 很重要:MySQL 首次启动初始化可能 30-60 秒,不设这个,健康检查在初始化期就判失败
- 应用侧也要有重试逻辑——依赖 healthy 只是减少概率,不是绝对保证
docker compose up -d后要等ps显示 healthy,不要立刻测应用
第四步:OOM 与资源限制
exit code 137 且日志戛然而止,基本就是 OOM。
# 确认是否被 OOM 杀掉
docker inspect <容器名> --format '{{.State.OOMKilled}}'
1. 看内存限制与实际用量
docker stats --no-stream <容器名>
1. 看宿主 dmesg 的 OOM 记录
dmesg | grep -i "killed process" | tail -5- 没有 swap 的机器特别容易 OOM:内存满了直接杀进程,没有缓冲
- Go/Java 要注意堆上限(
-Xmx或GOGC),容器限 512M 时 JVM 默认堆可能按宿主内存算 restart: always加 OOM 会形成「启动→OOM→重启」死循环,CPU 一直被打满
一条命令组合排查
CN=容器名
echo "== 状态与退出码 =="
docker inspect $CN --format 'Status={{.State.Status}} Exit={{.State.ExitCode}} Restarts={{.RestartCount}} OOM={{.State.OOMKilled}}'
echo "== 最近日志 =="
docker logs --tail 30 $CN 2>&1
echo "== 健康检查 =="
docker inspect $CN --format '{{json .State.Health}}' 2>/dev/null | head -c 500
echo; echo "== 资源 =="
docker stats --no-stream $CN
echo "== 挂载 =="
docker inspect $CN --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{"\n"}}{{end}}'
echo "== 事件 =="
docker events --filter container=$CN --since 10m --until 0s | tail -10复盘:这类故障的共同点
- 日志为空时不要继续盯日志——转而看 exit code 和权限,这两处信息量更大
- 权限问题占多数:非 root 容器 + root 属主的配置文件,是最高频组合
- compose 的 depends_on 不等于就绪:必须配 healthcheck + service_healthy + start_period
- 137 先查 OOM,别浪费时间看应用日志——内存超限时应用来不及写日志
- 先临时去掉 restart 策略再排查:它会把一次性启动错误变成无限循环,干扰观察