如果你的服务器磁盘又被 Docker 塞满了,如果/var/lib/docker已经吃掉了上百 GB 空间,如果docker build缓存越攒越多,如果你盯着overlay2目录大得离谱却不敢动手——这篇内容就是给你准备的。
我自己的机器就经历过这个阶段:最开始只有几十 G 镜像,半年后磁盘告警,提示可用空间只剩 2%。第一反应是删日志,删完发现没缓解多少;再跑docker system prune,确实释放了几个 G,可过了一周又满了。最后把/var/lib/docker拆开逐个查,才发现大头集中在三块:/var/lib/docker/mnt里的残留挂载数据、反复构建留下的 build 缓存,以及overlay2目录里堆积的容器层和镜像层数据。题目标题里写的 “overlap”,我理解就是overlay2的笔误,这篇文章就按overlay2来聊。
接下来我把这三种空间吃法逐一拆开,讲清楚哪些能清理、哪些绝对不能碰、分别用什么姿势清,最后给一套可以直接照着跑、不太会翻车的完整流程。
1. Docker存储空间为什么越来越膨胀
1.1 /var/lib/docker 目录结构
Docker 的所有本地数据都集中在/var/lib/docker下,正常情况下你会看到这些子目录:
containers/:存放容器本身的配置、标准输出日志,容器 ID 命名的子目录里那个-json.log文件就是最典型的日志文件,单容器日志能涨到几十 G。image/:镜像的元数据,包括分层索引、配置、仓库 tag 信息,这里存的是“账本”,不是实际层内容。overlay2/:镜像层和容器可写层的实际文件数据,这是/var/lib/docker里体积最大、最容易膨胀的目录。volumes/:通过docker volume创建的匿名或命名卷,如果你的应用把数据写进卷里,这里会继续增长。build/或 BuildKit 缓存目录:docker build过程中产生的中间缓存层,CI 机器上特别容易膨胀。mnt/:老版本 Docker 或某些插件用作临时挂载点,也是我们后面要单独处理的目录。tmp/:Docker 在运行过程中的临时目录,崩溃或异常退出时可能残留大量临时文件。plugins/:Docker 插件存储目录,一般占用不大,但部分插件日志可能异常。
先说结论:正常 Linux 环境里overlay2是最大的,其次是containers里的日志,再就是 build 缓存。如果你发现mnt特别大,那通常是异常情况,属于不正常的膨胀,不能放着不管。
1.2 overlay2 与写时复制机制
要理解overlay2为什么会越来越大,先得明白 Docker 镜像的分层机制。
Docker 镜像由只读层组成,每一层对应 Dockerfile 里的一条指令,比如FROM、RUN、COPY。构建时,每一层都会被单独保存为一份文件系统快照。容器运行的时候,Docker 会在这些只读层上面叠加一个可写层。读取文件时先去可写层找,找不到就去底层找;修改文件时执行“写时复制”:先把这个文件从只读层复制到可写层,再在可写层里修改。底层只读层始终保持不变。
这套设计的好处很明显:多个容器可以共享同一份底层镜像,不用每个容器都复制一份完整文件系统。但坏处也在这里——只要你不删除镜像、不清理悬空层,历史层会一直躺在overlay2里。你更新了镜像的某个依赖,理论上只新增一层,但旧的层不会被自动删掉,而是等镜像被清理时才回收。如果你频繁更新镜像、频繁重启容器,overlay2会以肉眼可见的速度膨胀。
容器退出之后,它的可写层不会立刻消失。只有docker rm删除容器时,这个可写层数据才会被标记为可回收。很多人长期跑docker run而不加--rm,容器明明不用了还留在那里,对应的可写层就一直占着磁盘,这就是日常磁盘越来越满的第一个隐藏原因。
1.3 build缓存是怎么积累的
docker build的过程其实就是在不断叠加层。每条能产生文件变化的指令都会生成一个层,并且 Docker 会把这些层作为缓存保存下来。
缓存的逻辑大致是这样:下一次构建时,如果指令和上下文没有变化,Docker 会直接复用上一次生成的缓存层,跳过这步执行,大幅缩短构建时间。听起来很好,但代价是每一个被缓存过的层都真实占用磁盘空间。
举个实际例子:一个项目每次构建都执行RUN apt-get install,第一次构建生成一个大约 200 M 的层,被记入缓存。之后你改了代码重新构建,前面几步缓存可以命中,但RUN这步如果命令文本没变,缓存会继续沿用,不会产生新层。可如果你改了镜像基础版本、换了安装包版本,或者调整了环境变量,缓存失效,就会在原缓存之外继续生成新的层。旧的缓存层不会因为“失效”而自动删除,它会变成悬空缓存,继续占着磁盘。CI/CD 环境尤其明显:代码变更多、构建频繁,缓存目录膨胀速度远超生产服务器。
所以,build cache 不是纯良性缓存,它是用磁盘空间换构建速度。当磁盘吃紧时,优先清理对象之一就是它。
2. 先弄清楚空间都去哪了:诊断三板斧
2.1 docker system df 快速定位
动手清理之前,先用官方命令看清全貌:
docker system df输出会显示 Images、Containers、Local Volumes、Build Cache 四类的总数、活跃数、总大小和可回收大小。重点看最后一列RECLAIMABLE,这就是你理论上能释放多少空间。
实践经验告诉我,这条命令的价值不只是看总大小,而是看“活跃”与“总量”的对比。如果 Images 总量 60 G,但 ACTIVE 只有 3 个镜像,说明大量旧版本镜像堆在那里没用,可以放心清理。如果 Containers 总量 20 G,ACTIVE 只有 1,说明停止状态的容器文件系统还在占用大量空间,docker container prune就能回收。
Build Cache 的 RECLAIMABLE 百分比经常很高,因为缓存更新频繁,旧缓存一旦失效就不会再被使用。看到这里数值大,基本可以优先清理。
2.2 用du逐层排查大目录
docker system df汇总的是 Docker 视角的数据,但有时候实际占用量和它统计的结果对不上,因为历史遗留文件、异常挂载、孤儿目录并不会计入 Docker 的统计口径。这时候就要用系统命令逐层查了。
先看/var/lib/docker根目录:
sudo du -sh /var/lib/docker/*这个命令会列出每个子目录的真实大小。注意第一次执行时可能比较慢,因为要遍历大量文件,耐心等它跑完。跑完后再针对最大的两个目录往下钻:
sudo du -sh /var/lib/docker/overlay2/* sudo du -sh /var/lib/docker/containers/* sudo du -sh /var/lib/docker/mnt/*overlay2里每个子目录代表一个镜像层或容器层,体积从几 M 到几 G 不等。如果某个层体积特别大,先别急着删,用docker ps -a确认它是否属于正在运行的容器。判断方法一般是通过查看目录名与容器挂载信息,但更省事的做法是先进入容器内确认数据是否有用,再决定去留。
这里有个小技巧:用du -x --max-depth=1能限制统计深度,只显示一层,速度会快很多,适合首次侦查。
2.3 日志文件是隐形杀手
很多人清理 Docker 空间时只盯镜像和容器,忽略了一个最容易被忽略的隐性占用:容器日志文件。
默认情况下,容器标准输出和标准错误会写到/var/lib/docker/containers/<容器ID>/<容器ID>-json.log,而且这个文件不会自动滚动、不会自动压缩、也不会自动清理。一个打印频繁的容器,跑一天就能写几个 G 日志。我见过最夸张的一次,一个 Java 服务的容器日志涨到 40 多 G,把数据盘直接撑满。
检查方式:
sudo du -sh /var/lib/docker/containers/*/*-json.log处理办法我会在后面专门讲,总之遇到磁盘告警,日志第一优先排查。很多人一上来就清镜像,清了半天释放的空间不够,其实大头全在日志上。
3. 清理/var/lib/docker/mnt 目录
3.1 mnt目录到到底在干什么
/var/lib/docker/mnt在不同的 Docker 版本和使用场景下角色不完全一样。早期 Docker 版本用它作为设备挂载和临时文件系统的父目录,部分卷操作、容器挂载操作会在这里创建临时目录。也有插件或外部存储驱动把数据挂到这里。
如果你在正常使用的环境中发现这个目录异常变大,通常不是正常的“数据存储”,而是某种异常残留。比如某个容器在启动过程中执行挂载操作时被强制停止,挂载点没有正常回收;又比如某个插件崩溃后留下的临时文件没有清理。
这里必须提醒一句:mnt目录不是用户数据存储目录,正常情况下它不该持续增长。如果你看到里面有大量内容,先确认它到底是哪些进程在用,再决定怎么处理。直接rm -rf /var/lib/docker/mnt/*不是不行,但前提是你已经确认没有活动容器依赖它。
3.2 什么时候mnt会异常膨胀
最常见的膨胀场景有三个。
第一个是容器异常退出。容器运行中涉及文件系统挂载时,如果宿主机强制重启或 Docker 服务被 kill,挂载点记录会残留在/var/lib/docker/mnt下,这些临时目录不会再被使用,但文件还在,持续占用磁盘。
第二个是使用了某些存储插件或共享卷方案。比如部分 Kubernetes 部署的 volume 插件、本地持久卷控制器,会在/var/lib/docker/mnt下留下映射目录。某些插件版本有内存泄漏级 bug,会不停地创建新目录而不清理旧目录,这也是这个目录膨胀的典型原因。
第三个是手动挂载操作踩坑。有些人直接在/var/lib/docker/mnt下面创建目录挂载宿主机路径,容器删除后挂载目标还在,形成残留。
判断是否异常的经验方法:先统计 mnt 目录内各子目录的修改时间和大小。如果修改时间集中在系统重启或容器大面积异常退出那几个时间点,基本可以确定是残留;如果修改时间持续变化,说明有活跃进程在使用,那就不能随便清理,得先找到对应进程。
3.3 安全清理mnt目录的操作流程
清理mnt目录最关键的一步是先确认没有活动容器引用。我的操作顺序大致是这样:
先重置 Docker 的 Lab 状态,然后把涉及挂载目标的容器全部停止并删除,再验证 mnt 里的挂载是否随之消失。如果挂了文件系统,先卸载再清理文件。
如果执行umount /var/lib/docker/mnt/xxx报target is busy,说明还有进程占用,不要强制卸载,先找占用进程:
sudo lsof +D /var/lib/docker/mnt sudo fuser -m /var/lib/docker/mnt/xxx找到占用进程后正常处理,确认没有活跃占用,再执行卸载和删除。删除用:
sudo rm -rf /var/lib/docker/mnt/*删除后建议重启 Docker 服务,让 Daemon 重写挂载状态:
sudo systemctl restart docker这里我有一个习惯:清理mnt之前先给 Docker 目录做一个磁盘快照备份,或者至少把du统计结果保存下来做对比。毕竟mnt和overlay2都涉及挂载状态,操作不当可能导致容器文件系统不可用。求稳永远比求快重要,生产环境尤其如此。
4. 清理Docker build缓存
4.1 build缓存的原理与分类
build cache 可以理解为“为了让你下次构建更快而留下的中间产物”。它并不是一个单一目录,而是若干层的集合。每条 Dockerfile 指令生成了一个层,那些不再被任何镜像引用的层,就是可回收的 build 缓存。
BuildKit 架构下,缓存数据存放在独立的存储区域里,与镜像层分开管理,这也是为什么docker system df把 Build Cache 单列一类的原因。它仍然占用真实磁盘空间,但在docker images里看不到,容易让人忽略。
按生命周期分类,build cache 大体有三类:仍然能被后续构建复用的有效缓存、因为基础镜像更改而失效的旧缓存、构建过程中虽然成功但不再被引用的临时层。后面两类都没有保留价值,是清理的主要对象。第一类虽然有用,但也不是非留不可,清掉后最多是下次构建变慢,不会影响功能和正确性。
4.2 清理命令:builder build 与 system 的正确姿势
清理 build 缓存最简单直接的方式:
docker builder prune -f这会回收所有不再被引用的缓存层,效果立竿见影。如果你想看可回收空间,先执行:
docker builder prune --dry-run它会列出可回收条目和预计释放空间,但不实际删除,方便评估。
如果你用 BuildKit,还可以加过滤器按时间清理,比如只清理保留时间超过 24 小时的缓存:
docker builder prune -f --filter "until=24h"这个命令在 CI 环境非常实用:每天跑一次清理,把超过 24 小时的缓存清掉,既不破坏当天构建速度,又能防止缓存无限增长。
docker system prune中-f标志会自动清理 Build Cache,但它的默认行为是不加--all时只清理“悬空”缓存。想彻底一点就:
docker system prune -a -f注意-a会把所有未被运行容器引用的镜像都标记为可删除,这个动作比只清悬空镜像更激进,后面我会在避坑部分细说。
4.3 缓存该留多少:经验权衡
build cache 不是垃圾,它是“时间换空间”的权衡体。保留越多,构建越快;磁盘越小,越应该清理。我的建议取决于你的使用场景:
开发机建议保留最近 3 次构建内的缓存。实现方式是定期跑docker builder prune -f --filter "until=48h",两天内的缓存都能用,两天前的缓存基本不会再被复用,留着纯占空间。生产服务器建议完全不构建或只构建不缓存,因为生产容器镜像应该直接从镜像仓库拉取,本地构建场景不多。
CI/CD 机器是缓存重灾区,因为每次提交都会触发构建,缓存增长速度极快。建议在 CI 末尾增加一个保留时间窗口更短的清理任务,比如只保留 6 小时内的缓存,只要不干扰当天的并行构建就行。
我踩过的最惨痛教训是:有次 CI 机器磁盘满到 100%,流水线直接挂掉,排查半天发现du显示 build cache 占了 80 G,清理后立省 50 G,CI 直接恢复。从此之后,我在所有 CI 机器上把docker builder prune写入了定时任务。缓存这个东西,该舍得的时候一定要舍得,构建慢几秒,好过磁盘爆掉跑不起来。
5. 清理Docker overlay2 目录
5.1 overlay2目录里到底有什么
overlay2是整个 Docker 存储的核心,理解它之后清理就不会慌了。
镜像由多层只读层组成,比如从基础镜像到应用代码可能有 5-10 层。overlay2里每个带哈希的目录就对应一个层,里面包含完整的文件系统内容。容器运行时,Docker 会把这些层叠加起来,在最上层生成一个可写层,保存容器运行过程中修改或新建的文件。
所以overlay2目录里存的其实是三类数据:镜像本身的分层文件、运行容器的可写层、以及容器停止后被保留但不再与任何容器关联的孤儿层。
孤儿层是最值得清理的,因为它们既不贡献给任何存活容器,也不属于任何有用镜像,纯粹是历史操作留下的残留。docker images -f dangling=true显示的悬空镜像,其层数据就属于这一类。
5.2 哪些能直接删,哪些绝对不能碰
先把最重要的一条规则说清楚:绝对不要手动rm -rfoverlay2目录下的子目录。
原因很简单,Docker 维护着层之间复杂的依赖关系。你删掉一个目录,Docker Daemon 元数据里还认为它存在。轻则docker images显示错乱,重则相关镜像和容器全部无法启动,甚至 daemon 直接崩溃无法拉起。我见过有人在网上看到“删除 overlay2 可以释放空间”就真的去删子目录,结果所有容器起不来,最后只能重装 Docker 再重建环境,代价极高。
那什么能删呢?原则是:只能删 Docker 自己标记为可回收的数据,只能通过 Docker 命令去删。
具体来说,悬空镜像的层、停止容器的可写层、未使用的镜像,这些都可以通过docker image prune、docker container prune和docker system prune安全回收。它们由 Docker 逐层检查引用关系后统一处理,不会破坏其他镜像的依赖链。
5.3 正确的overlay2清理姿势
清理overlay2的正确姿势,本质上就是清理镜像和容器引用,不需要直接碰目录。
先清理悬空镜像。悬空镜像就是仓库名和 tag 都显示为<none>的镜像,通常是构建新版本后旧版本不再被引用产生的。处理方式:
docker image prune -f清理停止状态的容器:
docker container prune -f这两步执行完后,overlay2中被这些对象占用的层就会被自动回收。查看回收效果:
docker system df对比清理前后的RECLAIMABLE数值,就能看出效果。
再进一步,如果你希望主动淘汰旧版本镜像,可以加-a清理所有未被活跃容器使用的镜像:
docker image prune -a -f这个命令会把所有只是“存在”但没有容器在用的镜像都删除,释放空间非常可观。但副作用是,下次要用某个旧版本镜像时得重新拉取,所以建议先确认保留策略。
还有一种相对费时但很彻底的做法:如果你发现某个镜像反复更新导致大量旧层积压,干脆删除整个镜像仓库的本地副本,再重新拉取最新版本。比如:
docker image rm example/app:v2删除后旧层被回收,重新拉取时只拉一份新层,空间占用降到最低。这个方法特别适合 Docker Hub 上镜像体积大、更新频繁的组件。
关于overlay2的最后一个建议:如果磁盘空间长期紧张,考虑给 Docker 换个数据盘。修改/etc/docker/daemon.json中的>docker system df df -h
第二步,清理日志文件并配置 Docker 日志轮转。如果个别容器日志已经特别大,先执行截断释放空间,后续再通过配置阻止它继续膨胀:
sudo truncate -s 0 /var/lib/docker/containers/*/*-json.log第三步,清理停止容器和无用网络:
docker container prune -f docker network prune -f第四步,清理悬空镜像,注意这步会删除所有没有容器使用的镜像:
docker image prune -a -f第五步,清理 build 缓存:
docker builder prune -f第六步,确认 Docker 目录里是否还有遗漏的大文件或不再被引用的残留:
sudo du -sh /var/lib/docker/*如果只是日常维护,推荐直接跑综合清理:
docker system prune -a -f docker builder prune -f这套命令能覆盖 90% 的清理需求,操作简单、风险可控。
6.2 定时清理与日志轮转配置
定期手动跑命令不够可靠,人总会忘,定时任务才是稳妥方案。
先配置好日志轮转,避免日志问题再次反弹。在/etc/docker/daemon.json里增加:
{ "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }max-size表示单个日志文件最大 10 M,max-file表示保留 3 个轮转文件,超过总量自动滚动覆盖。修改后重启 Docker:
sudo systemctl restart docker这条配置生效后,新启动容器的日志都会按这个策略轮转,旧容器如果还在写旧日志,需要 recreate 才生效,所以建议在配置完成后把重要容器都重启一�遍。
然后写一个简单的定时清理脚本,放到/usr/local/bin/docker-clean.sh:
#!/bin/bash docker image prune -a -f --filter "until=24h" docker builder prune -f --filter "until=24h" docker container prune -f --filter "until=24h" docker system prune -f --volumes这里加--filter "until=24h"是为了只删 24 小时之前的东西,避免误删还在用的资源。给脚本加执行权限,加入 crontab:
chmod +x /usr/local/bin/docker-clean.sh crontab -e添加一行每天凌晨执行:
0 2 * * * /usr/local/bin/docker-clean.sh >> /var/log/docker-clean.log 2>&1注意docker system prune -f --volumes会删除未被容器引用的卷。如果有些卷是存放备份数据但一时没挂载到容器上的,请谨慎使用。我在自己环境里是分开跑,卷清理单独手动执行,不全放进定时任务。
6.3 防止磁盘再满的日常习惯
清理只是补救,习惯才是根治。我总结了几个比较实用的习惯。
第一,容器运行尽量加--rm。临时执行的容器用完即删,不留可写层、不留日志、不留容器记录。比如调试用的docker run -it --rm ubuntu bash,退出就自动清理干净。
第二,镜像构建注意合并层数。Dockerfile 里能合并的RUN指令尽量合并,减少镜像层数,从根本上减少 overlay2 里的目录和文件数量:
RUN apt-get update && \ apt-get install -y --no-install-recommends package1 package2 && \ rm -rf /var/lib/apt/lists/*用--no-install-recommends避免带入大量依赖,构建完成后清掉 apt 缓存,镜像体积能小很多。镜像小了,overlay2 里的层文件小了,磁盘自然健康。
第三,定期审视本机镜像。很多镜像拉下来用过一次就再也不用了,躺着占空间。每月执行一次docker image ls,把不需要的镜像及时docker image rm清掉。生产环境尽量走镜像仓库管理,本地不保留多余副本。
第四,监控要提前做。用df -h写个简单的 shell 监控脚本,超过 80% 告警,或者直接接 Prometheus 加 Grafana 监控 Docker 目录使用率。别等磁盘满了才去救火,提前发现提前清理,体验完全不一样。
这些习惯看起来琐碎,但真能让你少处理好几起磁盘告警事故。
7. 避坑实录与常见问题
7.1 误删 overlay2 目录的后果
网上确实有人建议“删除 overlay2 目录释放空间”,这是非常危险的做法。有次我手滑把overlay2下某个目录删了,当时觉得只是某个旧镜像的层,结果那个旧镜像的另一个标签正被某个容器引用,容器直接无法启动,报错信息含糊不清,查了一下午才定位到是底层目录缺失。
所以再次强调:手动删除 overlay2 子目录,绝不推荐。如果已经误删了,首先尝试从备份恢复,没有备份就做好重建准备:重新拉取受影响镜像,重新创建容器。能救的概率不高,就别抱侥幸心理。
真实安全的路径是:用docker image prune、docker container prune、docker system prune这三个命令去让 Docker 自己回收,它才会正确更新元数据和依赖关系。就算某个应用镜像想直接删旧版本,也是用docker image rm,而不是去翻 overlay2。
7.2 docker system prune 该多久跑一次、用哪个参数
docker system prune和docker system prune -a的区别,很多人容易搞混。
不带-a只清理悬空资源:停止状态的容器、悬空镜像、未使用的网络和 build 缓存。活跃容器引用的镜像不会被动,相对温和,适合日常定期维护。
带-a会额外清理所有未被容器引用的镜像,包含有完整 tag 的旧版本镜像。比如你本地有app:v1和app:v2,当前只跑了app:v2,不带-a时app:v1不会被动;带-a就会被删。释放空间更多,但下次切回v1时需要重新拉取。
频率建议:开发环境一周一次带上-a,保证磁盘持续清爽;生产环境两周一次即可,不必太频繁。CI 环境按需加定时任务,缓存清理频率要高一些。
如果你用 Docker Desktop 而不是 Linux 环境,注意 GUI 里有个 “Clean up” 按钮也是走差不多的 prune 流程,但资源统计和实际释放空间不如命令行直观,建议还是用终端跑命令看输出。
7.3 清理后 Docker 服务异常的处理
清理期间或清理后出现 Docker 服务异常,先别慌,按顺序排查。
如果systemctl status docker显示 daemon 启动失败,先看日志:
journalctl -u docker -n 50常见错误是 “no space left on device”,这通常不是磁盘真满,而是 inode 耗尽或 overlay2 目录损坏产生大量残留。先用df -i查 inode 使用率,如果 inode 满了,删除大量小文件就能恢复。Docker 目录下碎片化的小文件很多,尤其 build 缓存里的临时符号链接,是 inode 杀手。
如果错误提示指向 overlay2 挂载失败,检查是否还有旧挂载未释放:
mount | grep overlay有残留挂载就先卸载再重启 Docker。
还有种情况是清理时误删了正在使用的日志文件。如果你删了容器日志但容器还在写文件,标准输出写入时会报错,日志配置会失效。处理方式是 recreate 容器,让 Docker 重新打开日志文件句柄。用--rm跑容器的人一般不会遇到这个问题,但长期后台服务就容易踩。
我自己遇到过几次清理后 API 连接失败的情况,一般systemctl restart docker都能解决。真的解决不了,优先检查/var/lib/docker目录权限是否被清理脚本误改过。清理脚本里尽量不要用chmod或chown操作 Docker 数据目录,权限一动,daemon 就无法读写元数据。
每个环境都会有各自的怪问题,但核心思路一致:多看一眼 daemon 日志,操作前备份关键数据,能在 90% 的情况下避免灾难性后果。
最后再分享一个小技巧。清理完空间之后,我习惯在/var/lib/docker上记录一个基线值,比如把du -sh /var/lib/docker的结果存到/etc/docker-space.log里,隔一段时间再跑一次做对比。如果清理后两周内空间又快速增长,那说明肯定有哪个容器或镜像在持续制造数据,而不是 Docker 本身的问题,顺着容器列表一个个查总有答案。磁盘空间管理本质上是件持续的事,一次性清理永远只能治标,真正有用的是,通过每次清理积累经验,搞清楚自己的环境里空间都消耗在哪里,这样下次磁盘再告警的时候,你都不用看第三、第四章的内容,直接就知道该去清理哪里了。