ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

macOS上Docker磁盘占用爆满?从虚拟磁盘原理到清理实战

macOS上Docker磁盘占用爆满?从虚拟磁盘原理到清理实战

做 macOS 开发的人,只要有 Docker Desktop 在跑,迟早会遇到一次“磁盘空间告急”的惊吓。我自己第一次撞上是在连续构建了两周项目后,Mac 突然提示“磁盘几乎已满”,查了一圈才发现,占用大头压根不是项目文件,而是 Docker 自己。后来我把这件事梳理成一套排查和保养方法,今天写出来,希望能让同样被磁盘占用折腾过的朋友少走弯路。这篇不搬运官方文档,只讲我怎么看懂 Docker 在 macOS 上的存储逻辑、怎么定位空间被谁吃了、哪些清理命令可以放心用,以及哪些操作容易误伤数据。

先给一个我踩坑后才牢记的结论:Docker Desktop for Mac 并不是直接把镜像、容器数据散落在 macOS 文件系统里,而是把所有东西装进一个大的虚拟磁盘镜像文件。这个文件会随着镜像拉取、容器创建、卷写入、构建缓存累积而不断膨胀,而且很多情况下,你从 Docker 里删除镜像后,空间并不会立刻还给 macOS。搞懂这条主线,后面的所有操作就顺理成章了。

1. 为什么 macOS 上的 Docker 磁盘占用特别容易失控

1.1 虚拟磁盘镜像才是真正的“空间仓库”

Docker Desktop 在 macOS 上运行的本质,是在一个轻量虚拟机里跑 Docker 引擎,而虚拟机的磁盘是一整个大文件。不同版本的文件名不一样,老版本可能是Docker.qcow2,新版本大多叫Docker.raw。你可以把它理解为“一个会自己长胖的硬盘盒”,Mac 看到的是这个文件,Docker 看到的是里面完整的分区。

这个虚拟磁盘文件默认放在用户目录的容器数据目录下,也就是类似~/Library/Containers/com.docker.docker/Data/vms/0/data/的位置。平时如果不是刻意去翻,你几乎注意不到它,但它的体积可以从初始的几个 GB 一路涨到几十 GB 甚至上百 GB,而你的 Mac 磁盘空间就是这么消失的。

提示:想知道这个文件到底多大,可以在 Finder 里按住 Cmd+Shift+G,输入~/Library/Containers/com.docker.docker/Data/vms/0/data/,然后查看Docker.raw的大小。不同版本路径可能有细微差异,但思路是一样的。

1.2 虚拟磁盘里的空间被什么人占用

既然磁盘里面是个完整的 Linux 环境,那占用就可以分成几类,每一类的清理难度和风险都不一样:

  • 镜像(Images):每次拉取新镜像、重新构建新版本镜像,都会产生新的镜像层。尤其当项目频繁调整依赖,旧的镜像层会慢慢堆成山。
  • 容器(Containers):容器在镜像之上多出来一个可写层,容器不删除,这个层就一直占着空间。
  • 数据卷(Volumes):数据库文件、缓存目录、上传文件,只要容器挂在卷上写入,全部落在虚拟磁盘里。卷的清理必须谨慎,因为它通常是你真正的业务数据。
  • 构建缓存(Build Cache):Docker 构建时为了加速,会缓存每一层中间结果。项目 Debug 构建几十次之后,缓存可能比最终镜像大好几倍。
  • 日志(Logs):容器标准输出日志写到宿主机,长期不轮转也可能吃下几 GB。

很多人在 Mac 上用的 Docker 场景是本地开发,本地开发意味着反复构建、反复启动、反复调试,以上五类占用会交替增长。Linux 服务器上可以用各种运维工具定时清理,macOS 上的 Docker Desktop 虽然有图形界面清理入口,但如果我们不理解它内部的结构,清理时心里总是没底。

1.3 为什么删了镜像磁盘还是没有回来

