拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Docker容器退出后日志不见了?从退出码到docker logs的排查指南

Docker容器退出后日志不见了?从退出码到docker logs的排查指南

1. 先搞清楚容器退出的状态,再决定怎么查日志

1.1 第一件事永远都是 docker ps -a

遇到容器“消失”的情况,很多人的第一反应是慌。其实容器只要不是被你手动 docker rm 删掉,哪怕已经退出(Exited),它在宿主机上的“档案”都还保留着。第一步永远是先跑一条命令:

docker ps -a

注意不是 docker ps。docker ps 只显示正在运行的容器,已经退出的是看不到的,所以你会误以为容器丢了。docker ps -a 会列出所有容器,包括正在运行的、正常停止的、异常退出的一股脑全在里面。

输出里的 STATUS 列是关键。常见的状态有:

  • Up 2 hours:正常运行,没啥问题。
  • Exited (0) 3 minutes ago:退出码 0,表示是正常结束,可能是任务跑完了,也可能是你主动 stop 了。
  • Exited (1) 5 seconds ago:退出码 1,应用自己抛错退出,这是最常见的故障状态。
  • Restarting (2) 1 second ago:容器在反复重启,一般是启动入口命令一直失败,配合 restart 策略在无限重试。

这个状态下,先把 CONTAINER ID 或者 NAMES 记下来,后面所有操作都要用这个唯一标识。有些朋友会顺手用 docker rename 给容器起个好记的名字,这也是个好习惯,后面看日志的时候不用对着那一长串 ID 发懵。

1.2 用 docker inspect 看退出码,比 ps 更详细

docker ps -a 能看到的退出码只是一个数字,但如果想了解更完整的退出信息,我建议再多跑一条:

docker inspect <容器名或ID> --format '{{.State.ExitCode}}' docker inspect <容器名或ID> --format '{{.State.Error}}' docker inspect <容器名或ID> --format '{{.State.FinishedAt}}'

docker inspect 拉出来的是一个超大的 JSON,直接人眼看会崩溃,所以一般用 --format 精准取值。ExitCode 是退出码,Error 会记录容器退出时的错误描述,FinishedAt 能看到它是什么时候挂掉的。

为什么要先看退出码而不是直接翻日志?因为退出码能帮你快速缩小排查范围。比如退出码 137,那你翻日志大概率看不到应用层报错,因为这是被系统杀掉(一般是 OOM),日志里往往只有戛然而止的痕迹。退出码 0,那就是正常退出,你翻半天日志也翻不出“故障”。

我把各环境常见的退出码整理了一下,可以直接对照:

退出码含义常见原因日志排查重点
0正常退出任务执行完毕、主动 stop不需要排查
1一般性错误应用代码退出、启动失败看进程结束前最后几十行
2misuse of shell builtins应用/脚本非法调用看 stderr 输出
126命令存在但无法执行权限问题看启动命令相关日志
127命令未找到镜像里没有该命令检查启动命令拼写
130被 Ctrl+C 终止手动中断一般非故障
137被 SIGKILL 杀掉OOM Killer 或 docker kill看系统日志、dmesg
139段错误 SIGSEGV应用内存越界看崩溃前堆栈输出
143被 SIGTERM 终止执行了 docker stop、平台发终止信号看优雅停机逻辑是否正常

看退出码这个步骤熟练之后,基本一眼就知道这个容器是“正常下班”还是“被抬走的”。

2. 用 docker logs 查看已退出容器的日志:最直接的办法

2.1 容器退出后日志还在吗

先说结论:默认情况下,只要容器不是用 --rm 启动的,退出之后日志依然完整保留在宿主机上,docker logs 依然可以读。

很多朋友以为容器退出等同于日志清空,其实不会。Docker 的日志机制是:容器进程往标准输出(stdout)和标准错误(stderr)写的所有内容,都会被宿主机上的 docker 守护进程接管,然后按日志驱动写入到宿主机文件里。这个过程和你容器是跑着还是已经退出没关系,只要你没有手动删容器,日志文件就一直在。

