Docker跑了大半年,最头疼的不是容器编排,而是磁盘被不知不觉塞满。明明镜像也不多,容器也就二十几个,df -h一看可用空间就剩几个G,Docker Desktop的虚拟磁盘文件动辄几十上百G,连带着整个电脑都开始卡顿。这篇文章就聊聊我踩过的坑和整理出来的一套清理思路,从最基础的docker system df排查,到日志限制、构建缓存、卷清理,再到daemon.json的长期防护配置,一条龙说清楚。
1. 先搞清楚磁盘空间到底被谁吃掉了
很多新手一上来就无脑docker system prune -a,其实这是最大的误区。清理之前先花两分钟搞清楚空间去向,才能对症下药,避免误删还在用的镜像和容器数据。
1.1 Docker磁盘占用的四大来源
先给你们画一张心理地图——Docker的磁盘占用其实分布在四个层面,互相独立又有交叉:
- 镜像层文件:每拉取一个镜像,Docker都会把它按照分层结构存储。同一个基础镜像的不同变体(比如Ubuntu 20.04和22.04),每层都要占一份空间。这是最大头的占用来源。
- 容器可写层:容器启动后对文件系统的所有写操作都会落在可写层里,容器删除后这部分才释放。如果容器长期运行还在不断写入数据(比如数据库容器),可写层会越滚越大。
- 数据卷:
docker volume是独立于容器生命周期的持久化存储,删除容器并不会删除卷。很多人把数据库数据直接放在容器里,或者用匿名卷存了一堆临时文件,容器删了卷还在,空间就白白浪费。 - 构建缓存与日志:
docker build产生的中间层镜像,以及容器写到JSON文件里的标准输出日志。前者是隐藏的幽灵,后者是无底洞——尤其是生产环境里打印日志密集的容器,单日日志就能冲到几个G。
1.2 三步定位空间占用大户
排查阶段不需要猜,Docker自带命令直接用起来:
docker system df这个命令会输出一张表格,分别统计镜像(IMAGES)、容器(CONTAINERS)、卷(LOCAL VOLUMES)、构建缓存(BUILD CACHE)的占用情况。我自己的服务器上曾经跑出来过镜像18G、容器可写层6G、缓存35G的恐怖数据,问题一目了然。
想看得更细,加上-v参数:
docker system df -v这个命令会列出每一个镜像、容器、卷的详细占用。比如你会发现某个打满版本的镜像占了好几G,某个容器可写层已经膨胀到2G却没在写数据。定位到具体对象之后,清理计划就非常清晰了。
如果你习惯用得多一些的排查方式,还可以直接去Docker根目录看物理文件:
sudo du -sh /var/lib/docker/*/var/lib/docker下overlay2、containers、volumes三个目录通常是大头,分别对应镜像层、容器写入和卷数据。这套方法适合Linux服务器,Docker Desktop用户则要注意去看虚拟磁盘文件在哪。
2. 最常用的清理三板斧
定位清楚之后,就可以上清理命令了。我总结成三板斧:一键系统清理、按需精准清理、定期深度清理。
2.1 docker system prune 快速清场
日常用的最多的就是docker system prune,它的作用是把“已经没用”的资源一次性回收:
docker system prune默认参数下会清理:
- 已停止的容器
- 未被任何容器使用的网络
- 悬空镜像(dangling images,即标签为
<none>的镜像) - 构建缓存
加上-a参数会连“没有被容器引用的镜像”一起删:
docker system prune -a注意这个操作的风险等级完全不同。prune只动悬空镜像,prune -a会把所有不再被容器使用的镜像全部删除,哪怕是有名字、有标签的正式版本。比如你本地拉了个nginx:latest,现在没有容器在用,执行prune -a之后这个镜像就直接没了,下次要用还得重新拉取。
2.2 按类型精准清理:镜像、容器、卷、缓存
三板斧的第二招是局部精准清理,按资源类型分别操作:
清理悬空镜像
docker image prune悬空镜像指那些没有标签、只占用空间的<none>镜像。它们通常是重新构建时留下的旧镜像层,不删白不删。如果想清理所有未被引用的镜像,用:
docker image prune -a这里我个人建议,构建节点可以放心加-a,生产环境的机器还是先看一眼再动手,最好加个过滤条件:
docker image prune -a --filter "until=72h"意思是只清理72小时前创建且未被引用的镜像,最近几天构建的版本还在保护期内,误删概率大大降低。
清理停止的容器
docker container prune这个命令会删除所有处于exited状态的容器,比如你调试代码时反复启动又停掉的一堆临时容器。加-f可以不经过确认直接执行。
清理无用数据卷
docker volume prune这是很多人最容易忽略的一块。Docker在docker run -v时如果不指定命名卷,会生成一堆匿名卷,容器删除后它们原地不动,长期积累下来体积非常可观。volume prune会把没有被任何容器引用的卷全部清空——操作前务必确认没有重要数据。
清理构建缓存
docker builder prune构建缓存通常是最意外的空间凶手。我之前构建一个前端镜像的流水线,缓存峰值竟然干到了20G以上。如果docker system df显示BUILD CACHE占用异常高,直接执行:
docker builder prune -a -f这个命令是清空所有构建缓存,保留结果是绝对安全,代价只是下次构建会慢一点。
2.3 清理命令的适用场景与代价
我用一个表格把这几个命令的使用场景和代价捋清楚,方便你们按需选择:
| 场景 | 推荐命令 | 风险等级 | 注意事项 |
|---|---|---|---|
| 日常快速清理 | docker system prune -f | 低 | 不停机不删镜像,可放心执行 |
| 一口气回收所有未用镜像 | docker system prune -a -f | 中 | 所有未运行容器的镜像都会被删,需要重新拉取 |
| 清理悬挂镜像 | docker image prune -f | 低 | 只清理<none>的镜像 |
| 批量删除停止的容器 | docker container prune -f | 低 | 不会动运行中的容器,但临时容器数据会丢 |
| 清理未引用的卷 | docker volume prune | 高风险 | 删除所有未被使用的数据卷,不可恢复 |
| 清空构建缓存 | docker builder prune -a -f | 低 | 只是构建变慢,不影响运行容器 |
经验之谈:服务器上别随便执行docker system prune -a,至少要加一个--filter "until=24h"保护最近的镜像;开发机能扛得住直接-a全清也没关系,反正拉取很快。数据卷的清理必须谨慎再谨慎,数据库容器停掉之后卷并不会自动消失,volume prune一下可能把备份也带走。
3. 容易被忽视的深度清理
三板斧只能解决表面问题,真正吃满磁盘的往往在这几个被忽视的角落:容器日志、构建缓存残留、以及镜像层叠加造成的重复空间。
3.1 日志文件才是隐藏杀手
这是我认为Docker磁盘清理中价值最高的一块——容器日志。标准输出的日志会被Docker写入宿主机,默认JSON格式,每行日志在/var/lib/docker/containers/<container_id>/目录下以<container_id>-json.log文件存在。
我见过单容器日志文件暴涨到15G的真实案例,排查半天发现只是个业务服务在debug级别下疯狂打印循环输出。处理办法是直接清空日志文件,注意不是删除文件——删除后Docker还会继续写,甚至可能因为句柄残留导致空间不释放:
truncate -s 0 /var/lib/docker/containers/*/*-json.log也可以先定位一下日志占用的最大值:
sudo du -sh /var/lib/docker/containers/*/想做得优雅一点,还可以用logrotate定期切割:
/var/lib/docker/containers/*/*.log { daily rotate 7 copytruncate compress missingok notifempty }但logrotate方案配置起来有点麻烦,我更推荐在Docker守护进程级别直接限制日志大小,这个放到下一部分详细说。
3.2 构建缓存和buildkit缓存清理
前面提到docker builder prune能清理构建缓存,但这一块实际上还有更细的层次。Docker 23.0之后默认使用BuildKit构建,它的缓存分为两大部分:一层是构建过程中产生的中间层,一层是挂载缓存(RUN --mount=type=cache)。中间层在prune时会被清掉,但挂载缓存有时候会以单独的缓存实体留在磁盘上。
查看当前构建缓存占用:
docker system df docker du -sh /var/lib/docker/buildkitBuildKit默认存储在/var/lib/docker/buildkit目录下,如果目录体积巨大(超过10G),说明历史构建的缓存没有清理过。可以手动执行:
docker builder prune --filter type=exec.cachemount这个命令专门清理RUN --mount=type=cache创建的挂载缓存,对依赖包缓存、编译缓存这类内容非常有效。
还有一种情况:docker build时使用了--cache-from和--cache-to导出的缓存,它们会存到Docker Hub或本地Registry,表面上不占本机空间,但拉取下来再用的时候又会占据新的空间。所以本地构建频率高的机器,定期docker builder prune -a -f几乎是强制性的。
3.3 宿主机层面的善后处理
Docker清理完之后,宿主机磁盘空间不一定立刻恢复,原因在于overlay2目录的镜像层虽然被删掉了,但存储驱动不一定立即回收磁盘块。某些情况下还需要做一些善后处理。
第一步,确认空间是否真的有释放:
df -h如果df显示的Avail空间没有明显变化,先别急着重启,试试:
sudo fstrim -v /var/lib/dockerfstrim会向SSD设备发送TRIM指令,配合存储驱动的瘦身回收机制,把已经被标记为删除的块真正返还给底层文件系统。这条命令对ext4、xfs配合SSD的场景有效,机械硬盘上没用,但至少无害。
第二步,如果宿主机上还有残留的Docker临时文件,在/var/lib/docker/tmp里可以手动清理:
sudo rm -rf /var/lib/docker/tmp/*仓库里还可能出现某些因中断操作留下的临时目录,直接删掉不会影响Docker运行。
第三步,确认/var/lib/docker/overlay2目录里没有大量无用的孤儿目录。有些场景下容器删除了,但旧的overlay挂载点没有卸载,导致目录残留。这时候要么重启Docker进程,要么执行:
sudo systemctl restart docker重启Docker会让所有容器发生一次短暂中断,如果环境不允许停服,可以跳过这一步,等维护窗口期再处理。
4. 从源头控制:让磁盘不再膨胀
会清理只是及格水平,会设置各种限制、让Docker不再疯狂吃空间才是高手操作。这一部分说说怎么从配置层面堵住洞口。
4.1 daemon.json里的日志与限制配置
Docker守护进程的配置文件是/etc/docker/daemon.json,没有这个文件就新建一个,重启Docker后生效。我目前在生产环境上用的核心配置是这样的:
{ "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" }, "storage-driver": "overlay2", "storage-opt": [ "overlay2.size=50G" ] }重点看前两项:max-size限制单个日志文件最大10MB,max-file限制保留3个日志文件,也就是说单个容器最多保留30MB日志,写满了会自动滚动覆盖旧的。这个配置能彻底解决日志无限膨胀的问题,前提是业务上不能依赖历史日志做排障——反正我真实的经验是,日常排障最多看到最近几小时到几天的日志,太旧的也没啥用。
第三项storage-opt是针对overlay2存储驱动的容器可写层大小限制,单位是G或B。设置后每个容器可写层最多占用50GB,超出会触发写失败。这个限制能防止某个容器因为写满数据而拖垮整个宿主机的磁盘,适合多租户场景。
注意修改daemon.json之后要执行:
sudo systemctl daemon-reload sudo systemctl restart docker重启Docker会使所有运行中的容器中断,这个操作最好安排在维护窗口期。而且改日志配置只对新创建的容器生效,已经存在的容器还是要重建或者手动处理日志文件。
4.2 日常运维习惯与自动化清理
配置之外,平时多注意一些运维细节,能省去很多半夜救火的痛苦:
定期执行清理脚本
我习惯在crontab里挂一个每周清理任务:
0 2 * * 1 docker container prune -f --filter "until=72h" && docker image prune -f --filter "until=168h" && docker builder prune -f这个任务是每周一凌晨两点执行:清理停止时间超过72小时的容器、清理挂起时间超过168小时的悬空镜像、清理构建缓存。这些参数不会删除还在使用的镜像,也不会误删正在运行的东西,安全系数比较高。
提示:用crontab跑Docker命令时,一定要写清Docker的完整路径,或者先
export PATH=$PATH:/usr/bin,不然cron环境里经常找不到docker命令。
镜像管理规范
拉镜像、建镜像的时候也留个心眼:
- 同一个项目尽量复用基础镜像层,不要每个镜像都独立拉一个完整OS层,省下的是真金白银的磁盘。
- 给镜像打标签时注意清理旧版本,避免每个旧tag都在本地留一份。
docker build时善用.dockerignore,把node_modules、target这类编译产物排除在构建上下文之外,不然每次构建都把整个项目复制一遍,缓存和网络层都白占空间。- 私有仓库定期清理不再使用的镜像Tag,本地也同步清理。
使用代理或镜像加速源
在国内环境下,拉取官方镜像经常很慢,慢的就要反复重试,重试的文件断断续续,也会在本地产生大量临时镜像层。改用能稳定访问的镜像加速源之后,拉取过程更稳定,本地残留的临时文件少得多,磁盘压力也小一些。
5. 常见问题与排查技巧实录
理论说完,到了实操的疑难杂症环节。这些内容几乎都是我从各种翻车现场总结出来的,每一条都值得备份收藏。
5.1 清理完空间没释放?三个可能原因
清理命令执行完之后df -h发现空间没有明显回升,这个现象很常见。我总结三个高概率原因:
原因一:存储驱动未回收
overlay2在删除镜像层之后,需要配合fstrim或者重启Docker才能把块真正释放,特别是使用ext4和XFS文件系统时。解决方案就是前面提到的sudo fstrim -v /var/lib/docker。
原因二:日志文件被进程占用
即使truncate -s 0清了日志文件,但如果容器还在运行中,文件句柄一直被Docker持有,磁盘上的blocks可能还不会被释放。可以先停掉容器再清理,或者直接用/dev/null来覆盖日志文件:
sudo sh -c 'cat /dev/null > /var/lib/docker/containers/<container_id>/<container_id>-json.log'原因三:Deleted文件仍被占用
用lsof +L1查找被标记为deleted但仍然打开的文件:
sudo lsof +L1 | grep deleted如果发现Docker相关进程仍在写某个已删除的文件,重启该容器或重启Docker后空间一般就能释放。
5.2 悬空镜像清理不掉?检查容器依赖
docker image prune -f执行完以后再次docker images还看到一堆<none>镜像,通常是某个停止的容器还引用着这些镜像层。可以先清理停止的容器,再清理悬空镜像:
docker container prune -f docker image prune -f还有种情况是构建缓存导致的<none>镜像,这类记录不会在docker images列表里显示,却实实在在占用空间。处理方式是docker builder prune -a -f,或者干脆docker image prune -a --filter "until=12h"把最近12小时之前的无引用镜像全部清走。
5.3 Docker Desktop 的虚拟磁盘文件膨胀
不少同学用的不是Linux服务器,而是Mac或Windows上的Docker Desktop,这类工具本质是跑在虚拟机里的Docker,磁盘占用多数时候都集中在那个虚拟磁盘文件上。你执行了docker system prune,发现虚拟磁盘镜像文件大小没有变化,需要额外处理。
在Docker Desktop里,清理完成后可以执行:
docker system prune -a --volumes然后在Docker Desktop界面中找到Dashboard的Troubleshoot或者Resources面板,一般会有“Clean / Purge data”或者“Disk utilization”的选项,点击之后Docker会重建精简后的磁盘镜像。或者手动打开虚拟磁盘管理工具执行一次磁盘压缩(对应的就是macOS上的hdiutil / Windows上的Optimize-VHD)。这个操作能回收的空间非常可观,经常有人一口气释放掉20到30G。
5.4 排查技巧汇总速查表
| 症状 | 原因 | 解决方案 |
|---|---|---|
df -h显示空间没释放 | 存储驱动未做TRIM | sudo fstrim -v /var/lib/docker或重启Docker |
| 单个容器日志文件巨大 | 未设置日志滚动上限 | 修改daemon.json的log-opts,重建容器 |
<none>镜像清理不掉 | 停止的容器仍引用镜像 | 先删容器再删镜像 |
| BUILD CACHE占用超高 | 构建频繁或buildkit缓存积累 | docker builder prune -a -f |
| Docker Desktop磁盘文件不减 | 虚拟机磁盘未压缩 | 在Dashboard内清理并压缩磁盘 |
| overlay2目录特别大 | 镜像层叠加太多 | 用docker system prune -a清理镜像,配合fstrim回收 |
| 容器停止后卷仍然占用 | 匿名卷未清理 | docker volume prune(确认无数据后) |
5.5 最后再分享一个小操作习惯
清理Docker之余,也顺手看一眼宿主机的其他角落,有时候能发现比Docker更占空间的元凶——比如node_modules、构建产物、日志压缩包之类的。我自己的习惯是每月跑一次ncdu或者du -sh /*,把宿主机上所有大目录拉出来排查一遍,对照Docker内部资源一起做整体减法。
还有,有任何重要数据操作前,先做备份。尤其是执行docker volume prune之前,先想清楚现在到底有哪些卷在跑数据。如果拿不准,宁可多检查几遍,也别为了省事把自己坑了。
模棱两可的时候,不要用-a,不要用--volumes,精确定位后再动手。清理这种事情,最怕的不是清理不干净,而是删了不该删的。