
Docker容器一直处于Restarting状态时,网站可能间歇性出现502、503或连接失败,后台任务也会不断中断。此时最重要的不是反复执行docker restart,而是先暂停自动重启,保留退出现场,再根据退出码、日志和容器状态找到真正原因。
容器反复重启通常不是Docker本身“坏了”,而是容器中的主进程退出后,重启策略又把它拉了起来。配置缺失、启动命令错误、端口冲突、依赖服务未就绪、文件权限、内存不足和健康检查设计不当,都可能制造重启循环。
本文介绍一套适用于Docker Engine和Docker Compose环境的排查方法。
一、先确认容器是否真的在重启
查看全部容器:
docker ps -a
只查看目标容器:
docker ps -a --filter name=myapp
如果状态不断在Restarting、Up和Exited之间切换,继续查看重启次数:
docker inspect -f '{{.RestartCount}}' myapp
再输出完整运行状态:
docker inspect -f '{{json .State}}' myapp
重点关注这些字段:
| 字段 | 含义 |
|---|---|
Status | 当前状态,例如running、exited、restarting |
ExitCode | 容器主进程退出码 |
Error | Docker启动容器时记录的错误 |
OOMKilled | 是否被Docker记录为内存不足杀死 |
StartedAt | 最近一次启动时间 |
FinishedAt | 最近一次退出时间 |
Health | 配置健康检查后显示健康状态和探测记录 |
如果StartedAt与FinishedAt间隔只有几秒,通常说明应用在启动阶段就失败了。如果容器运行一段时间后才退出,更需要关注内存增长、外部依赖、定时任务和运行期间的异常。
二、先暂停重启循环,保留故障现场
容器每隔几秒重启一次,会快速刷掉关键日志,也可能持续冲击数据库、消息队列或第三方接口。可以先关闭目标容器的重启策略:
docker update --restart=no myapp
然后停止容器:
docker stop myapp
确认状态:
docker ps -a --filter name=myapp
不要直接删除容器,更不要立刻执行docker compose down -v。删除容器可能丢失未挂载到卷中的现场文件,而-v还会删除Compose管理的命名卷,可能造成实际数据损失。
如果这是线上核心服务,暂停前应先确认是否有其他副本承接流量,并记录当前镜像ID、环境变量来源、挂载、网络和启动参数。
三、查看容器日志
先查看最近200行并显示时间:
docker logs --tail 200 --timestamps myapp
查看最近30分钟:
docker logs --since 30m --timestamps myapp
持续观察下一次启动过程:
docker logs -f --since 5m --timestamps myapp
常见报错包括:
- 配置文件不存在或格式错误。
- 环境变量、密钥或证书缺失。
- 数据库、Redis、消息队列连接失败。
- 监听端口已被占用。
- 挂载目录没有读写权限。
- 启动脚本或可执行文件不存在。
- 数据库迁移失败。
- Java、Node.js、PHP或其他运行时抛出未处理异常。
docker logs主要读取容器标准输出和标准错误。如果应用把日志只写进容器内文件,或者使用了不支持本地读取的日志驱动,命令可能没有足够信息。此时需要检查应用日志目录和Docker日志配置:
docker inspect -f '{{.HostConfig.LogConfig.Type}}' myapp
不要为了排查方便就长期启用无限增长的日志。生产环境应配置日志轮转或日志平台,避免日志本身占满宿主机磁盘。
四、结合Docker事件还原时间线
查看最近30分钟内与目标容器有关的事件:
docker events \
--since 30m \
--filter container=myapp
事件中可能出现start、die、restart、oom、health_status等记录。它能帮助确认是主进程主动退出、Docker重启策略触发,还是健康状态发生变化。
docker events不是永久审计日志,历史事件可用范围有限。重要生产环境应把容器事件、应用日志和主机监控统一采集,避免故障发生几小时后已经找不到现场。
五、正确理解常见退出码
退出码是排查入口,不是最终结论。
| 退出码 | 常见含义 | 下一步 |
|---|---|---|
0 | 主进程正常结束 | 检查服务是否误把一次性脚本当成长驻进程,或程序是否启动后自行退出 |
1 | 应用通用错误 | 查看应用日志、配置和启动参数 |
125 | docker run自身或Docker守护进程未能正常启动容器 | 查看命令参数、Docker错误和守护进程日志 |
126 | 找到命令但无法执行 | 检查执行权限、文件格式和架构 |
127 | 找不到启动命令 | 检查镜像中的路径、ENTRYPOINT和CMD |
137 | 进程收到SIGKILL,常见于OOM或被强制终止 | 检查OOMKilled、内核日志、内存限制和人工操作 |
143 | 进程收到SIGTERM后退出 | 检查停止、部署、超时和优雅退出流程 |
退出码137经常被直接判断为内存不足,但它只说明进程受到强制终止。只有结合下面的信息,才能判断是否发生OOM:
docker inspect -f '{{.State.OOMKilled}}' myapp
查看容器内存限制:
docker inspect -f '{{.HostConfig.Memory}}' myapp
查看当前资源使用:
docker stats --no-stream myapp
检查宿主机内核日志:
sudo journalctl -k --since '1 hour ago' \
| grep -Ei 'out of memory|oom|killed process'
HostConfig.Memory输出为字节,值为0通常表示没有设置容器内存上限,但进程仍可能因为宿主机整体内存耗尽而被内核终止。
六、检查启动命令和PID 1
查看镜像与容器最终使用的启动配置:
docker inspect -f '{{json .Config.Entrypoint}}' myapp
docker inspect -f '{{json .Config.Cmd}}' myapp
常见问题包括:
ENTRYPOINT脚本没有执行权限。- Windows换行符导致Linux脚本无法解释。
- 脚本中引用了镜像内不存在的命令。
- 工作目录错误,相对路径找不到文件。
- 启动脚本把服务放到后台后自身退出。
- Shell没有使用
exec把主进程交给PID 1。
Dockerfile更推荐使用exec形式启动长驻进程:
ENTRYPOINT ["/app/entrypoint.sh"]
CMD ["/app/server"]
启动脚本最后可以使用:
exec "$@"
这样应用会成为容器中的主进程,能够更直接地接收停止信号并返回真实退出码。
如果镜像内包含Shell,可以临时覆盖入口进行检查:
docker run --rm -it \
--entrypoint /bin/sh \
your-image:tag
精简镜像或distroless镜像可能没有/bin/sh。不要为了调试就随意修改线上镜像,可在同一构建版本上制作专用调试镜像。
七、检查环境变量、挂载和文件权限
查看容器配置:
docker inspect myapp
重点检查:
Config.Env中的必要变量是否存在。Mounts中的源路径和目标路径是否正确。- 配置文件是否被空目录覆盖。
- 容器运行用户是否有权读取证书、配置和数据目录。
- 挂载文件在宿主机上是否存在。
查看挂载:
docker inspect -f '{{json .Mounts}}' myapp
检查容器运行用户:
docker inspect -f '{{.Config.User}}' myapp
宿主机目录权限应与容器内进程使用的UID和GID匹配。不要为了快速恢复直接把目录改成chmod 777,这会扩大安全风险,也可能掩盖镜像用户配置错误。
docker inspect和docker compose config可能展开环境变量,其中可能包含密码、令牌和连接串。排查输出不要直接粘贴到公开工单或聊天群。
八、检查依赖服务是否尚未就绪
应用容器可能比数据库、Redis或消息队列更早启动。如果程序连接失败后直接退出,再配合自动重启策略,就会形成循环。
先确认各服务状态:
docker compose ps
查看依赖服务日志:
docker compose logs --tail 200 db redis
Docker Compose可以用健康状态控制启动依赖,例如:
services:
db:
image: mysql:8.4
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
app:
image: example/app:1.0
depends_on:
db:
condition: service_healthy
restart: "on-failure:5"
这只能改善启动顺序,不能代替应用自身的重试、超时和断线重连。数据库在运行期间短暂不可用时,应用仍应采用有上限的退避重试,而不是遇到一次连接失败就结束整个进程。
九、健康检查失败会自动重启容器吗
普通Docker容器的健康检查会把状态标记为starting、healthy或unhealthy,并保存最近的探测结果,但unhealthy本身不会触发Docker Engine的重启策略。
查看健康状态:
docker inspect -f '{{json .State.Health}}' myapp
如果容器状态是Up但健康状态为unhealthy,应查看健康检查输出和应用内部状态,而不是把它和“主进程退出后自动重启”混为一谈。某些编排平台或外部监控会根据健康状态执行替换或重启,那是编排层行为。
一个较完整的Compose健康检查示例:
services:
app:
image: example/app:1.0
restart: unless-stopped
init: true
healthcheck:
test: ["CMD", "/app/healthcheck"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
/app/healthcheck必须真实存在于镜像中,并在服务可用时返回0。如果使用curl或wget探测HTTP接口,要先确认镜像中安装了对应工具。健康检查过于频繁、超时太短或依赖外部网络,也可能制造大量误报。
十、检查重启策略是否合理
查看当前策略:
docker inspect -f '{{json .HostConfig.RestartPolicy}}' myapp
Docker常见重启策略包括:
| 策略 | 行为 |
|---|---|
no | 不自动重启,默认值 |
on-failure[:max-retries] | 仅在非零退出时重启,可限制次数 |
always | 容器停止后持续尝试重启;人工停止后,要等Docker守护进程重启或手动启动才会恢复 |
unless-stopped | 与always相近,但人工停止的容器在Docker守护进程重启后仍保持停止 |
重启策略只有在容器成功启动并持续运行至少约10秒后才会生效,这可以避免Docker把一个从未正常启动的容器无限当作稳定服务处理。
排障阶段更适合暂时使用no,批处理任务可根据业务选择带最大次数的on-failure,长驻服务通常使用unless-stopped或由统一编排平台管理。不要同时让宿主机进程管理器和Docker重启策略反复拉起同一个容器,否则故障行为会更难判断。
修复后恢复策略:
docker update --restart=unless-stopped myapp
如果使用Compose,应修改compose.yaml中的配置后重新创建容器,避免运行时配置与文件不一致:
docker compose up -d --force-recreate app
十一、修复后如何验证
不要只看到容器状态变成Up就结束排查。建议继续验证:
docker ps --filter name=myapp
docker inspect -f '{{.RestartCount}}' myapp
docker logs --since 10m --timestamps myapp
docker stats --no-stream myapp
同时确认:
- 重启次数不再增长。
- 容器持续运行时间正常。
- 健康状态达到
healthy,且探测输出没有间歇性失败。 - 应用端口、接口和后台任务实际可用。
- CPU、内存和磁盘使用没有持续上升。
- 数据库迁移、消息消费和定时任务没有重复执行。
如果故障涉及OOM、数据卷或数据库连接,还应观察一段完整业务高峰。短时间“看起来正常”并不能证明问题已经解决。
十二、一套实用的排查顺序
遇到Docker容器反复重启,可以按下面的顺序处理:
- 使用
docker ps -a确认状态和容器名称。 - 用
docker inspect记录退出码、OOMKilled、重启次数和时间。 - 暂时关闭重启策略,避免日志和依赖服务被持续冲击。
- 使用
docker logs和docker events还原退出前后的时间线。 - 根据退出码检查内存、启动命令、配置、权限和外部依赖。
- 单独检查健康状态,不要把
unhealthy直接等同于自动重启。 - 修复后重新创建或启动容器,并持续观察重启次数和资源使用。
重启策略的作用是帮助服务从偶发故障中恢复,不是替代错误处理。一个容器每隔几秒被重新拉起,只说明恢复动作在执行,并不说明服务已经恢复正常。




