我上一次被磁盘告警逼到必须做 Docker 清理数据卷 volumes 的完整操作是在一台没有业务压力的测试机上docker ps只列得出五个容器/var/lib/docker却吃了 63GB。顺着df -h往下挖volumes 目录自己占了 39GB而里面那些文件夹的名字全是随机字符串——它们属于谁、是哪个项目留下的、里面到底装着什么我完全没有头绪因为创建它们的容器早被rm掉了。这正是 Docker 数据卷最容易让人忽略的地方容器的生命周期和卷的生命周期是解耦的容器删了卷还在。这篇文章就围绕“Docker 清理数据卷 volumes”这条主线把我实际用下来的排查流程、清理命令、翻车现场以及一套适合测试机和开发机的定期维护方法都整理出来。适合正在被磁盘空间问题困扰、或者刚接触 Docker volume 管理的开发者参考也适合运维同学做操作备忘。1. 三种卷的“终点”各不相同先分清可清、需慎清、不能清很多人一上来就敲docker volume prune结果要么什么都不让删要么把不该删的也删了。问题不在于命令不熟而在于对“卷的类型”没有概念。Docker 里的数据卷其实有三种完全不同的形态它们的清理方式、风险等级和归宿都不一样。1.1 从 -v 的两侧看类型判断卷类型最简单的方法是看-v参数的写法也就是容器启动时挂载参数的“两侧”到底写了什么。我把常用写法整理成了一张表挂载写法类型Docker 是否管理存储位置清理方式-v /host/path:/container/path绑定挂载bind mount否宿主机任意路径手动删宿主机文件-v myvolume:/container/path命名卷named volume是/var/lib/docker/volumes/myvolume/_datadocker volume rm-v /container/path匿名卷anonymous volume是/var/lib/docker/volumes/随机id/_data容器带-v删除或卷清理绑定挂载不在docker volume ls的列表里Docker 只把它当成一个“通道”数据真正归宿主机管所以不存在“清理 Docker 卷”一说它的删除只能你去宿主机上删对应目录。命名卷是唯一值得你认真呵护的一种。它可以被多个容器共享可以独立于容器存在哪怕容器全部销毁卷里的数据照样完好。这就是我前面说“需慎清”的类型。匿名卷是最常见的磁盘泄漏来源。当你只写-v /data而没有指定卷名时Docker 自动创建一个随机字符串名字的卷这个卷和容器没有强绑定关系容器删除后它不会跟着删。开发阶段反复docker run临时容器很快就能堆出一堆随机目录。1.2 为什么容器删了卷却还在这个设计初看很反直觉但理解了就顺了。Docker 把容器视为“可丢弃的”镜像层可以被重写容器可以被销毁重建但数据必须独立存续。卷的生命周期和容器分离是为了支持这些场景容器升级时数据不丢、多个容器共享同一份数据、用专用容器做备份恢复。打个比方容器像一次性纸杯卷像桌上的水壶。纸杯用完扔进垃圾桶水壶不会跟着消失。Docker 的设计师显然认为“多留一个水壶”比“渴的时候没水喝”更安全。所以docker rm默认不删卷这不是 bug而是防止你误删数据的安全机制。明白这一点你就知道清理卷不能只盯着容器而是要单独对待卷。1.3 匿名卷不全是垃圾有一个很容易犯的误判看到匿名卷就觉得是垃圾。其实很多镜像在构建时就声明了 VOLUME比如数据库镜像会把数据目录声明为卷路径。只要容器还存在——哪怕处于 Exited 状态——这个卷都算“被引用”是应该保留的。判断标准只有一个这个卷现在还有没有被容器引用。有引用就不能清没有引用才有资格进入候选名单。这个标准在整篇文章里会反复出现是清理卷的核心决策依据。2. 清理前的磁盘体检从 docker system df 到卷归属核查直接清理之前我建议先做一轮“体检”搞清楚空间到底被谁吃了、哪些卷是真正的悬空卷、有哪些容器正在引用卷。跳过这一步直接开删后面很容易翻车。2.1 一眼看清空间构成docker system dfdocker system df是 Docker 自带的磁盘占用分析工具输出大致是这样的表格TYPE TOTAL ACTIVE SIZE RECLAIMABLE Images 12 6 8.524GB 4.211GB (49%) Containers 17 5 876.2MB 312.5MB (35%) Local Volumes 23 7 39.12GB 30.91GB (79%) Build Cache 9 0 2.144GB 2.144GB这个命令的价值在于它把镜像、容器、卷、构建缓存四类空间分开了。很多时候磁盘爆了真正的大头根本不是卷而是构建缓存或悬空镜像。如果 Local Volumes 的 RECLAIMABLE 比例很高说明大量卷处于无人引用状态这就是清理目标。如果显示卷占用不大但磁盘依然告警就要检查另外两处Docker 日志文件大小以及你在 Mac/Windows 上的 Docker Desktop 虚拟磁盘本身是否膨胀。后者我后面会专门讲。2.2 用 danglingtrue 把无人认领的卷列出来体检确认卷占了大头之后第二步是列出所有悬空卷docker volume ls -f danglingtruedanglingtrue是一个过滤器用来筛出“没有被任何容器引用”的卷。这个列表里的卷就是候选清理对象。不过要注意一点dangling 只表示“当前无人引用”不代表你不需要它比如你留了一个备份卷专门挂到临时容器上做导入导出平时不运行容器那它也会被归进 dangling。实际工作中我会把这一列的输出重定向到文件里然后逐个用后面的方法核实而不是凭印象判断。2.3 三层确认它到底有没有人用有了候选清单下一步是确认卷的真实归属。我习惯按三层来做第一层看有没有容器引用这个卷docker ps -a --filter volume卷名注意这里必须用-a因为停止状态的容器也算引用。这是最多人踩的坑运行中的容器看了一圈都没有就以为卷没人用了。实际上某个 Exited 状态的旧容器依然拽着它不放。第二层如果上面没有输出还可以换个角度直接看所有容器的挂载关系确认卷与容器路径的对应docker inspect -f {{range .Mounts}}{{.Name}}: {{.Destination}}{{end}} 容器名第三层给卷本身做一次“体检”docker volume inspect 卷名这个命令会显示卷的 Name、Labels、CreatedAt、Mountpoint。CreatedAt能帮你回忆这卷是哪一轮项目留下的Labels则是你之前有没有给它打保护标签的证据。把这三层走完你能基本确定一个卷到底能不能动。整个过程不复杂就是多敲几条命令但换来的是不用赌运气。3. 由轻到重三种清理打法select、批量 prune 与全量 system prune确认完目标之后就可以根据风险承受能力选择清理方式了。我按风险等级把命令分成三档单卷定向删除、批量悬空卷清理、全量组合清理。每一档都有适用场景和注意事项。3.1 定向精确删除docker volume rm当你知道某个卷确实没有用了比如临时跑数据分析留下的卷最直接的方式是精确删除docker volume rm 卷名这个命令只删指定卷不碰其他任何东西。默认情况下如果卷还被任何容器引用包括停止状态的Docker 会返回类似下面的报错Error response from daemon: unable to remove volume: volume is in use遇到这个报错不要硬删回到上一章的“三层确认”找到引用它的容器。如果那个容器确实不打算再用了就把它和卷一起处理掉docker rm -v 容器名这里我要强调一个细节docker rm -v里的-v只清理该容器关联的匿名卷命名卷不会被删。如果你确定命名卷也不要了需要再单独docker volume rm一次。精确删除适合“确定目标”的场景比如我清楚记得某个卷是昨天调试用的临时数据。如果目标是系统性的清理这种方式效率太低要用下一档。3.2 批量清理悬空卷docker volume prune 与 filterdocker volume prune是用来清理所有未被容器引用的卷的。直接执行docker volume prune它会先列出将要删除的卷名然后等你在终端确认。所以我特别建议第一次执行不要加-f耐心看完列表按y之前把每个名字扫一遍。如果你在上一章看到了太多格式可疑的随机卷-f可以跳过交互直接删docker volume prune -f但更安全的做法是结合过滤器留出保护名单。给重要卷打标签是一种很实用的保护方式后面第 5 章会详细说。这里先给出保留指定标签卷的清理命令docker volume prune -f --filter label!keep逻辑很简单所有没有被容器引用、且没有keep标签的卷全清带keep标签的卷一律不动。这里还有一个优化执行顺序的小技巧清理卷之前先清理所有停止状态的容器。因为很多卷其实还被 Exited 容器引用着如果你先docker container prune把这些容器删掉卷的引用关系就断了后续docker volume prune才能清掉更多空间。3.3 全量组合清理docker system prune -a --volumes 的代价最后这档是“大扫除”镜像、容器、网络、构建缓存一起清连带卷也清docker system prune -a --volumes需要特别提醒这个命令不加--volumes时系统默认不碰卷一旦加了所有未被引用的卷都会被删掉。配合-a还会把未被任何容器使用的镜像全部清掉包括那些曾经打了 tag 但现在没用到的镜像。代价是什么你的本地 Docker 会变“干净到几乎什么都没了”。下次启动项目时所有镜像都要重新拉取、重新构建开发机上执行了一次半天时间都在等镜像下载。所以我的建议是这台机器如果是长期开发/测试环境system prune -a --volumes不要用太奢侈了。真正适合它的场景是临时机器或 CI 节点做大扫除。平时维护用前两档就够了如果构建缓存膨胀单独执行docker builder prune别把自己逼到全量清理的角落。4. 清理卷翻车现场四个真实案例与教训这一章我写的是实际踩坑记录。每个案例都来自真实工作虽然当时很痛但都是很好的教材。看完你就能理解为什么我在前面反复强调“先确认再执行”。4.1 案例一volume prune 把备用数据库备份卷一起清了某次我一台机器磁盘告警检查后发现有大量悬空卷。当时图省事docker volume prune -f直接清了。几天后才发现一个挂着backup字样的重要卷也没了——那个卷平时的作用是把数据库备份写入进去容器是定时任务按需启动的平时没有任何容器引用它因此在 Docker 眼里它是“悬空卷”但它存的是我们留作回滚用的数据库备份。这个教训非常深刻。所以我现在立了两条规矩第一任何需要长期保留的卷创建时立刻打标签作为“保护名单”第二执行批量 prune 时永远带上--filter label!keep这样的过滤条件并且第一次不加-f看名单。4.2 案例二compose down -v 的连锁反应docker compose down本身只删除容器和网络不会删数据卷但如果加了-v它会把 compose 文件里定义的所有命名字一并删掉。某次某位朋友就是在这种“想清理测试环境”的心态下敲了docker compose down -v项目里的 MySQL 数据卷也跟着没了数据库里的临时数据全部归零。你说数据是备份过的吗没有因为大家以为测试环境的数据不重要。结果丢失的数据里有一批用来验证新算法的样本记录花了整整一天重新造数。我的建议是日常使用永远用docker compose down只有当你明确要删除项目全部数据时才追加-v。如果担心手滑可以在 shell 里给这个组合命令单独做个确认脚本或者干脆把.env里加一个变量脚本里判断变量等于yes才允许执行带卷删除的操作。4.3 案例三in use 报错是因为“看不见”的停止容器这是我刚开始用 Docker 时的一个经典误判。跑docker volume rm提示卷被占用但我docker ps看不到任何容器一度怀疑是 Docker bug。最后用docker ps -a才发现一周前调试用的那个容器还在列表里状态是 Exited它挂载的卷一直没释放。从那以后我就把“用-a看所有容器”当成习惯。遇到卷删除失败第一反应不是加-f而是查引用来源。最稳妥的顺序是docker ps -a --filter volume卷名查引用 → 判断容器是否还需要 → 不需要就直接docker rm -v需要就另找时间处理。4.4 案例四清理完之后宿主机磁盘纹丝不动桌面版在 Linux 服务器上清理的卷空间会直接释放回宿主机。但在 macOS/Windows 的 Docker Desktop 上情况完全不同卷的数据存储在一个独立的虚拟磁盘镜像文件里你执行docker volume prune只是把虚拟机内部的数据删了虚拟磁盘镜像文件本身不会自动收缩。于是你明明看到卷清理成功宿主机磁盘根本不为所动。正确做法是数据清理完成后进入 Docker Desktop 的磁盘管理/Troubleshoot 相关面板查看分区占用情况并执行类似“压缩磁盘镜像”的操作把虚拟机镜像文件里已释放的空白部分还给宿主机。如果那个面板里的功能条目不好找不同版本的入口差异也很大建议去查你自己版本的官方文档。至于 Linux 原生 Docker直接在宿主机上du -sh /var/lib/docker/volumes就能验证空间释放是立竿见影的。另外还有一种极端做法是把 Docker 数据全部重置这会把镜像、容器、卷全清干净代价极大不到万不得已不要用。在用了很久且虚拟磁盘已经膨胀到异常程度的机器上配合备份使用可能反而更省事但这句话的前提是“备份一定到位”。5. 一套从备份到执行的维护流程把上面的知识串起来我认为一台测试机或开发机需要的不是某个单条命令而是一套可以反复执行的维护流程。核心是三步先备份值得保的卷、给重要卷上标签保护、最后才执行清理。5.1 卷备份不重装一遍的最小代价方案清理数据卷之前无论如何都要先给“拿不准”的卷做备份。备份卷我用的是一个很轻量的方式起一个临时容器把卷挂进去用 tar 打包输出。docker run --rm \ -v 卷名:/data \ -v $(pwd)/backup:/backup \ alpine tar czf /backup/卷名-$(date %Y%m%d).tar.gz -C /data .如果不想在宿主机上多建一个 backup 目录也可以让 tar 直接输出到标准输出然后把输出重定向到宿主机文件docker run --rm -v 卷名:/data alpine tar czf - -C /data . backup-$(date %Y%m%d).tar.gz这种方式对文件型数据很有效。数据库类数据卷需要注意直接 tar 打包/var/lib/mysql目录得到的是“崩溃一致性”备份不代表一定能正常恢复。稳妥的做法是先用数据库本身的能力导出逻辑数据或者停止写操作再备份。备份完了心里就有底了后面可以放心清理。5.2 标签护栏让卷在被清理时“有话要说”给重要卷打标签是防止误删最有效的防御措施。创建卷时直接给标签docker volume create --labelkeep 卷名在 Compose 文件里也可以给卷定义标签volumes: data: labels: keep: true有了标签之后清理命令就变成了这样docker volume prune -f --filter label!keep这条命令的语义是所有没有keep标签的悬空卷清掉有keep标签的全部保留。这套护栏能保证我面对随机卷名时不需要一个个猜也能保证重要数据不会在一次手滑中消失。5.3 定期清理脚本示例我最终的维护脚本大概长这样放在测试机上用 cron 每周跑一次#!/usr/bin/env bash set -euo pipefail echo Docker Disk Usage docker system df echo Prune Stop Containers docker container prune -f --filter until168h echo Prune Dangling Images docker image prune -f echo Prune Dangling Volumes (except keep) docker volume prune -f --filter label!keep echo Prune Build Cache docker builder prune -f --filter until168h echo Done 这里有几个设计要点第一先清容器再清卷顺序不能反。停止一个多月的容器删掉后它引用的卷才可能变成悬空卷后续的卷清理才会生效。第二所有的 prune 都加了过滤器。容器清理只动until168h也就是 7 天前的停止容器避免误删还在排障期内的对象卷清理一定加label!keep。第三脚本里我刻意没有放docker system prune -a。开发机或测试机的本地镜像缓存对重建速度影响很大全量清理我只会手动执行绝不放到定时任务里。脚本跑完后你可以在 Linux 机器上直接验证卷目录的大小du -sh /var/lib/docker/volumes/*在 Docker Desktop 上则去压缩虚拟磁盘镜像把空间真正还给宿主机。如果你和我一样测试机上永远长着十几二十个随机字符串卷名的卷先把上面的检查和备份流程跑熟再碰清理指令。特别是第一次清理我建议把所有卷名列出来逐个打开docker volume inspect看一眼全局掌握之后再动手。与其等磁盘告警响了再慌慌张张找命令不如在每个新项目开始时就把卷的标签、备份和清理习惯一起纳入常规流程。