Docker容器反复重启排查指南:从Restarting状态到根因解决
这个标题我看着特别眼熟因为前天刚在生产环境里踩完。当时线上一个容器化服务在凌晨三点悄悄进入Restarting (1) Less than a second ago状态监控告警触发的时候业务已经断了将近二十分钟。跑上服务器一看docker ps里那行刺眼的红字让人头皮发麻——容器不是没起来而是起来一瞬间就死死了又被拉起来反反复复。这种故障比单纯的Exited更难受它像一个永远在重启的机器人表面上看还在运行实际上每次存活时间不到一秒钟。这篇文章把这一类问题彻底掰开。不管你是刚接触 Docker 的新人还是已经维护了一段时间生产环境的开发者只要遇到容器反复Restarting都可以按这篇文章的排查路径一步步来。我会从状态含义、日志分析、底层字段挖掘到具体根因和解决模板再到 restart policy 配置全部用实际案例讲透。1. 这个状态真正想告诉你的事容器在反复猝死不是启动中很多人第一次看到Restarting (1) Less than a second ago时第一反应是容器还在启动中等一会儿就好。这个误解非常危险因为两者本质完全不同。启动中的容器 STATUS 列显示的是Up后面跟着启动时长比如Up 3 seconds而Restarting表示一个已经启动过、但又退出的容器被 Docker 守护进程按重启策略拉起来之后又退出了。说白了它不是在慢慢启动而是在反复猝死。1.1 读好一行 STATUS括号数字和时间分别代表什么先看懂这一行字。拿Restarting (1) Less than a second ago来说拆开看就三部分Restarting容器当前处于重启中状态也就是说它刚刚退出Docker 正在或已经按 restart policy 再次启动它。(1)这个数字是 Docker 统计到的重启次数随着每次崩溃拉起重启数字会持续往上加。你看到(1)说明已经重启了 1 次看到(15)说明这个容器已经反复死了 15 次。Less than a second ago这是上次容器退出的时间距现在多久。这里特别关键——不到一秒前意味着容器每次起来后撑不过 1 秒就挂了而且退出事件极其频繁。这里有个非常重要的画像如果docker ps里显示的是Restarting (1) 2 minutes ago说明容器每次能撑上几分钟再死这种往往是因为程序内部有间歇性错误比如内存慢慢涨满、连接池耗尽、定时任务里某个异常等。如果显示Less than a second ago说明程序连启动阶段都没能完成就退出了问题大概率出在环境、权限、依赖、命令这些起跑线层面而不是业务逻辑层面。这个诊断方向先确立排查思路就不会跑偏。1.2 为什么 Docker 会不厌其烦地拉起重启很多新人会疑惑容器都退出了为什么 Docker 还非要一遍遍拉起来这就要说到 Docker 的重启策略restart policy。如果在docker run时用了--restart always、--restart unless-stopped或--restart on-failureDocker 守护进程会监测容器退出事件然后根据策略自动重新启动它。生产环境里大家为了服务自动恢复普遍会给容器配置 restart policy这本意是好的但副作用就是如果容器自身有问题就会陷入无限重启循环。可以这样理解一台电脑系统崩溃了会自动重启第一次你可能觉得是偶发但如果它每次开机不到一秒就蓝屏还自动重启一百次你就该意识到这不是等它启动好的问题而是这台机器在系统层面就有什么硬伤。容器反复Restarting (1)、Restarting (2)、Restarting (3)就是 Docker 在告诉你这里有个东西连起跑线都跨不过去。接下来要做的就是找到那个让它跨不过去的硬伤。2. 第一板斧先用 docker logs 找出程序自己的遗言面对一个反复重启的容器我的习惯是先看日志而且一定要在容器刚退出、还没被再次拉起的时间窗口里看。那有人会问容器都已经挂了还看什么日志能看到的。容器退出的那一瞬间程序在退出前打印到标准输出stdout和标准错误stderr的内容会被 Docker 保存下来docker logs就是把这份遗言读出来。2.1 三种常用的日志姿势按场景选用docker logs 容器名查看容器从创建以来的全部日志如果日志太多会刷屏建议配合 tail 使用。docker logs --tail 200 容器名只看最近 200 行。这是排查重启类问题最常用的姿势因为崩溃前的最后几行日志往往就是线索。docker logs -f 容器名实时跟踪输出。如果容器重启很频繁用docker logs -f盯着看能看到每次崩溃前程序到底打出了什么。还有一个小技巧如果一个容器崩溃得特别快日志可能每次只产生几行用docker logs -f反复刷新观察几次基本能抓到规律。比如一段日志在每次重启后都只输出一行error: config file not found就没下文了说明程序是死在配置加载阶段如果日志是完整打完启动流程、最后才报错那就要往资源限制、OOM 那个方向想。2.2 日志里最常见的几类典型现场我把实际排障中高频出现的日志类型和判断方式整理了一下你可以对照着自己容器里的日志看日志特征大概率原因下一步动作chown: changing ownership of /var/lib/mysql/: Operation not permitted之类权限报错容器内用户对挂载目录没有写权限检查挂载目录权限、用户 UID 映射exec: bash: executable file not found或exec: no such file or directory启动脚本的换行符有问题或脚本内命令路径不存在检查 CRLF 换行符、CMD 中命令路径Cant open the mysql.plugin table. Please run mysql_upgrade to create it数据目录损坏或初始化不完整清理或重建数据卷/bin/sh: 1: xxx: not found启动命令里的可执行文件不在容器的 PATH 中用绝对路径启动或确认镜像内确实装了该程序Out of memory或直接没有日志、进程被秒杀容器内存超限被内核 OOM Kill查 inspect 的 OOMKilled 字段dial tcp 127.0.0.1:3306: connect: connection refused依赖的服务还没起来就启动主程序增加等待依赖、调整启动顺序database is locked多进程并发访问 SQLite 等文件型数据库检查是否启动多个实例有几个值得多说两句。第一种是有没有日志本身就是一个巨大信号。如果一个容器每次都是Restarting (1) Less than a second ago但docker logs显示No log output说明容器的主进程还没走到第一条日志输出就挂了。这时候问题多半不在业务代码而在主进程根本没起来成功——可能是 CMD 写错、动态库缺失、权限不对、被信号杀掉后面 inspect 退码那节我会展开。第二种要注意的是有些程序会把日志写到文件里面而不是标准输出比如一些 Java 应用和传统中间件。这种容器在崩溃时docker logs可能只有一行 JVM 启动信息真正的错误埋在容器内日志文件里或者直接通过挂载卷对外暴露。如果遇到这种情况先别急着给程序判死刑想办法进容器看看日志文件或者看挂载出来的日志目录。3. 第二板斧docker inspect 能挖出比日志更底层的死亡报告日志能看到程序自己说了什么但有时候程序连话都没来得及说就死了。这时候就得靠docker inspect它能直接读取 Docker 守护进程记录的死亡报告包括退码、错误信息、是否被 OOM 杀死这些信息是日志之外最客观的现场证据。3.1 一条命令拿到关键状态字段先给出一条我排障时一定会用的组合命令docker inspect -f {{.State.Status}} | RestartCount{{.RestartCount}} | ExitCode{{.State.ExitCode}} | OOMKilled{{.State.OOMKilled}} | Error{{.State.Error}} | StartedAt{{.State.StartedAt}} | FinishedAt{{.State.FinishedAt}} 容器名执行后你会看到类似这样的输出running | RestartCount7 | ExitCode1 | OOMKilledfalse | Errornil | StartedAt2025-01-15T09:22:31.123Z | FinishedAt2025-01-15T09:22:30.987Z几个字段各有用处RestartCount确认重启次数如果数字在持续增长说明循环还没停止。State.ExitCode上一次退出的退出码这是判断死因最重要的一把钥匙。State.OOMKilled如果为true说明上一次退出是内存超限被内核杀掉基本不用再看别的了。State.ErrorDocker 在启动过程中的报错信息如果非空直接看它。StartedAt和FinishedAt两个时间一减能算出来容器每次存活了多久。如果FinishedAt比StartedAt只晚了几毫秒那就是起来即死。3.2 ExitCode 退码速查表我做了张表基本覆盖了实践中遇到的 90% 的退出码场景退出码含义典型场景排查方向0程序正常退出主进程执行完主动退出或后台化后父进程返回检查 CMD 是否启动的是前台进程1通用程序错误程序抛异常、配置校验失败重点看日志里报错信息2误用 shell 内建命令脚本语法错误、命令拼接问题检查启动脚本和 ENTRYPOINT126命令无法执行没有执行权限检查脚本是否有 x 权限127命令找不到CMD/ENTRYPOINT 路径写错或镜像中缺命令检查命令路径、PATH 环境变量137SIGKILL多数是 OOM内存超过容器 limit 被内核杀死或人为 kill -9查 OOMKilled 字段、调大内存、优化进程139SIGSEGV 段错误二进制兼容问题、底层库损坏考虑镜像架构、glibc 版本问题143SIGTERM优雅终止收到停止信号检查是否被脚本主动 kill或 Docker stop3.3 OOMKilled 怎么确认最靠谱很多人一看 ExitCode 是 137就认定是内存超限。这个判断通常没错但最好还是用OOMKilled字段确认因为kill -9也会产生 137两者处理方式完全不同。确认方法docker inspect -f {{.State.OOMKilled}} 容器名如果是true基本可以锁定是内存问题。另外还可以看内核日志确认dmesg | grep -i oom或者查宿主机上有没有对应的内核记录比如oom-killer杀掉了容器内 PID。如果内存超限常规解法是把启动命令里的 JVM 堆参数、Node 内存限制调下来同时把容器的--memory限制抬高。但还是建议先用docker stats观察下容器的正常内存占用量再定合理的 limit。上来直接加内存也不是不行但搞清楚程序到底为什么吃这么多内存才是长久之策。4. 覆盖九成场景的四个根因每个都附排查模板看完了日志也查了 inspect下面进入实操环节。根据我维护了几十个容器化服务的经验Restarting (1) Less than a second ago这类起来即死的问题绝大多数逃不出这四类根因。4.1 主进程没有前台化一启动就跑完收工这是新手上路时最经典的一坑也是最容易被忽略的一条。Docker 容器的生命周期是由它的主进程PID 1决定的。如果主进程执行完就退出容器就会进入退出状态即使 restart policy 设置了 auto restart也会循环重启。我见过一个真实例子有人用CMD service mysql start或CMD /etc/init.d/mysql start启动数据库。service命令启动完守护进程后它自己就退出了容器里的 PID 1 消失容器整体退出。Docker 一拉起重启又跑一遍service mysql start又退出周而复始。排查模板也很简单看看 Dockerfile 或 docker-compose.yml 里 CMD 是不是一个启动后立即返回的命令。如果是常见的改法有两种如果程序本身支持前台运行像 Nginx 有nginx -g daemon off;直接改成前台模式CMD [nginx, -g, daemon off;]如果服务必须后台跑就需要一个能托住前台进程的启动脚本通常用exec把服务进程提升为 PID 1。比如#!/bin/bash exec /usr/local/bin/my-server这个exec很关键它让脚本进程被目标程序替换目标程序直接成为容器的主进程信号也能正确传递。4.2 CMD/ENTRYPOINT 路径不存在127 退码的经典场景还有一种很典型的现场docker logs没有任何业务日志docker inspect里 ExitCode 是 127Error字段提示exec: xxx: executable file not found或exec: xxx: no such file or directory。这个报错的含义是Docker 尝试在容器里执行你指定的命令但找不到这个可执行文件。这种情况多半是因为 Dockerfile 里写死了CMD [/usr/local/bin/start.sh]但镜像更新后脚本路径变了或者基础镜像从 CentOS 换成了 Alpine包管理器和命令都变了原来装的程序根本不存在。处理方式分两步。第一步先用docker run --rm --entrypoint sh 镜像名 -c ls -l /usr/local/bin/start.sh进容器确认文件到底存不存在、有没有执行权限。第二步如果文件存在但报 no such file反而要往解释器缺失方向想比如脚本第一行是#!/bin/bash但基础镜像里连 bash 都没有那就换成#!/bin/sh或者重新安装 bash。这里还有个细节很容易踩很多人用docker run --rm 镜像名 命令直接在镜像里执行程序但容器环境里没有交互式终端如果你执行的是类似python3 -i这种需要标准输入的命令它也会立即退出。这种不是路径问题是启动方式不对。4.3 启动脚本的权限与 CRLF 换行符最容易被忽视的两件小事有一次一个 Java 服务每次起来不到一秒就死我看日志看到一句exec: /app/start.sh: permission denied当时排查了很久才发现是脚本没有执行权限。Docker 在 Exec 一个文件之前会校验它的权限位如果start.sh没有 x 权限直接报 permission denied。解法很简单在 Dockerfile 里加上RUN chmod x /app/start.sh另一件事更隐蔽Windows 上开发的脚本拷贝到 Linux 容器后换行符是\r\nCRLF而 Linux 下 shell 期待的是\nLF。这会导致脚本执行时报出类似exec: /app/start.sh: no such file or directory或者/bin/sh^M: bad interpreter这种诡异错误。为什么文件明明存在却报 no such file因为脚本第一行#!/bin/sh\r被 shell 读成了#!/bin/sh\r解释器路径里带着一个回车符自然找不到。解法是把脚本转成 LF 换行符可以在 Dockerfile 里用sed -i s/\r$// /app/start.sh也可以直接用dos2unix。4.4 内存超限被内核杀死137 退码与 OOMKilled 字段程序本身写得没问题命令也对权限也对但容器就是反复重启而且每次存活时间越来越短最后看一眼 exit code 是 137。这种大概率是容器内进程在干重活时吃掉了所有分配到的内存被内核 OOM Killer 一梭子带走。比如我之前有一次用 Docker 启动一个 Java 服务默认 JVM 堆内存向宿主机申请了总内存的 1/4容器 limit 只给了 1GB结果 JVM 一跑就爆。优化方式就是把 JVM 的内存参数写明白CMD [java, -Xms256m, -Xmx512m, -jar, app.jar]如果是 Node 应用可以用环境变量限制老生代内存大小docker run -e NODE_OPTIONS--max-old-space-size512 镜像名如果排查下来程序确实需要大内存那就调高容器的--memory上限。但注意如果宿主机内存也不够了光调容器参数解决不了问题得考虑加机器或做服务降级。5. 别让故障无限重试restart policy 要与场景匹配排查完根因、修复问题之后还有一件一定要做的事重新审视容器的 restart policy。很多服务反复重启导致事故扩大的原因不是故障本身而是无限重试把负载打满了。我甚至见过一个容器每分钟重启几十次把宿主机的磁盘 I/O 吃掉大半连带其他正常业务一起遭殃。5.1 四种常见策略各自适合什么场景Docker 的 restart policy 有四种区别用一张表说清楚策略行为适合场景no退出后不自动重启即默认策略一次性任务、批处理容器on-failure[:max-retries]非零退出码时自动重启可限制最大次数生产服务搭配重试次数限制always无论退出码是什么总是重启需要持续在线的常驻服务unless-stopped总是重启但手动 stop 后不再自动拉起需要手动控制的常驻服务生产环境里我最推荐的是on-failure:3或on-failure:5也就是允许容器在异常退出时重试几次但不要无限重试。这样既留了偶发故障自动恢复的余地又避免了必现故障反复烧资源的灾难。如果服务本身是绝对不能中断的还要另外配健康检查和外部告警不能只靠 restart policy 兜底。5.2 已经陷入重启循环时如何紧急止损如果容器已经处于Restarting循环第一时间要做的是停止这种无效消耗。两种方式# 方式一直接关掉重启策略再停止 docker update --restartno 容器名 docker stop 容器名 # 方式二直接强制停止当前运行中的进程会收到 SIGKILL docker kill 容器名注意光docker stop可能不够因为 restart policy 会把它再次拉起来。一定要先用docker update --restartno把策略摘掉再 stop否则会看到怎么 stop 了又自己起来了的怪现象。这个顺序很重要。然后保留现场不要急着删除容器凡是和故障相关的容器都留着等收集完docker logs、docker inspect之后再做清理。5.3 给重启次数过多加一道监控闸生产环境里不能只依赖docker ps肉眼观察。我给自己的服务加了两层保护第一层是健康检查healthcheck第二层是容器重启次数监控。健康检查可以在 Dockerfile 或 compose 里声明healthcheck: test: [CMD, curl, -f, http://localhost:8080/health] interval: 30s timeout: 5s retries: 3 start_period: 10s有了健康检查监控系统可以直接拉取容器状态unhealthy状态一出现就能告警不用等到整个容器挂掉。重启次数监控则看docker inspect -f {{.RestartCount}}这个字段定期采集如果发现某个容器的重启次数短时间内快速增长就触发告警。这本质上是在给 Docker 的自动恢复机制系一根安全绳防止它变成连环事故的发源地。6. 日志和退码都看了还是没头绪试试这几个进阶调试手段剩下不到 10% 的情况日志看了、退码也查了、restart policy 也改了问题依旧。这时候常规手段就不够用了需要上点手术刀级别的排查方式。我自己用的最多的是这三招钻进临时容器、盯住事件流、处理挂载目录权限的边界问题。6.1 用临时容器手动跑一下原命令遇到容器反复崩溃又找不到原因最快的办法是先不让它跑用同一个镜像起一个临时容器手动执行启动命令看看真实报错是什么。因为docker logs只能看到输出到 stdout/stderr 的内容某些场景下主进程崩溃是因为依赖的子进程拉起失败这些细节在 logs 里不一定有。# 用 sh 覆盖原入口点进容器手动跑 docker run --rm -it --entrypoint sh 镜像名 # 然后在容器里手动执行原入口命令 /app/start.sh手动执行时你是在终端里看遇到缺依赖、缺权限、脚本卡住之类的问题感受会直观很多。如果手动执行时命令能正常跑起来说明问题出在运行时环境差异比如环境变量没有传进来、挂载卷没挂对。这时再对比docker run和原容器之间的环境差异基本就能定位了。6.2 用 docker events 观察容器生死的完整时间线另一种进阶手段是开一个实时事件通道观察容器的完整生命周期docker events --filter container容器名输出会显示容器每次的 start、die、restart 事件。如果你发现容器每次都是启动后几十毫秒就 die且 die 事件的 exitCode 始终是某个固定值那问题就相当明确了。结合FinishedAt和StartedAt的差值还能算出每次存活时长帮助判断崩溃行为是否有规律。这种方法的优势在于能看到docker ps那一刻之外的连续过程。比如你刚看到Restarting (1)的时候容器其实已经被拉起过多次了事件流能还原整个过程的完整节奏不会被一帧画面误导。6.3 挂载目录权限、SELinux 与容器内用户映射最后一类坑出现在使用数据卷挂载、或者跑一些需要写文件的容器时。典型报错包括chown: changing ownership of /var/lib/mysql/: Operation not permitted或mkdir: cannot create directory /data: Permission denied原因通常很直接宿主机上挂载进去的目录其属主和权限跟容器内进程的用户 UID 不匹配。比如 MySQL 镜像内进程默认以mysql用户运行UID 通常是 999而你宿主机挂载的目录属主是 root容器内用户没有写权限它就起不来。解决办法有几个方向。一个是直接把挂载目录的属主改成容器内用户对应的 UIDchown -R 999:999 /data/mysql另一个是在容器启动参数里指定用户docker run --user 0:0 镜像名或者给挂载目录开足够权限。如果宿主机开启了 SELinux还可能需要加:z或:Z结尾-v /data/mysql:/var/lib/mysql:Z这类问题最麻烦的地方在于报错往往不是直接写在业务日志里而是藏在系统调用的报错中但好在docker logs一般还是能看到一句 Permission related 的错误抓到了就往挂载目录权限方向查很快就能解决。最后再分享一点个人的小习惯吧。每次我把一个容器从Restarting状态救回来都会顺手做两件事一是把完整排查过程记录到团队的故障文档里包括退出码、日志样本、修复命令二是给这个容器的 restart policy 和健康检查做一次压力测试——停掉依赖、删掉数据卷、改错配置人为制造几种故障看它会不会再出现那种起来即死的循环。这个习惯帮我提前发现过好几次隐蔽问题比每次都等线上告警再慌慌张张去查要省心太多。Docker 容器化的坑几乎踩不完但排查思路一旦固定下来再花式的故障也就是多花点时间的事。