适用场景
Docker 默认使用 json-file 日志驱动。运行一段时间后,宿主机 /var/lib/docker 持续变大,最终出现磁盘告警,甚至应用写文件、创建临时目录或重启容器都失败。本文适用于单机 Docker、Docker Compose 以及未由集中日志系统完全接管容器标准输出的场景。
现象描述
一次线上告警中,业务分区使用率从 78% 在两小时内升到 98%。df 显示根分区空间不足,但按项目目录统计并没有对应的大文件;docker ps 中几个 API 容器仍在持续输出调试日志。更容易误判的是:即使删除了应用自己的日志文件,空间也没有立即回收。
典型影响包括:
- 容器启动时报
no space left on device; - 数据库或队列组件无法落盘;
- 日志采集、镜像拉取和
docker compose up失败; - 宿主机根分区满后,SSH 登录和 systemd 服务也可能异常。
先确认空间到底被谁占用
先确认 Docker 数据目录和实际的大文件,不要一上来就删除目录:
df -hT
docker system df -v
sudo du -xh --max-depth=2 /var/lib/docker | sort -h | tail -n 20
sudo find /var/lib/docker/containers -name '*-json.log' -type f -printf '%s %p\\n' | sort -nr | head -n 20
docker system df -v 能区分镜像、容器、卷和构建缓存;最后一条会按字节列出最大的容器标准输出日志。若最大的文件路径类似下面这样,基本可以确认是 json-file 日志:
/var/lib/docker/containers/<container-id>/<container-id>-json.log
再将容器 ID 映射为服务名,避免清理错对象:
docker ps -a --no-trunc --format 'table {{.ID}}\\t{{.Names}}\\t{{.Image}}'
docker inspect -f '{{.Name}} driver={{.HostConfig.LogConfig.Type}} config={{json .HostConfig.LogConfig.Config}}' <container-id>
如果 driver=json-file 且配置为空,说明该容器继承 Docker 默认配置,并没有设置轮转上限。
为什么删了文件,空间仍不回来
应用日志与容器标准输出日志不是同一件事。应用把日志写到 stdout/stderr 后,Docker 会写入 *-json.log。常见的两个坑是:
- 只在应用容器内删除文件,却没有处理宿主机上的
json.log; - 用
rm删除一个仍被进程打开的日志文件。文件目录项消失了,但已打开的文件描述符仍占用磁盘块。
可用下面的命令检查“已删除但仍被占用”的文件:
sudo lsof +L1 | grep -E 'docker|containerd'
输出中的 NLINK 为 0 表示文件已删除;SIZE/OFF 很大时,必须让持有该描述符的进程重开日志或重启,空间才会释放。
应急恢复:只截断已确认的大日志
生产应急时,不要删除 /var/lib/docker,也不要直接执行会影响所有业务的 docker system prune -a --volumes。确认目标日志对应的容器后,可截断该单个文件:
sudo truncate -s 0 /var/lib/docker/containers/<container-id>/<container-id>-json.log
df -h /var/lib/docker
truncate -s 0 保留原文件 inode,Docker 仍可继续写入,适合先快速释放空间。操作后检查容器是否仍健康:
docker ps --filter "id=<container-id>"
docker logs --tail 50 <container-id>
若日志增长极快,应同时将应用日志级别从 debug 调整为 info 或 warn,否则几分钟内可能再次写满。
永久修复:设置 Docker 默认日志轮转
在 /etc/docker/daemon.json 中配置默认日志驱动和轮转策略。已有文件时应合并 JSON,不能覆盖其中的 registry、data-root 等配置。
{
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "5"
}
}
含义是单个容器的当前日志最多 100 MB,保留当前文件加 4 个轮转文件,单容器日志约占用 500 MB。配置后先验证 JSON 格式并重启 Docker:
sudo python3 -m json.tool /etc/docker/daemon.json >/dev/null
sudo systemctl restart docker
docker info --format 'Logging driver: {{.LoggingDriver}}'
注意:Docker 的默认日志配置只对新创建的容器生效。使用 Compose 时必须重建容器:
docker compose up -d --force-recreate
docker inspect -f '{{json .HostConfig.LogConfig}}' <new-container-id>
对于需要更严格限制的高频日志服务,可以在 compose.yaml 里显式覆盖:
services:
api:
image: example/api:stable
logging:
driver: json-file
options:
max-size: "50m"
max-file: "3"
这里 max-size 和 max-file 必须写成字符串;Compose 会将它们传给 Docker 日志驱动。不要只改 daemon.json 而不重建旧容器,否则检查时仍会看到旧容器的 LogConfig 为空。
验证轮转是否真正工作
不要只看配置文件。可对一个保留的测试容器连续写入日志,再观察文件数量与总大小:
docker run -d --name log-rotate-check \\
--log-opt max-size=1m --log-opt max-file=3 \\
alpine sh -c 'i=0; while [ $i -lt 50000 ]; do echo "$(date) test-log-$i $(head -c 200 /dev/zero | tr "\\0" x)"; i=$((i+1)); done; sleep 3600'
docker inspect -f '{{.Id}}' log-rotate-check
sudo ls -lh /var/lib/docker/containers/<returned-id>/
docker rm -f log-rotate-check
实际验证中,目录内不应无限增加,只会保留当前日志和设定数量的轮转文件。生产环境还应通过监控确认:容器日志目录增速应被上限约束,分区使用率不再与单个请求异常成正比。
预防措施
- 为
/var/lib/docker所在文件系统设置 80%、90% 两级容量告警,并同时采集 inode 使用率; - 对请求体、响应体、SQL 全量参数等高基数字段禁止长期 debug 输出;
- 接入 Loki、ELK 等集中日志后,仍保留本地
max-size和max-file,它们是最后一道磁盘保护; - 将 Docker 数据目录单独挂载分区,避免容器日志写满根分区影响操作系统;
- 在发布流程中检查新容器的
HostConfig.LogConfig,把“配置存在”升级为“配置已生效”。
总结
Docker 日志写满磁盘的关键是先识别 json-file 的真实占用,再以截断单个确认文件的方式应急,最后通过默认配置和容器重建实现轮转。清理空间只能恢复现场;限制单容器日志大小、降低无效日志量并配置容量告警,才能避免同类事故反复发生。
Discussion
评论