所以查看已退出容器日志的核心命令非常简单:

docker logs <容器名或ID>

不过这条命令有个坑:它会一次性把所有日志全打到终端上,哪怕容器已经跑了一个月,日志有几万行。如果你的终端比较“脆”,或者日志量巨大,屏幕会直接爆掉,甚至把终端卡死。所以我建议,线上排查几乎别用不带参数的 docker logs,至少加个 --tail 限制一下:

docker logs --tail 200 <容器名或ID>

这条命令只看最后 200 行,基本覆盖了容器退出前 90% 的线索。

2.2 搭配时间戳和时间范围来过滤

容器日志排查最烦的一个问题就是:不显示时间戳,根本不知道哪条日志是退出前几秒打出来的。所以在排查已退出容器时,我强烈建议默认加上 --timestamps:

docker logs --tail 200 --timestamps <容器名或ID>

每条日志前面会带完整的 RFC3339 格式时间戳,比如 2025-06-01T14:23:11.582Z。看到时间戳之后,再配合 --since 就能按时间段精确过滤。

假设你的容器是在 6 月 1 日 14:30 退出的,你想看它死前 10 分钟里到底发生了什么:

docker logs --since 2025-06-01T14:20:00 --until 2025-06-01T14:30:00 --timestamps <容器名或ID>

--since 和 --until 也支持相对时间,比如 --since 10m 表示最近 10 分钟,--until 1h 表示 1 小时前以前(其实一般不会单独用 until)。这种相对时间在快速排查时特别方便:

docker logs --since 20m --timestamps <容器名或ID>

这里有个小坑:Docker 的 --since 参数,相对时间还好说,但如果用绝对时间,默认解析是 UTC。你在国内日常用的是东八区,写绝对时间时要换算好,或者干脆用相对时间,省得出错。我遇到太多人排查时因为时区对不上,少看了 8 个小时的日志,愣是没找到问题。

2.3 把日志导出来慢慢看,别在终端硬扛

当容器日志量特别大的时候,与其直接在终端里翻,不如先把日志重定向到文件里再分析:

docker logs <容器名或ID> > /tmp/container.log 2>&1

加了 2>&1 才能把标准错误一起导进文件。然后你想怎么折腾都行:

tail -n 200 /tmp/container.log grep -n "ERROR\|Exception" /tmp/container.log | tail -n 50 less /tmp/container.log

如果日志本身是 JSON 格式(比如很多 Java 应用配了 JSON 日志格式),还可以用 jq 把它解析成表格方便筛选:

docker logs <容器名或ID> 2>&1 | jq -r 'select(.level=="ERROR") | .message'

这个操作在终端里跑和在导出文件里跑都可以,但对于 GB 级日志,docker logs 每次都要把整个 json-file 从头解析一遍,效率并不高。日志特别大的时候,我更推荐直接去宿主机上读日志文件,方法往下看第三节。

2.4 容器退出后想再进容器里查怎么办

docker logs 只能看 stdout/stderr。如果应用把日志写到了容器内的文件里,比如 /var/log/app/app.log,而 docker logs 什么都看不到,这时候怎么办?

容器已经退出,docker exec 是进不去的。但有个小技巧:先把容器重新启动起来,再 exec 进去:

docker start <容器名或ID> docker exec -it <容器名或ID> bash

启动命令可以临时覆盖,方便卡在启动阶段就崩溃的应用:

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

如果不想让容器真正跑业务,只是想进去看看文件,这个办法最实用。

如果容器本身还在(哪怕已经退出),但你想把里面的日志复制出来分析,docker cp 在容器停止状态下也能用:

docker cp <容器名或ID>:/var/log/app/app.log /tmp/app.log

这点知道的人不多,但真到排查的时候能救命。

3. 从宿主机日志文件入手:底层原理与替代方案

