Docker 磁盘空间爆满排查:overlay2 清理实战

某天半夜收到磁盘告警,登录服务器执行 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 远程容器 开发环境里,遇到磁盘占用异常,也可按同样思路下钻排查。记住一句话:删之前先看、再删低风险、最后才碰数据卷。

上一篇 Redis 核心数据结构实战:五种类型用法与选型
下一篇 Java 反射实战:从 Class 对象到框架底层原理
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]