某天半夜收到磁盘告警,登录服务器执行 df -h,发现根分区 / 使用率已经 100%,服务大面积报错 “No space left on device”。但用 du 逐层排查却找不到明显的大目录——问题其实藏在 /var/lib/docker/overlay2 里。Docker 磁盘空间爆满几乎是每个运维都踩过的坑:长时间运行后,它会悄悄积累镜像层、容器日志、构建缓存和匿名卷,最终把系统盘撑爆。本文以一次真实的救火为例,带你定位根因,并给出从应急清理到根治配置的完整方案。
一、故障现场:Docker 把根分区吃满了
典型现象是:监控系统(如自建的 Uptime Kuma 服务监控)先报磁盘使用率超阈值,随后 Nginx、MySQL 等依赖写盘的服务连锁异常。最迷惑人的地方在于,df -h 显示 / 满了,但 du -sh /* 加总却远小于分区大小。这并不是磁盘坏了,而是 Docker 的存储机制在”捣鬼”。
Docker 默认使用 overlay2 存储驱动,镜像是只读层、容器是可写层的联合挂载。由于写时复制(Copy-on-Write)特性,容器产生的临时文件、日志、缓存都会落到 overlay2 目录里。更麻烦的是,overlay2 的层叠文件会让普通 du 在统计时出现偏差,再加上 Docker 目录路径很深,很容易让人误以为空间”凭空消失”。直接看 Docker 自己的地盘最靠谱:
# 看根分区
df -h /
# 看 Docker 数据目录占用
du -sh /var/lib/docker
# 逐层下钻
du -sh /var/lib/docker/*
二、定位:到底是哪一类 Docker 资源在吃空间
别急着删,先用官方命令看清空间构成。docker system df 会分别列出镜像、容器、本地卷和构建缓存各自占用的空间,一眼看出是谁在膨胀。理解这四个桶的区别,是精准清理的前提:镜像层是应用的静态打包,容器是可写运行实例,卷是持久化数据,构建缓存则是编译过程的中间产物。
docker system df -v
2.1 镜像与悬空镜像(dangling)
悬空镜像指没有被任何标签引用、也没有被容器使用的游离层,常见于反复 docker build 后残留的 <none>:<none>。它们不会自己消失,而每一次构建都可能留下新的悬空层,日积月累就成为头号空间杀手。下面两条命令可以快速把它们揪出来:
# 列出悬空镜像
docker images -f "dangling=true"
# 按体积排序看最大的镜像
docker images --format '{{.Size}}\t{{.Repository}}:{{.Tag}}' | sort -h
2.2 容器日志(json-file 默认不限制大小)
若使用默认驱动 json-file 且没设置上限,一个疯狂打印日志的容器能在几小时内写出几十 GB 的 -json.log。这是最容易被忽略、也最容易瞬间爆盘的元凶,尤其在调试阶段开了 DEBUG 级别日志时。定位方法就是直接量日志文件大小:
# 找出最大的容器日志
sudo find /var/lib/docker/containers -name "*-json.log" -exec du -ch {} + | sort -h | tail -1
# 逐个查看
sudo du -sh /var/lib/docker/containers/*/*-json.log 2>/dev/null | sort -h | tail
2.3 构建缓存与 BuildKit
反复 docker build 会累积中间层,未打标签的构建缓存不会自动回收。在 BuildKit 模式下,这些缓存由独立的元数据管理,普通 prune 不一定能清干净,需要用 BuildKit 专用命令查看与清理:
docker builder du
docker buildx prune -f
2.4 匿名卷泄漏
容器删除时若没加 --rm 或显式 -v,挂载的匿名卷会被保留。数据库、缓存类容器最容易中招,卷里往往躺着已删容器的旧数据,既占空间又容易让人误以为是”重要数据”不敢删。用下面命令列出所有悬空卷:
docker volume ls -qf dangling=true
docker volume ls --format '{{.Name}}\t{{.Driver}}' | head
三、清理实战:从低风险到高风险
清理顺序很重要:先动”肯定能删”的,再动”可能重要”的。这样既能快速回收空间,又能把误删风险降到最低。原则是——永远别用 rm -rf 直接删 Docker 目录,而是让 Docker 自己的命令来决定什么能删。
3.1 第一步:docker system prune(安全起点)
prune 是最常用的回收命令。默认只清理悬空镜像、停止容器的网络和悬空镜像,不动正在使用的数据。按风险逐级加码,建议生产环境先用最保守的一档观察回收效果:
# 仅清理悬空资源(最安全)
docker system prune
# 加 -a 额外删除所有未被容器引用的镜像
docker system prune -a
# 再加 --volumes 清理匿名卷(谨慎,可能删数据)
docker system prune -a --volumes
| 命令 | 清理范围 | 风险 |
| docker system prune | 悬空镜像 / 停止容器网络 / 构建缓存 | 低 |
| + -a | 额外删除所有未被引用的镜像 | 中(需重新拉取) |
| + –volumes | 额外删除匿名 / 悬空卷 | 高(可能丢数据) |
3.2 第二步:截断失控的容器日志
prune 不碰日志文件。对正在写大日志的容器,先在线截断救急(不重启容器、不影响服务),等流量低谷再改配置根治。注意截断只是把文件大小归零,不会删除文件本身,因此是零风险的应急手段:
# 在线截断(不重启容器)
sudo truncate -s 0 /var/lib/docker/containers/<容器ID>/*-json.log
# 批量截断超过 100M 的日志
sudo find /var/lib/docker/containers -name "*-json.log" -size +100M -exec truncate -s 0 {} +
3.3 第三步:把 /var/lib/docker 迁到数据盘
如果系统盘本来就小(比如云服务器常见的 40G 系统盘),最根本的办法是整体迁移数据目录,把 Docker 从根分区挪到独立数据盘。这样即使镜像再多,也不会威胁到操作系统本身的运行。迁移步骤务必按顺序执行,避免数据损坏:
# 1. 停止 Docker
sudo systemctl stop docker
# 2. 修改 /etc/docker/daemon.json 指定新路径(见第四节)
# 3. 同步旧数据(保留权限)
sudo rsync -avP /var/lib/docker/ /data/docker/
# 4. 启动并验证
sudo systemctl start docker && docker system df
3.4 第四步:给自动更新装”刹车”
像 Watchtower 自动更新镜像 这类工具,若只拉不删,旧镜像会无限堆积。务必在自动更新后配合定期 prune,或在 CI 流水线里加清理步骤。一个稳妥的做法是让 Watchtower 更新后自动执行清理,而不是让它无节制地拉取新版本。
四、根治:从配置上让磁盘不再爆
应急清理只是治标,真正治本是改好 /etc/docker/daemon.json。上面那些”半夜救火”的场景,本质上都是因为日志没有上限、数据目录没有隔离。下面这份配置把单容器日志限制在 50M、保留 3 份,并把数据目录迁到数据盘,从机制上杜绝再次爆盘:
{
"data-root": "/data/docker",
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "3"
},
"storage-driver": "overlay2"
}
| 参数 | 作用 | 建议值 |
| log-opts.max-size | 单容器日志上限 | 50m |
| log-opts.max-file | 保留的历史日志份数 | 3 |
| data-root | 数据目录(迁到数据盘) | /data/docker |
| storage-driver | 存储驱动 | overlay2 |
改完执行 sudo systemctl restart docker 生效(注意会重启所有容器,建议在低峰期操作)。除了配置,监控兜底同样关键——可参考 自建 Uptime Kuma 监控 的思路,对 /var/lib/docker 所在分区设置 85% 使用率告警,把”救火”变成”预防”。当告警触发时,你已经有上面的清理脚本可以立即执行,而不是等到服务全挂才动手。
五、排查命令速查与避坑清单
为了让你在真正遇到问题时能秒级定位,我把最常见的四种现象、对应命令和根因整理成一张速查表。把它收藏起来,下次磁盘告警直接对着查即可:
| 现象 | 快速定位命令 | 常见根因 |
| df 显示满但 du 找不到 | docker system df | 镜像 / 卷堆积 |
| 容器日志巨大 | find … -name “*-json.log” | 未限制 json-file |
| 反复构建后空间涨 | docker builder du | 构建缓存未回收 |
| 删容器空间不释放 | docker volume ls -f dangling=true | 匿名卷泄漏 |
踩坑提醒:
- 不要用
rm -rf /var/lib/docker直接删除正在运行的 Docker,会损坏运行中容器与镜像;优先用 docker 官方命令。 prune --volumes会删除未被引用的卷,数据库数据可能随之消失,执行前务必确认。- 根分区爆满时连 Docker 自身都可能无法启动,建议一开始就把
data-root放在独立数据盘(也可回顾 Docker 入门实战 的目录规划)。
六、小结
Docker 磁盘空间爆满的根因无非四类:悬空镜像、失控日志、构建缓存和匿名卷。应急时用 docker system df 定位、prune 与 truncate 救火;根治靠 daemon.json 限高 + 监控告警。把这套流程固化进 CI/CD 流水线 定期清理,就能让磁盘长治久安;如果 Docker 跑在 VS Code 远程容器 开发环境里,遇到磁盘占用异常,也可按同样思路下钻排查。记住一句话:删之前先看、再删低风险、最后才碰数据卷。