这是被问得最多的问题,也是最让人抓狂的现象。表面上看,docker rmi执行成功,镜像列表里确实没了,可是 Finder 里虚拟磁盘文件体积纹丝不动。

原因在于虚拟磁盘的“瘦身”不是时时的。文件系统内部的块被释放后,虚拟机自身不一定知道要把这些空间归还给宿主机,除非 Docker Desktop 在后台执行了垃圾回收和压缩。不同版本行为不完全一样,但至少我自己遇到的情况是:清理后不重启 Docker Desktop,空间常常不会自动缩小。所以,评估清理效果时,只看docker system df的输出还不够,还要配合虚拟磁盘文件的真实大小变化。

理解这一点后,你会明白,优化磁盘使用率的要点不是“删几个镜像这么简单”,而是要从诊断、清理、设置三层同时下手。

2. 动手之前先看清:用命令定位磁盘消耗大户

2.1 docker system df 是首个体检报告

在终端里执行:

docker system df

输出一般长这样:

TYPE TOTAL ACTIVE SIZE RECLAIMABLE Images 128 56 14.28GB 8.92GB (62%) Containers 96 23 3.42GB 2.01GB (58%) Local Volumes 41 21 12.51GB 4.73GB (37%) Build Cache 0 0 0B 0B

这一行一行的含义,对应我前面说的那几类存储。SIZE是当前占用的总量,RECLAIMABLE表示可以安全回收的空间。如果RECLAIMABLE占比很高,说明大量空间被废弃对象占着,清理潜力很大。

我第一次跑这个命令时,发现镜像总量不高,但 Local Volumes 占了大头,这直接改变了我的清理策略。如果没有这一步,我可能只顾着删镜像,结果卷里的数据还在原地继续占着空间。

注意:Build Cache 在 Docker Desktop 的某些版本里可能单独显示,也可能不显示。如果看不到,可以用docker system df -v进一步查看,或者检查是否启用了 BuildKit 的缓存统计。

2.2 用 docker system df -v 找到具体元凶

当总量看起来还可以接受,但磁盘却不够用的时候,就需要把粒度继续往下钻。执行:

docker system df -v

这个命令会列出所有镜像的仓库标签、镜像 ID、创建时间、共享大小、唯一大小,以及所有容器的卷占用和挂载点信息。对着这份输出,你能一眼看出哪些项目镜像已经几个月没动过,哪些容器虽然停止但依然占着可写层空间。

我自己常用的做法是:

  • 先把结果保存到文件里,方便分析:docker system df -v > docker_df.txt
  • 按SIZE降序人工看一遍,把超过 1GB 的对象圈出来
  • 对照项目目录,判断哪些是当前还要用的,哪些是历史遗留

这种“先量化再动手”的方式,避免了好几次误删镜像的尴尬。比如有一次我差点把某个基础镜像删掉,因为它在列表里占了 3GB,但后来发现它是一个正在运行的老项目容器的基础依赖,删了之后依赖它的容器全部会受影响。

2.3 用静态文件检查确认虚拟磁盘的真实体量

命令输出只能说明 Docker 引擎视角下的空间,不代表 macOS 实际占用的磁盘空间。因为虚拟磁盘文件自身的物理大小和逻辑占用不一定一致。确认真实体量,用这个:

du -sh ~/Library/Containers/com.docker.docker/Data/vms/0/data/

如果你想看到最核心那个文件的体积:

ls -lh ~/Library/Containers/com.docker.docker/Data/vms/0/data/

如果发现Docker.raw的体积远大于 Docker 引擎报告的内部使用量,说明存在大量“已删除但未归还”的空间,需要通过后续步骤处理。

实操心得:不要只看 Finder 的“显示简介”,在命令行里用du看到的数字更直接,尤其是当 Docker Desktop 正在运行时,Finder 的显示可能滞后。我推荐在清理前后各跑一次这个命令,用来判断清理到底有没有效果。

3. 清理实操:从低风险到高风险逐步收口

