先说结论:/var/lib/docker/containers 目录越来越大,十有八九是容器标准输出日志(*-json.log)在无限增长,而不是容器运行时的数据文件。这个问题我在生产环境排查过很多次,也接过不少“磁盘告警”的现场,套路基本一致:日志文件不轮转、不压缩、不清理,时间一长就把磁盘塞满。严重的时候,不仅是 Docker 自身无法写入,宿主机上的其他进程也会一起遭殃。
这篇文章会把 /var/lib/docker/containers 这个目录拆开看清楚,解释为什么它会长大、影响范围到底有多大、怎么快速止血清理,以及怎么从根上配置日志轮转,让它以后不再爆。适合所有用 Docker 跑业务的开发者、运维同学,也是排查磁盘占用问题时必看的一份实操记录。
1. 目录里到底装了什么?先搞清楚结构再看占用
1.1 每个容器在 containers 下的“身份证目录”
/var/lib/docker/containers/ 下面通常会看到一堆以 64 位哈希值命名的目录,比如:
1a2b3c4d5e6f... 8f9a0b1c2d3e...每一个目录对应一个容器,里面存的是这个容器的运行状态和配置信息,不是容器直接打包的镜像层内容。你如果进到某个哈希目录里,会看到这些文件:
- config.v2.json:容器的配置信息,比如环境变量、挂载点、启动参数,是 Docker daemon 启动容器时读取的核心文件。
- hostname、hosts、resolv.conf:容器启动时注入的网络和主机配置文件。
- mounts:容器挂载点的描述信息。
- shm:容器 /dev/shm 对应的临时文件,默认大小 64MB,由 --shm-size 参数控制。
- 主目录里最显眼的,是 <容器ID>-json.log:这是容器标准输出(stdout/stderr)的落盘日志文件。
所以先记住一个结论:containers 目录大,通常就是 -json.log 这个文件大。其他文件基本上都是 KB 级别,就算内容再多,也很难超过几十兆。真正能把目录撑爆的,永远是日志。
1.2 一条命令定位“重量级选手”
靠猜没用,先量化。我常用的排查命令是:
du -sh /var/lib/docker/containers/*/ | sort -rh | head -20这一步能快速找出哪些容器目录占用最大。然后进入对应目录,再看看到底是哪个文件撑的:
ls -lhS /var/lib/docker/containers/<容器ID>/看到的场景通常是:
-rw-r--r-- 1 root root 12G Dec 20 10:33 <容器ID>-json.log -rw-r--r-- 1 root root 4.0K Dec 20 10:33 config.v2.json -rw-r--r-- 1 root root 1.1K Dec 20 10:33 hostname如果 -json.log 显示几 GB,甚至几十 GB,问题就锁定了。还可以用 docker inspect 拿到日志路径和日志驱动信息:
docker inspect -f '{{.HostConfig.LogConfig.Type}} | {{.LogPath}}' <容器ID>如果输出的是 json-file,说明用的就是默认日志驱动,日志会一直追加写本地文件,不做任何自动截断。
注意:这里有个容易混淆的点。容器运行的层数据,也就是真正的可写层,存放在 /var/lib/docker/overlay2 下,不在 containers 目录里。所以你在 containers 目录里看到的体积,不等于“容器实际数据体积”,它只是配置、挂载描述和日志的体量。
2. 日志为什么会无限增长?根因和影响范围
2.1 json-file 日志驱动的工作机制
Docker 默认的日志驱动是 json-file。容器里进程往标准输出(stdout)和标准错误(stderr)写内容时,Docker daemon 会把每一行封装成 JSON 结构,追加写到 <容器ID>-json.log 文件里。
比如容器里跑着一个服务,日志输出长这样:
2025-01-12 10:00:01 INFO request handled 2025-01-12 10:00:02 ERROR upstream timeout落盘到 -json.log 的实际上是 JSON 结构:
{"log":"2025-01-12 10:00:01 INFO request handled\n","stream":"stdout","time":"2025-01-12T10:00:01.123456789Z"} {"log":"2025-01-12 10:00:02 ERROR upstream timeout\n","stream":"stderr","time":"2025-01-12T10:00:02.987654321Z"}默认情况下,这个文件没有大小上限、没有轮转、没有清理策略,只会一直往下写。很多容器镜像的启动脚本又是直接打印日志到 stdout,不写文件,也没有外部日志采集来消费,时间一长就变成磁盘炸弹。
2.2 影响范围:从磁盘告警到服务不可用
日志膨胀的影响不是“磁盘多占点”这么简单。真实场景下我看到过以下几种连锁反应:
- 根分区被占满,系统级服务先出问题。如果 Docker 数据目录在 /var/lib/docker,日志写满根分区后,不仅是容器写不进数据,连 tmp 目录、需要写日志的系统服务、SSH 登录后的临时文件写入都会报错。
- 容器内的写操作开始报 file full。别以为只要日志文件在宿主机上,容器里就没事。日志把磁盘占满后,容器内即使是往自己的数据卷或者临时目录写文件,也会碰到 No space left on device。
- docker logs 命令变得极慢。-json.log 大了以后,docker logs 需要读取并解析整个文件,几十 GB 的文件会让命令卡到怀疑人生。
- inode 风险:如果整个 /var/lib/docker 下有过量小文件,比如大量容器各自创建了多层 overlay 目录,哪怕磁盘还有剩余,inode 先耗尽,也会报同样的 No space left on device。containers 目录本身小文件不多,但日志轮转如果要留多个历史文件,也要注意文件个数。
2.3 哪些场景最容易踩中
我总结下来,高发场景集中在:
- 长时间压测环境:压测任务直接打印请求日志,几小时就能写出几 GB。
- 没有接入集中式日志系统的小团队:日志只靠 docker logs 查看,又懒得搭采集服务。
- 依赖库的默认日志输出没关:比如很多框架启动时会疯狂打印调试日志,跑在新版本容器里就攒成巨无霸。
- 容器长期不重启:日志文件从未被重建或截断,越攒越大。
所以排查这个问题时,不要在“清理”本身纠结,要先想清楚“为什么日志会这么大”,后面才谈得上治理。
3. 先止血:安全清空运行中的容器日志
3.1 清空日志文件的正确姿势
找到了嫌疑容器和大日志文件,很多人的第一反应是直接删:
rm -f /var/lib/docker/containers/<容器ID>/<容器ID>-json.log注意,这个操作对运行中的容器几乎没用。删掉文件之后,Docker daemon 还握着这个文件的文件描述符(fd),进程继续往这个已删除但未释放的文件里写数据。磁盘空间不会立刻释放,df 看到的剩余空间还是没少,甚至容器还能继续把数据写进“幽灵文件”。
正确的止血姿势是用 truncate 截断文件,而不是删除文件:
truncate -s 0 /var/lib/docker/containers/<容器ID>/<容器ID>-json.log这样文件大小变成 0 字节,但因为文件路径还在,Daemon 的文件描述符继续有效,后续日志还能正常写入。这是我在生产环境实测最稳的方式,不需要重启容器,也不需要停服务。
再自动化一点,先把日志路径全部取出来,然后循环截断:
for log in $(find /var/lib/docker/containers -name "*-json.log" -size +100M); do truncate -s 0 "$log" echo "truncated: $log" done加上 -size +100M 条件,只处理超大日志,避免误伤正常的、当前正在写的小日志。
注意:截断正在被读写的大日志文件,对容器本身的影响很小,理论上最坏情况是容器输出的某一瞬间写完 fd 时,文件还处于截断和写入交错的状态,不会导致容器崩溃。如果你的环境极其敏感,也可以选择重启容器,但重启的代价往往比截断日志大得多。
3.2 其他常见“止血”动作
如果日志文件太多,或者容器已经没用了,直接删除容器也是个选项:
docker rm -f <容器ID>容器删除后,containers 目录下对应的哈希目录会被清理,日志文件也会一起消失。这对已退出但占着日志空间的容器特别有效。但注意:docker rm 和 docker container prune 都不会动正在运行的容器日志。
还有一种情况是 shm 文件过大。默认 --shm-size 是 64MB,但如果你在容器里跑了需要共享内存的应用,比如大数据组件、一些特殊数据库,可能手动把 shm 调到了几个 GB,那 shm 文件也会在 containers 目录里占据大量空间。这种文件不能乱删,它是挂载在容器 /dev/shm 上的临时文件,只能通过调整容器的 shm 配置并重建容器来解决。
综合来看,先预估风险,再选择动作:
| 场景 | 操作 | 是否重启容器 |
|---|---|---|
| 运行中容器日志过大 | truncate -s 0 | 否 |
| 已退出容器占用空间 | docker rm / docker container prune | 否 |
| shm 设置过大 | 修改 --shm-size,重建容器 | 是 |
| 容器损坏,无法读写 | docker rm -f 后按需重建 | 是 |
3.3 踩坑:docker logs --tail 不能清理日志
有同事问过:我用 docker logs --tail=100 <容器ID> 是不是就能只留最后 100 行,把前面日志清掉?
不行。docker logs --tail 只是控制读取输出的行数,相当于“只显示最后 N 条日志”,并不会删除文件里的历史内容。同理,docker logs --since 也只是过滤读取,不影响落盘文件大小。这个误解很常见,一定要纠正过来。
4. 从根上治理:配置日志轮转,而不是每次手动清
4.1 修改 Docker daemon 全局日志配置
手动 truncate 只是应急。治理的唯一正解是开启 json-file 日志轮转,让 Docker 自动切割、自动清理老日志。
在 /etc/docker/daemon.json 中配置:
{ "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }参数含义:
- max-size:单个日志文件最大体积,超过后 Docker 会按数字递增,比如 -json.log、-json.log.1、-json.log.2。
- max-file:最多保留几个日志文件,超过后最老的会被删除。
- 组合效果:单容器最多占用约 30MB 日志空间,而不是无限制增长。
改完后重启 Docker 服务:
systemctl reload docker注意,这里我用的是 reload,不是 restart。reload 能避免重启所有容器,但对某些系统版本,daemon.json 的 log-opts 改动需要完全重启 Docker 才能生效。如果你的环境对容器可用性要求高,建议先在测试环境验证 reload 后新容器的日志配置是否生效。
4.2 这个配置只对新建容器生效
重点来了:修改 daemon.json 里的 log-opts,不会让已经创建的容器自动使用新配置。容器创建的时候,日志驱动和日志参数已经写进 config.v2.json 了。已经存在的容器,即使你 reload 了 Docker,它的 json-file 配置还是原来的无轮转模式。
想让已有容器也启用轮转,有两条路:
- 用 docker update 临时调整日志参数。注意,docker update 支持调整部分容器配置,但对 log-opts 的支持并不像 docker run 那样完整,不同版本行为有差异。实测下来,最稳妥的方式还是容器重建。
- 直接删除并重建容器。如果容器是无状态的,或者启动参数写在 compose 文件里,重建成本很低,这是最推荐的方式。
重建后验证一下:
docker inspect -f '{{.HostConfig.LogConfig}}' <容器ID>输出里应该能看到:
{json-file map[max-file:3 max-size:10m]}如果看不到这些参数,说明没有生效,回去检查 daemon.json 的 JSON 语法,或者确认 docker 服务已重启。
4.3 用 compose 管理日志参数的细节
如果用的是 docker-compose,建议把日志配置直接写在服务定义里,避免依赖全局 daemon.json:
services: app: image: your-image:v1 logging: driver: json-file options: max-size: "5m" max-file: "5"这样每个服务自带轮转策略,新增环境时不用额外配置。全局 daemon.json 适合统一下限兜底,compose 文件适合按服务定制,比如某些日志量特别大的服务可以调小 max-size。
4.4 更进一步:应用日志不走 stdout
轮转配置是治标,真正处理高并发日志场景,更好的方案是让应用把日志直接写到文件或用标准日志库管理,而不是全部喷到 stdout。因为 json-file 的底层是 JSON 文件,格式冗长,同样一条日志,落盘体积比纯文本多 30% 以上。
常见做法:
- 应用内配置日志库,按大小、时间自己轮转。
- 日志采集器直接收集容器内日志文件。
- 将 Docker 日志驱动改成 external,比如 syslog/journald,由系统统一管理。
不过在绝大多数中轻量场景里,json-file + max-size + max-file 已经足够,没必要为了一个日志治理去引入整套日志平台。
5. 排查过程中最容易踩的坑
5.1 删了日志文件,磁盘空间没释放
前面已经提到,运行中的容器日志文件被 rm 后,空间不会立刻释放。处理方式是用 truncate 而不是 rm,或者重启容器让 fd 释放。
如果已经误删,不要慌。比较快速的方法是在该容器目录下用 lsof 找到被占用但已删除的文件:
lsof +L1 | grep deleted看到容器进程还拿着已删除的 json.log,重启这个容器即可释放空间。但为了不丢数据,更推荐先想办法让容器平滑重启,再确认空间是否回落。
5.2 改了 daemon.json 没生效
先确认 JSON 语法:
docker info --format '{{.LoggingDriver}}'如果全局驱动还是 json-file,再看配置项有没有被正确载入。可以直接启动一个测试容器,inspect 它的 LogConfig。有些系统上 Docker 服务对 daemon.json 的改动不是热加载,需要 systemctl restart docker。我遇到过一次,改了 log-opts 只有新容器生效,要看老容器必须重启整个 Docker 服务。
这里要权衡:重启 Docker 服务等于重启所有容器,对无状态的部署影响不大,但对有状态的生产容器要谨慎安排。
5.3 docker system prune 为什么救不了你
docker system prune -af --volumes 可以清理悬空镜像、停止的容器、无用网络,但它不会处理运行中容器的日志文件。很多人磁盘满时先跑它,结果发现空间几乎没变化,原因就在日志根本不在这个清理范围里。
它更致命的一点是,-a 会把所有未被容器引用的镜像层全部删除,如果你需要回滚镜像版本,缓存被清了重建起来很慢。所以不要在磁盘告警时毫无准备地执行这个命令,先定位大文件,再决定哪些对象该回收。
5.4 别忽略容器目录里的“幽灵占位”
还有一种情况,用 du 统计 /var/lib/docker/containers 目录时,会发现某个哈希目录体积特别大,但进去看文件都很小。
这种情况通常是挂载点导致的。容器的 mount 配置里如果绑定了宿主机的大目录或文件,du 在统计这个目录时会把挂载内容算进去一部分,尤其是 bind mount 的目录。真正的容量在宿主机的某个业务目录里,不在 Docker 的数据里。
排查时用 du -h 查看每个子目录的实际大小,再对可疑目录用 du -shx 跳过其他文件系统:
du -shx /var/lib/docker/containers/*/这样能避免挂载点干扰统计。
5.5 inode 耗尽也要警惕
日志轮转配置好之后,会生成多个历史文件,比如 .log、.log.1、.log.2。如果你的容器特别多,每个容器都留 3 个文件,文件总数也不少,再加上 overlay2 层目录,inode 压力会变大。
巡检命令:
df -i /var/lib/docker如果 IUse% 很高,及时清理陈旧容器和悬空镜像,防止 inode 先于磁盘容量耗尽。
6. 日常巡检和监控脚本参考
有了轮转配置,不代表完全不看了。我习惯写一个轻量的巡检脚本,放到 cron 里定时跑,发现超大日志就报警或直接截断:
#!/bin/bash # 查找超过 500MB 的容器日志文件 file=$(find /var/lib/docker/containers -name "*-json.log" -size +500M) if [ -n "$file" ]; then echo "$(date), large docker logs found: $file" >> /var/log/docker_log_audit.log # 如果需要自动截断,放开下面这行 # echo $file | while read f; do truncate -s 0 "$f"; done fi也可以配合磁盘用量监控,当 /var/lib/docker 所在分区超过阈值时执行清点。脚本本身不复杂,核心价值是让你有机会在日志撑爆磁盘之前就发现问题。
另外建议做一次数据目录体检,确认 Docker 数据盘的容量规划。日志轮转后的空间上限可以估算:单容器日志上限 = max-size × max-file,比如 10m × 3 = 30MB。再乘上容器数量,就能算出日志最多会占用多少空间,给容量规划提供依据。
7. 写在最后的几点体会
/var/lib/docker/containers 文件过大这个问题,看起来很简单,但每次现场排查时,大家的第一反应往往不是去看日志配置,而是想办法扩容或盲目删数据。我在实际处理中感受最深的几点:
- 先看日志驱动,再看日志文件大小,最后才考虑改配置。顺序反了会走很多弯路。
- truncate 比 rm 更安全。运行中容器删日志文件这个坑,几乎每个团队都会踩一次。
- 日志轮转配置要写在模板和 compose 文件里,而不是只体现在当前这台机器上。新环境首次部署时最容易漏。
- 定期巡检日志体积,尤其是流量高峰期前后。一次流量突增就能把没有轮转的容器日志撑大几十 GB。
最后补充一个小技巧:如果你要对某个容器做日志治理,又不想重建容器,可以先 docker inspect 拿到当前 LogConfig,再用 docker update --log-opt max-size=10m <容器ID> 试着修改。不同 Docker 版本对 docker update 支持程度不一样,实测在较新版本上可以生效,但老版本可能报错。能用就先用,不能就计划重建,重要的是别让日志问题拖到磁盘彻底满掉那天。