上周有台跑 Docker 的机器突然开始报「no space left on device」,我第一反应是镜像拉多了,结果 df -h 一看,40G 的数据盘已经用了 39.6G,du -sh /var/lib/docker/* 一层层挖下去,真正吃掉磁盘的既不是镜像也不是容器层,而是 /var/lib/docker/containers/ 下面一串 -json.log 文件单个文件 11G,日期还是三个月前的。整个排查过程不到二十分钟,但结论让我有点意外:这台机器从头到尾没配过任何日志限制,Docker 默认把容器的标准输出和错误输出全部写进本地文件,而且永不自动清理。今天这篇就把 Docker 日志这块彻底捋一遍:日志到底存在哪、为什么会撑爆磁盘、json-file 驱动必须配哪几个参数,以及已经写满的磁盘怎么安全清理。
如果你正准备给容器服务配机器,或者手上这台机器磁盘本来就紧张,这两家可以先看看:spinservers(低价云服务器,全栈托管与定制私有云部署)、digvps(服务器测评平台,性能与网络实测数据可查)。容器跑多跑少最终都要落到磁盘和带宽上,机器的底子打好,日志策略那点小事就不值得半夜爬起来救火。

先说结论:默认的 json-file 驱动会把每个容器的日志写成一个本地文件,路径是 /var/lib/docker/containers/<容器完整ID>/<容器完整ID>-json.log。完整 ID 是 64 位十六进制,一般人根本记不住,所以查的时候直接用容器名反查:
docker inspect --format='{{.LogPath}}' 容器名
这个路径在 docker info 输出的 Docker Root Dir 下面,默认是 /var/lib/docker。这条路径很关键,因为很多人在排查磁盘占用时习惯性去看 /var/lib/docker/overlay2(镜像层)和 /var/lib/docker/volumes(数据卷),结果发现加起来也没多少,就以为搞错了方向。真正的大头往往就在同级那个 containers 目录里。
顺带说清楚日志是怎么进去的:容器里进程写 stdout 和 stderr,Docker 的 json-file 驱动 把每一行包装成一条 JSON 记录(包含 log、stream、time 三个字段)追加写入那个文件。它不区分你的应用打了普通日志还是打了报错,对它来说都是同一类数据流,区别只是 stream 字段是 stdout 还是 stderr。

关键在于 json-file 驱动的默认行为:只写不删。它不会检查文件大小,也不会做数量限制,更没有保留天数这个概念。只要容器还在跑,日志文件就一直在追加;哪怕容器已经停掉、已经被删掉,文件也还留在磁盘上,除非你手动清理。
常见的几种爆盘场景:
一、无限循环的报错刷屏。某个容器因为配置错了,每秒重试一次并打十几行堆栈,一天下来轻松几百 MB。这是最凶的一种,量级完全取决于错误频率。
二、正常但高频的业务日志。访问量大的站点、网关、消息队列消费者,每秒几百行很正常,一天几个 G 也不夸张。
三、已经退出的容器残留。容器删了,日志文件留在磁盘上;跑过几百个一次性任务机器之后,几十 G 的僵尸日志堆积在那儿,谁也不会注意到。
四、把日志目录当成备份盘用。有人图省事,直接把宿主机目录挂进容器里写日志文件,完全绕过了 Docker 的日志机制,这种更是查不到源头。
我在排查时习惯先跑这一条,一眼就能看出谁是元凶:
sudo du -sh /var/lib/docker/containers/* | sort -rh | head -10
再配上 docker ps -a --size 看容器列表,如果某个容器的 SIZE 大得离谱,基本可以断定就是它。

Docker 的日志行为是在 daemon 级或者 容器级配置的,容器级的 log-opts 优先级更高。四个核心参数如下:
max-size(单个文件最大体积):超过这个值就切分。单位支持 k、m、g,比如 100m。这是最关键的一个,不配它等于不设上限。
max-file(最多保留几个文件):切分出来的文件保留数量,比如 3。和 max-size 一起算总占用:max-size × max-file 就是这个容器日志的理论天花板。100m × 3 = 300M,一个容器封顶 300M,十个容器就是 3G,账很好算。
labels(日志标签):给日志打标签,配合 docker-compose 或者 logrotate 方案做分组管理,属于进阶用法,日常可以不配。
compress(是否压缩轮转后的文件):开启后旧文件会被 gzip 压缩,能再省 60% 左右的文本日志空间。代价是压缩期间该文件不能直接 tail 看内容,需要先解压。追求极致省盘可以开,日常用不着。
临时给一个容器加限制,用 docker run 的 --log-opt:
docker run -d --name web --log-opt max-size=100m --log-opt max-file=3 nginx
如果想一次性改掉所有新建容器的默认行为,改 /etc/docker/daemon.json:
{ "log-driver": "json-file", "log-opts": { "max-size": "100m", "max-file": "3" } }
改完 systemctl restart docker 重启守护进程。注意这只会影响重启之后新建的容器,已经在跑的容器不会追溯生效,这是很多人配完发现「怎么没效果」的原因。想让老容器也生效,只能删掉重建(docker compose up -d --force-recreate),重建前记得确认数据都放在 volume 或挂载目录里。
如果用 docker-compose,写法是这样:
logging:
driver: json-file
options:
max-size: "100m"
max-file: "3"

磁盘满着的时候最忌讳的就是乱删。这里有几个安全的姿势,按风险从低到高排:
方式一:直接清空正在跑的容器日志文件。用 truncate -s 0 而不是 rm。因为容器进程还持有这个文件的文件描述符,rm 掉之后空间不会立刻释放,文件描述符还指着已删除的 inode,磁盘空间等于白删;truncate 把文件截断到 0 字节,句柄还在,inode 也还在,但空间立刻回来了。
sudo truncate -s 0 /var/lib/docker/containers/<ID>/<ID>-json.log
方式二:只清理已经停止的容器日志。停掉的容器不占着句柄,直接删文件是安全的:
docker ps -aq --filter status=exited | xargs -r docker rm
方式三:全局清掉所有不再被引用的日志。Docker 自带 container prune,只删停止的容器,不动运行中的:
docker container prune
配合 docker system df 可以先看清各类资源各占多少再决定下刀。
方式四:改用 local 驱动 + logrotate。这是目前更推荐的长期方案。local 驱动比 json-file 更省空间(内部做了分块存储,不会因为一个文件切分而浪费整块空间),而且原生支持 max-size、max-file、compress。改 daemon.json 把驱动换成 local 即可:
{ "log-driver": "local", "log-opts": { "max-size": "50m", "max-file": "3", "compress": "true" } }
如果日志量特别大、轮转频率高,还可以让应用自己把日志写到挂载出来的目录里,再用 logrotate 按天或按大小切割,然后彻底不用 Docker 的日志驱动。这种方式最可控,代价是应用需要支持写文件。
一、docker logs 会把内容全部读进内存。一个 11G 的日志文件,执行 docker logs 容器名 有把内存吃爆的风险。排查时用 docker logs --tail 100 或 --since 1h 限制范围,别上来就全量输出。
二、日志驱动不等于日志采集。json-file 和 local 都只解决「写多少、留多久」,真正要做集中检索还得靠 Filebeat 之类的采集器发到 Elasticsearch。两者是并行的,不是替代关系很多团队之前写过 Docker 日志分析的文章,其实前提就是先在 daemon.json 里配好 size 限制,否则采集器本身也会被磁盘写满的日志拖垮。
三、磁盘水位要配告警。最有效的防护不是事后清理,而是提前知道。可以设一条规则:df 使用率超过 85% 就告警,配合 docker system df 的量级变化做趋势判断。这一条能挡掉九成以上的同类事故。
最后把要点收一下,下次遇到同类问题按这个顺序走:
1. df -h 确认是哪个分区满了,看是 / 还是 /var 还是数据盘。
2. du -sh /var/lib/docker/* | sort -rh 看 Docker 目录里谁最大,重点看 containers。
3. docker ps -a --size 找出日志最大的容器,记下容器 ID。
4. docker inspect --format='{{.LogPath}}' 容器名 拿到准确路径。
5. 运行中的容器用 truncate -s 0,停止的容器直接 docker rm。
6. 检查 /etc/docker/daemon.json,把 max-size 和 max-file 补上,重启 Docker。
7. 用 docker compose up -d --force-recreate 重建容器让新配置生效。
8. 加磁盘水位告警。
整套下来其实不到二十分钟,唯一的门槛是知不知道日志文件在哪、删的时候会不会真释放空间这两件事。下次再遇到 no space left on device,先别急着删镜像,按这个清单走一遍,八成能直接定位到元凶。