3.1 最安全的清理:悬空镜像与停止容器

如果只是日常维护,不想影响任何正在使用的项目,先从悬空镜像开始。悬空镜像指的是没有标签、也没有被任何容器引用的镜像层,通常是重新构建后留下的旧版本。删除它们不会影响任何正常运行的应用。

docker image prune

它只会清理<none>这种悬空镜像,保留有标签的镜像。如果空间还是不够,再考虑:

docker container prune

这条命令会删除所有已停止的容器。已停止容器里的未提交修改会丢失,但如果你平时不是靠“docker commit 保存现场”来工作的,风险极低。

做完这两步后,再跑一次docker system df,大概率能看到Images和Containers的RECLAIMABLE明显下降。

3.2 按条件清理镜像:保留最近资源,删除历史包袱

如果悬空镜像清理完还不够,那就进入“按条件清镜像”阶段。常用的是按时间过滤:

docker image prune -a --filter "until=72h"

这条命令会删除所有没有被容器使用、且创建时间超过 72 小时的镜像。注意-a意味着包括有标签但未使用的镜像,所以执行前一定确认那些老镜像不再需要恢复。

我一般在项目发布稳定版之后,把一个月前的镜像统一清理掉。本地开发的镜像没有长期保留的必要,需要时重新拉取或者重新构建都比占着几十 GB 空间划算。

风险提醒:如果某个镜像被停止容器引用,image prune不会删除它。所以你不需要担心误删正在使用的镜像,但反过来,这也说明停止容器占用的空间会拖住镜像,让清理显得“不够干净”。

3.3 卷清理动手前必须想清楚

卷清理是最容易“回不了头”的操作。docker volume prune会删除所有没有被任何容器使用的卷。看着很简单,但如果你曾经跑过类似 MySQL、PostgreSQL、Redis 这类带持久化数据的容器,而容器已经被删除,里面的数据可能就在这些“废弃卷”里。

我的建议分两种情况:

  • 确认这些卷只是缓存、临时文件、测试数据:直接执行docker volume prune
  • 卷里可能是数据库内容,无法确定是否还重要:先在容器列表里确认对应容器是否存在,再用docker volume ls查看卷名,尽量恢复容器挂载后再考虑清理

有一次我就遇到过这样的场景:一个联调环境里的 PostgreSQL 容器早就停了,卷占着 8GB,我以为里面是临时数据,执行 volume prune 之后才发现联调环境需要那批数据复盘。虽然没有造成不可挽回的损失,但当时真的很被动。

3.4 构建缓存清理:释放效果最明显的一刀

构建缓存是很多 mac 用户忽略的大头。一个复杂的docker build会缓存几十个中间层,频繁构建时,缓存体积可能达到最终镜像的 5 到 10 倍。

docker builder prune

这条命令清理所有未被使用的构建缓存。如果空间实在吃紧,可以加-a:

docker builder prune -a

区别在于,前者保留可能还在被后续构建使用的缓存,后者把缓存全部清掉。清完后第一次构建会因为缺少缓存而变慢,但磁盘空间会立刻宽松。

实操心得:如果你的 CI 和本地都用 Docker 构建,我建议发布大版本后再做一次docker builder prune -a。别心疼那点构建时间,磁盘满了导致系统卡死,浪费的时间更多。

3.5 终极清场命令:什么时候才用 docker system prune -a --volumes

网上很多教程会让你遇到问题直接执行:

docker system prune -a --volumes

这条命令会把所有未使用的镜像、容器、网络、构建缓存和卷一起删掉。听起来很省事,但它最危险的地方在于“全部”。我见过有人把刚拉下来准备晚间发布的镜像也删了,最后恢复到手忙脚乱。

我的建议是:仅在确认这台 Mac 上没有任何需要保留的本地 Docker 数据时使用。比如刚接手一台旧开发机,想彻底清空环境再重新配置,这时候用它没问题。平时维护别用。

3.6 清理后别忘重启:让虚拟磁盘真正瘦下来