3.1 json-file 日志文件到底存在哪儿

Docker 默认的日志驱动是 json-file,也就是说,容器打的 stdout/stderr 最终会被 docker 守护进程写进一个 JSON 格式的文件里,一行一条日志,而且这个文件天然支持 docker logs 读取。

这个文件怎么找?最靠谱的姿势是让 docker 自己告诉你:

docker inspect --format '{{.LogPath}}' <容器名或ID>

输出通常类似:

/var/lib/docker/containers/8f2a4c9e5d61a3e7b0c2d4f6a8b1c3e5d7f9a0b1c2d4e5f67890a1b2c3d4e5f6/config.v2.json

不对,LogPath 一般指向的是日志文件本身,会是以 -json.log 结尾的路径:

/var/lib/docker/containers/8f2a4c9.../<容器长ID>-json.log

拿到路径之后,直接当成普通文件操作就行:

tail -n 200 /var/lib/docker/containers/8f2a4c9.../8f2a4c9...-json.log

文件内容每一行是一个 JSON 对象,结构类似:

{"log":"2025-06-01 14:20:11 INFO starting...","stream":"stdout","time":"2025-06-01T14:20:11.582Z"}

stream 字段区分是 stdout 还是 stderr,time 是写入时间,log 就是原始日志行。

直接读文件的优势在于:不经过 docker 守护进程的过滤和解析,对于超大日志文件的检索速度更快。另外,即使某些极端情况下 docker logs 命令不可用(比如 docker daemon 异常,但文件还在),你依然能拿到日志内容。

3.2 日志驱动不是 json-file 怎么办

不一定所有 Docker 环境都默认用 json-file,有些运维会全局改掉日志驱动。用下面这条命令可以查看当前默认驱动:

docker info --format '{{.LoggingDriver}}'

也可以查单个容器的实际驱动配置:

docker inspect --format '{{.HostConfig.LogConfig.Type}}' <容器名或ID>

不同驱动下,“查看日志”的方式完全不同:

日志驱动docker logs 是否可用日志流向查看方式
json-file可用宿主机 JSON 文件docker logs、直接读文件
local可用宿主机二进制文件docker logs
journald新版可用systemd journaljournalctl -u docker、journalctl CONTAINER_ID=xxx
syslog不可用宿主机 syslog/rsyslog/var/log/messages、/var/log/syslog
fluentd不可用Fluentd 采集端去 Fluentd 日志中心
gelf不可用Graylog 等去对应日志平台
awslogs不可用CloudWatch Logs去 AWS 控制台

如果你是老版本 Docker,docker logs 对 journald 驱动不一定支持,这时候直接用 journalctl 查:

journalctl CONTAINER_ID=<容器ID> --since "10 min ago"

journalctl 里的 CONTAINER_ID 是全小写的容器完整 ID,也就是 docker inspect 里的长 ID,不是短 ID。

另外,如果你环境里日志驱动被改成了 syslog,那 docker logs 铁定查不到任何东西,得去系统日志文件里 grep 容器名。

3.3 日志轮转配置:别等磁盘被写爆才想起来

容器日志不清理,宿主机磁盘迟早被写满,这是个经典事故。尤其是那些高并发的应用,一天写几个 GB 日志很常见。等你发现磁盘满了,容器可能早就因为写不进去日志而退出,这又是一个“查已退出容器日志”的场景,只不过这次排查的入口是磁盘告警。

Docker 日志轮转是在 daemon.json 里全局配置的:

{ "log-driver": "json-file", "log-opts": { "max-size": "100m", "max-file": "3" } }

这个配置就是日志文件单个最大 100MB,最多保留 3 个文件,超过上限就会滚动清理。配置修改后需要重启 docker daemon:

systemctl restart docker

注意:daemon.json 里的配置只对之后新建的容器生效,已经存在的容器不会自动套用新配置。所以要么新建容器,要么修改已有容器的启动参数(只能删了重建)。