清理命令执行完后,虚拟磁盘内部的空间可能已经释放了,但物理文件大小不一定立刻变化。前文提到过,Docker Desktop 需要重启甚至做一次虚拟磁盘整理,才会把空间归还给 macOS。

最简单的方式是点 Docker Desktop 菜单栏图标,选择 “Restart”。重启后再看ls -lh Docker.raw,体积一般会有明显下降。如果下降幅度很小,可能是内部碎片太多,这时可以用后面讲到的“重置”手段处理。

注意:重启 Docker Desktop 会把正在运行的容器全部停掉。如果配置了开机自启和自动恢复,通常没事;如果是自己手动docker run的临时容器,重启前确认没在跑重要任务。

4. 从设置层控制增长:资源限制与磁盘映像迁移

4.1 给 Docker Desktop 设置磁盘用量上限

控制磁盘增长最直接的办法,不是天天清理,而是给它一个硬性“笼子”。打开 Docker Desktop 的 Settings,找到 Resources 或者 Advanced 选项,里面可以设置 “Disk image size”。这个值表示虚拟磁盘的最大容量。

我自己的开发机设置成 32GB,因为项目镜像和构建缓存加一起,大多数时候不会超过这个量。一旦到了上限,Docker 会报磁盘空间不足,这反而是一种“主动提醒”,让我想起来去清理。如果没有上限,虚拟磁盘会偷偷长到 60GB、80GB,等到 Mac 磁盘报警时才发现,往往已经很难收拾。

提示:修改 Disk image size 后,Docker Desktop 一般会要求重启生效。注意这种方式不会立刻压缩现有文件,只是限制未来的增长上限。

4.2 把磁盘映像挪到大分区或移动硬盘

如果你的 Mac 本身空间紧张,但外接了一块大容量硬盘,可以把 Docker 的虚拟磁盘文件迁移到外置存储上。Docker Desktop 的 Settings 里有一个 “Disk image location” 选项,指定新的位置后,Docker 会重新初始化虚拟机,并把你现有的镜像数据一并迁移过去。

实际操作时,需要注意几点:

  • 目标位置剩余空间必须大于当前虚拟磁盘文件大小
  • 迁移过程会重启 Docker Desktop,期间无法使用 Docker
  • 如果外接硬盘是可移除的,请确保 Docker 运行时硬盘不会意外断开

用移动硬盘跑 Docker 的缺点是性能受接口速度影响。如果你用的是机械硬盘,拉取镜像和构建速度会明显下降;如果是高速固态移动硬盘,体感还能接受。我自己更推荐把虚拟磁盘留在本机,但把“不常用的大镜像”定期导出并删除,既保留可用性又控制占用。

4.3 善用 Docker Desktop 的排查与重置功能

Docker Desktop 界面里实际还有一个很实用的入口,在菜单栏图标上选择 “Troubleshoot” 或 “Reset”,具体名称在不同版本略有差异。这里有几个功能:

  • Restart:重启 Docker 引擎,用于让清理生效
  • Clean / Purge data:清理所有 Docker 数据,相当于把所有镜像、容器、卷全部删除,虚拟磁盘会恢复到最小状态
  • Reset to factory defaults:把 Docker Desktop 配置恢复出厂,相当于彻底重装

其中 “Clean / Purge data” 适合你已经决定从零搭建环境的情况。执行前 Docker 通常会提示你确认,因为它是不可逆的。我一般只在“虚拟磁盘体积压缩无效”时才用它,操作完再重新拉取需要的镜像。

实操心得:不要拿 “Clean / Purge data” 代替日常清理。它虽然能立刻把 Docker.raw 体积压下来,但那相当于把整台“虚拟机”清空,需要重新拉取所有镜像、重建所有容器,成本远高于用 prune 系列命令精准删除。

5. 两个我反复踩过坑的典型场景

5.1 反复 docker build 后磁盘被构建缓存吃光

在一次偏后端的项目迭代中,我一天之内改了 20 几次代码,每次都是直接docker compose up -d --build。Rust 项目编译出来的中间镜像非常庞大,一天下来 Docker.raw 直接从 12GB 涨到 31GB。

当时我不知道构建缓存会这么夸张,后来进docker build的详情才发现,每个中间步骤的缓存层都保留着。执行docker builder prune之后,虚拟磁盘内部释放了将近 16GB,再重启 Docker Desktop,Mac 的可用空间立刻恢复了。

从那以后,我给自己定了一个规矩:大版本迭代告一段落,立刻做一次docker builder prune -a;日常调试时尽量用docker build --cache-from控制缓存策略。

5.2 docker compose down 之后,匿名卷还赖在虚拟磁盘里

使用 Docker Compose 时,如果 compose 文件里没有给卷指定名字,系统会生成匿名卷。比如 MySQL 容器一般会写一条类似-v /var/lib/mysql的配置,这句话没有指定卷名,每次启动都会生成一个匿名卷。

一开始我以为执行docker compose down会把容器和卷一起清理干净,后来发现默认情况只删除容器和网络,卷并不会自动删除。多次重建后,系统里堆了一堆abcdef123456...开头的匿名卷,全是旧容器留下的数据。用docker volume ls能看到一大堆名字长得一模一样的卷,占着少则几百 MB、多则几 GB 的空间。

针对这个问题,我用两个习惯来避免:

  • compose 文件里主动写name: project_db_data之类有意义的卷名
  • 定期执行docker volume ls -qf dangling=true找出悬空卷,确认不需要后再docker volume prune

5.3 容器日志能把小空间吃成满格

还有一个隐蔽的大户:容器日志。如果一个容器内部的服务频繁打印日志,而 Docker 的日志驱动默认是 json-file,日志文件会不断增长。并且日志增长路径在虚拟磁盘内部,从外面看还是“Docker.raw 变大”,很难第一时间联想到日志。

排查方法很简单:

docker system df -v

在容器列表里能看到每个容器的SIZE和它的可写层占用,如果某个进程日志量大的容器占了几个 GB,多半就是日志没轮转。

处理方式也很直接:容器确认不需要保留现有输出时,直接重启容器并考虑加日志驱动配置,或者用类似--log-opt max-size=10m --log-opt max-file=3的参数限制单容器日志大小。这个配置加在你自己的docker run参数或 compose 文件的logging配置里,能从根本上避免日志无限增长。

实操心得:现代编译型语言项目或长期运行的服务,日志增长是必然事件。与其等磁盘满了再清理,不如在创建容器时就带上日志限制参数,这个习惯能省掉后来 80% 的磁盘告警。

6. 日常防胀手册:从源头减少 Docker 磁盘压力

6.1 定期观察磁盘报告,把清理变成周常

Docker 的磁盘占用不会自己消失,定期观察是必须的。每周花半分钟跑一次:

docker system df

如果RECLAIMABLE开始接近总占用的一半,说明又到了清理时间。这个频率不高,但能让你在空间告急之前就行动起来。

6.2 少用 docker commit,多用正规定义方式

很多人在本地调试时,习惯 “容器改动完,直接 docker commit 保存成新镜像”。这种做法特别容易撑大镜像体积,因为容器可写层里可能包含了大量临时文件、编译产物。长此以往,虚拟磁盘内部会产生很多“脏层”。

更好的做法是把改动写进 Dockerfile,重新构建。虽然构建也会产生缓存,但至少删缓存是可控的;而且镜像层的历史是可读的,不会留下不可追踪的胖镜像。

6.3 用 Dockerfile 合并指令减少中间层堆积

Dockerfile 里每条RUN指令都会产生一个新层。比如:

RUN apt-get update RUN apt-get install -y curl RUN apt-get install -y unzip

这种写法会形成三层中间层,构建缓存也会多三份。合并成一条:

RUN apt-get update && apt-get install -y curl unzip