单容器也可以在创建时指定:

docker run -d --log-opt max-size=100m --log-opt max-file=3 --name app your_image

这里特别提醒:日志轮转只影响 stdout/stderr 路线上的日志,也就是 docker logs 能看到的那部分。容器内应用自己写文件的日志(比如 Java 应用写 /var/log/app.log)不归 docker 管,要用应用自身的日志框架或者挂载目录 + logrotate 去清理。两者别搞混了。

4. 容器被删除了日志还能找回吗

4.1 最坑的 --rm 参数

有些朋友图省事,启动容器时习惯加 --rm:

docker run --rm your_image

--rm 的含义是:容器退出时自动把容器删掉,连同它的文件系统、配置、日志全部清理干净。这个参数在临时调试时很香,但放在生产环境就是个巨大的雷。一旦容器异常退出,你连 docker ps -a 都看不到它,docker logs 更是直接报错“No such container”。日志去哪儿了?没了,被我清理掉了。

如果遇到报错:

Error: No such container: xxx

恭喜,你踩的就是这个坑。这时候唯一的办法去看能不能从外部日志系统(如果你当初配了日志收集)里捞出点东西。如果你至今什么都没配,那就只能当一次深刻教训了。

所以我的习惯是:生产环境容器一律不加 --rm,重要容器还要配合重启策略和日志挂载。加 --rm 省的那点磁盘空间,远不及排查问题时损失的时间。

4.2 日志目录被挂出来了吗

容器删除后,/var/lib/docker/containers/<容器ID>/ 目录通常也会被清理,json-file 日志文件跟着没了。但有两种情况日志还能救回来:

第一种,你把应用日志目录挂载到了宿主机上。比如启动时这么写的:

docker run -d -v /data/app/logs:/app/logs your_image

应用写入 /app/logs 的文件,实际落在宿主机 /data/app/logs 里。容器删了也不影响宿主机上的文件,直接去宿主机目录翻就行。

第二种,容器还没删,只是退出了。这种情况按第二部分讲的操作先 docker logs 导出,或者 docker cp 把容器内日志文件拷出来,然后再删容器。顺序千万别反。

我一般建议上线前就把重要容器的日志目录挂载出来,这个习惯在排查问题时受益无穷。出问题直接进目录 tail -f 或者 grep,比 docker logs 顺手得多。

4.3 Docker Desktop 用户怎么看到宿主机文件

Windows / macOS 上跑 Docker Desktop,容器的 /var/lib/docker 目录在它内置的虚拟机里,不是直接暴露在 Windows 文件系统里的。所以你想直接去宿主机找 json-file 日志文件,路径没那么直观。

但好消息是,docker logs、docker inspect 这些命令是经过虚拟化层封装的,你在 Windows 终端里照样能跑,所以日常排查 Docker Desktop 环境下的已退出容器日志,直接用 docker logs 即可,不需要折腾底层文件。

如果你一定要访问虚拟机里的 /var/lib/docker(比如日志驱动是 json-file 但 docker logs 因为某些原因坏了),Docker Desktop 基于 WSL2 的环境可以这样进入:

wsl -d docker-desktop

进去之后你就能看到 /var/lib/docker/containers 目录了。不过说实话,我基本不会这么做,太绕了,能靠 docker logs 解决的问题不值得浪费这个时间。

Windows 下的一个额外提醒:cmd 和 PowerShell 默认的代码页不太兼容 UTF-8 日志,中文乱码的情况很常见。看日志前可以先执行:

chcp 65001

把代码页切到 UTF-8,乱码能缓解很多。搜文件内容的时候尽量不要用 findstr,它对 UTF-8 的中文支持也不行,建议用 grep(Git Bash 环境)或者直接导出文件后用编辑器打开检索。

5. 常见问题与排查技巧实录

5.1 一个完整的排查过程:从退出到定位

拿一个我实际遇到过很多次的场景举例:某 Java 服务容器退出码是 137。