不仅镜像层数减少,构建缓存也会更精简。对于本地开发机,这意味着虚拟磁盘内部的碎片更少,清理时回收率更高。

6.4 给容器设置自动清理选项

对于本地一次性测试容器,创建时直接加上--rm参数。比如:

docker run --rm -it node:20 bash

这样当容器停止时,Docker 会自动删除该容器及其可写层,不会残留停止容器占空间。如果你用的docker compose,可以在服务定义里加auto_remove: true(部分新版支持)或者在 teardown 时确保手动删除。

6.5 外部存储不要轻举妄动

前面提到可以把虚拟磁盘放到外部硬盘,但绝不要把正在使用的卷目录软链到外部存储,尤其是网络存储。我见过有人把数据库卷直接软链到 NAS 上,结果 Docker 容器启动时由于 IO 延迟,数据库频繁报错,最后数据受损。本地开发里,性能和稳定更重要,空间不够用清理来代替,别拿数据冒险。

7. 常见问题与我的处理经验

7.1 为什么执行 prune 后 Docker.raw 还是那么大

最典型的原因是没有重启 Docker Desktop。处理顺序是:清理 → 退出 Docker Desktop → 重新启动 → 检查文件大小。如果重启后还是大,可以尝试重置 Docker Desktop 的磁盘配置,或者在 Troubleshoot 里执行一次清理操作。

7.2 磁盘映像位置修改后,旧数据要不要手动删

修改 Disk image location 后,Docker Desktop 通常会在新位置重新创建虚拟磁盘,旧文件保留在原来的路径。确认新环境没问题前,不要手动删除旧文件,否则一旦回退会发现数据都没了。等稳定运行几天后,再把旧地址的Docker.raw转移到备份盘或删除。

7.3 volume prune 后容器还能不能一键恢复

不能。docker volume prune删掉的是容器曾经使用过的数据卷,容器本身的配置和代码还在,但数据没了。如果你不确认数据重要性,不要执行这条命令。想快速查看卷和容器的关联关系,用docker inspect或者可视化工具能更直观。

7.4 Docker Desktop 自带清理按钮和命令行清理哪个更彻底

没有绝对哪个更彻底。命令行 prune 适合精准删除,Docker Desktop 的 Reset 类操作适合整体重来。我的经验是先命令行删大块头,再通过重启让虚拟磁盘收缩;如果仍然异常,用界面里的 Purge 功能做兜底。

7.5 本地开发环境的 Docker 究竟留多少空间合适

这个问题没有统一答案,但可以参考一个经验值:把项目需要的所有镜像体积加起来,再放宽一倍给构建缓存和容器日志,基本够用。比如我的项目镜像体积约 10GB,日常构建缓存和日志预留 4GB,所以磁盘镜像上限设为 32GB;如果你同时维护两三个项目,往 64GB 设置也不过分。

注意:磁盘镜像上限不是设置越小越好,设太小会导致构建中途报 space 不足,反而打断工作。宁可设置成“足够大但有限制”,也比无限增长好。

8. 写在最后的一点个人体会

从那次磁盘告急到现在,我养成了一批和 Docker 有关的固定习惯:每周看一下 Docker 的磁盘报表、大版本发布后清一次构建缓存、容器日志加上限制、外部存储用来存放不常读的镜像包而不是把数据库卷放上去。这些习惯看起来琐碎,但正好卡住了 Docker 磁盘增长的几个关键节点。

如果你现在也被 Docker Desktop 的磁盘占用折腾得够呛,不用急着格式化硬盘,也不用立刻把电脑重装。按我上面说的顺序来一遍:先跑docker system df -v看懂结构,再针对大头做精准清理,然后重启 Docker Desktop 让虚拟磁盘瘦身,最后在设置里把磁盘上限定下来。做完这几步,大多数情况下 Mac 的磁盘空间都能恢复到一个合理的水平,Docker 也不会再成为那个“看不见的磁盘杀手”。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进