第一步,发现容器退出:

docker ps -a | grep java-app

输出类似:

d3f8a1b2c3e4 java-app java -jar app.jar Exited (137) 2 minutes ago

第二步,确认退出码语义,137 对应 SIGKILL,优先怀疑 OOM:

docker inspect java-app --format '{{.State.OOMKilled}}'

输出 true,基本坐实是被 OOM Killer 杀掉的。

第三步,看容器死前日志,确认有没有业务报错:

docker logs --since 30m --timestamps java-app 2>&1 | tail -n 100

日志里可能看不到业务异常,只有正常输出到某一行就断了,这跟 OOM 的特征吻合。

第四步,去宿主机系统日志确认内存情况:

dmesg | grep -i "oom" | tail -n 20

或者更直接一点:

dmesg -T | grep -i "killed process"

能明确看到内核把 java 进程杀了。到此定位流程结束:不是应用代码问题,是容器内存限制太小。排查完调大内存限制,重新启动容器。

这套流程的关键在于:用退出码确定方向,用 docker logs 找业务线索,用系统日志找底层证据。以后你碰到类似问题,也可以照这个组合拳来打。

5.2 高频问题速查表

现象可能原因建议排查方式
docker logs 显示 No such container容器被 --rm 删除,或手动 rm 掉了查日志收集平台;恢复备份;避免 --rm
退出码正常 0,但业务看起来有问题应用可能只是进程退出了,业务状态没落盘看应用自身状态文件/数据库
docker logs 一直没有任何输出应用日志写到了文件而不是 stdout/stderr进容器查文件、docker cp 导出
容器退出码为 137OOM 或外部 killdocker inspect .State.OOMKilled,dmesg
日志文件太大,docker logs 卡死json-file 文件增长失控直接读宿主机文件,配置 max-size
日志中文字符乱码Windows 终端编码问题chcp 65001,导出用文本编辑器看
restarting 状态看不到日志输出容器启动即崩,一直没有 stdout结合 --since 10s 和 docker events 观察启动周期
容器频繁重启但 docker logs 时间戳很乱日志时间戳默认 UTC加 --timestamps,注意时区换算

5.3 几个值得长期坚持的好习惯

看完已退出容器日志这个动作本身很简单,但要把异常容器快速排查明白,背后拼的是平时习惯。我踩了几年坑,整理几个很值得长期坚持的做法:

第一,所有业务容器启动时都加 --name。名字是人类友好的,日志定位、inspect、restart 全都靠名字,别光记着一串随机 ID。

第二,重要容器一律加日志轮转参数,不管 Docker 默认有没有,自己显式写一遍:

docker run -d --log-opt max-size=50m --log-opt max-file=5 --name app your_image

这行参数不写,短时间看不出来问题,等出问题的时候,大概率是磁盘警报先响起。

第三,应用尽量把日志打到 stdout/stderr,而不是只写文件。因为 stdout/stderr 是 Docker 生态的统一语言,docker logs、journald、日志采集器全都在这条线上收集。应用写文件当然也有价值,但请务必两条腿走路,别只走一条。

第四,关键时刻要养成看时间戳的习惯。别用无时间戳的 docker logs 发呆看半天,泡杯茶的功夫一回头,发现看的日志是两小时前的。加 --timestamps 之后再配合 --since 过滤,效率完全两个级别。

第五,排查大门一旦打开,要按顺序走:docker ps -a 找容器,docker inspect 看退出码,docker logs --since 看关键时间段的日志。上来就 docker logs 一把梭,很容易被海量日志淹没在噪声里,浪费时间。

做容器排查这些年,我最大的体会是:日志是容器留下的唯一“遗言”,退出码则是“遗言”的摘要。两者结合着看,大部分容器死亡原因都能在几分钟内定位。希望这篇内容能帮你少走点弯路,下次再遇到已退出的容器,冷静打开 docker logs,问题往往就在那最后几十行日志里等着你。

返回列表