
Docker 用久了最容易被忽视的磁盘杀手就是数据卷。镜像随便几个G容器层几十M可数据卷里的日志、缓存、临时文件、数据库文件经常在不知不觉中把磁盘塞满。尤其是线上环境出现过一次因为 /var/lib/docker 分区爆满导致容器集体拉胯的事故之后我才认认真真把清理 volumes 的整套路子捋了一遍。这篇文章就把我自己的踩坑经验、命令组合、判断思路完整写出来给正在被 Docker 磁盘占用困扰的朋友一个可以直接照着做的参考。这篇内容适合谁看适合那些发现自己 Docker 目录越来越大、不敢随便删、又想知道“哪些能删哪些不能删”的人也适合想把自己本地开发环境彻底清理干净恢复磁盘空间的人。我会从最基础的概念讲起然后逐步深入到命令组合和危险操作避坑保证新手能看懂老手也能找到一些平时不太注意的细节。1. 为什么要清理 volumes先搞清楚空间到底被什么吃了很多人在意镜像清理和容器清理却因为数据卷藏在底层而忽略了它。这不是一个小问题。镜像层本身是只读的容器删除后如果你没加 -v 参数那些写入数据就会被保留而数据卷的机制决定了它完全可以脱离容器继续存在。换句话说你可以删掉所有容器、所有镜像但以前留下的 volumes 在一个专门的数据目录下面待得好好的继续占着磁盘你甚至都不一定能直接感知到。要判断当前磁盘被谁吃了我最常用的命令是 docker system df。这个命令会列出镜像、容器、本地数据卷、构建缓存四类对象各自的占用大小输出格式大概是docker system df输出结果中有一个 VOLUME 列显示的是所有本地数据卷占用的总大小。这里注意它显示的其实是“会被统计到卷里的数据量”并不包含那些以 bind mount 方式挂载到宿主目录的数据。所以我一般还会配合执行 docker system df -v 去看更细的列表它会展示每个卷具体被哪个容器引用、卷名是什么、占用多大空间这样一上来就能定位最肥的那几个卷。还有一个很关键的点为什么数据卷会越来越大如果容器里跑的是数据库或者日志采集器比如 PostgreSQL、MySQL、Elasticsearch、logrotate 没配的 Nginx那数据卷的增长几乎是无上限的。容器虽然只是个沙盒但卷的实际存储位置是在宿主机上路径通常是 /var/lib/docker/volumes/卷名/_data不管容器删不删只要卷还在文件就还在。这里还必须区分一种常见误区有人把宿主机上的某个绝对路径通过 -v /host/data:/container/data 挂进容器这属于 bind mount不归 Docker 管理也不会出现在 docker volume ls 里更不会被 docker volume prune 清理。大家经常提到的“清理数据卷”默认只针对 Docker 自己管理的 volume这个边界要搞清不然会误以为清理失效或者反过来误删宿主机目录。我自己的习惯是先跑 docker system df 打底心里有数再看 /var/lib/docker 所在分区的 df -h 确认真实剩余空间两者一对比就知道到底卷占了多少、还有多少空间被镜像或 build 缓存占据。你如果发现 docker system df 显示的卷占用不大但磁盘还是满的那就要考虑是不是 build cache 太大或者日志文件没限制大小。这个问题很常见因为很多版本的 Docker 默认不对日志文件做大小上限单个容器几百MB到几GB的 json 日志就这么累积起来了你甚至会以为又是 volumes 惹的祸其实和卷没什么关系。看清楚了这些才算真正进入到“清理 volumes”的实操环节。2. 清理 volumes 的常规操作与命令细节2.1 docker volume ls 和 inspect先摸清家底清理前很重要的一步是看看当前到底有哪些卷。命令很简单docker volume ls它会输出 DRIVER 和 VOLUME NAME 两列。DRIVER 默认是 local也就是卷数据存在本机如果是其他第三方驱动比如 NFS 或者云厂商的存储插件那背后逻辑就完全不一样了清理时要特别当心。要查看某个卷到底被谁用可以docker volume inspect 卷名输出结果里有一个名为“Labels”的字段和“Mountpoint”字段。Mountpoint 就是卷的实际数据目录比如 /var/lib/docker/volumes/myapp_data/_data。用这个路径可以直接去看数据内容也可以确认大文件都在哪儿。inspect 结果里还有一个很有用的信息——CreatedAt能告诉你这个卷是什么时候创建的。你发现某个卷已经大半年没有容器引用了那基本可以下定论该清理。还有要不要提的是 docker volume ls 支持过滤条件docker volume ls --filter labelkeeptrue docker volume ls --filter danglingtruelabel 过滤适合你自己在创建卷时打了标记的情况比如 docker volume create --label keeptrue mydata。而 danglingtrue 过滤出的就是那种“已经没有任何容器引用”的悬空卷。这个过滤条件比较实用因为日常清理也就是冲着这些卷去的。2.2 docker volume rm, 删掉单个卷的正确姿势单个卷的删除非常简单docker volume rm 卷名但这句话的前提是这个卷必须没有被任何容器引用否则会报错。报错信息类似Error response from daemon: remove 卷名: volume is in use - [容器ID]这时候你就要找到是哪个容器还在用它。方法可以是逐个执行 docker inspect 去翻 Mounts 列表不过更快的是直接用 docker ps -a 找出所有存在容器结合 docker inspect 批量看或者用 docker ps -a --filter volumexxx 这种过滤方式。在 Docker 较新的版本中可以使用docker ps -a --filter volumemyvolume这样能够列出所有挂载了该卷的容器包括已经退出但没删除的容器。必须注意即便是处于 exited 状态的容器只要没有被 docker rm 删除它也依然占用着卷引用这时候你直接删除卷同样会失败。所以常见的流程就是先 docker rm 掉无用容器再删除卷。2.3 docker volume prune批量清理悬空卷如果你有几十个悬空卷一个一个删太蠢这时候用docker volume prune执行后 Docker 会列出所有将被删除的悬空卷并让你确认。它会告诉你一共可以释放多少空间Total reclaimed space: 8.621GB这是我最常用的一条清理命令但必须清楚它的边界只有未被任何容器引用的卷才会被删。如果一个容器还在运行或者只是存在哪怕你再也不需要那个卷了prune 也不会动它。所以实际场景中要么先把容器删了要么单独针对这些卷做 rm。prune 也可以加 -a 参数表示删除所有未被使用的卷不光悬空卷包括那些没有被容器引用、但以前手动创建的具名卷。用法docker volume prune -a不过提示也很明显如果某个卷虽然当前没被引用但你想留着以后重新用那需要做好备份或者确认不需要了再执行因为这一步没有后悔药。2.4 filter 配合 prune按条件精确清理生产环境批量清理的时候用 -a 容易误伤无辜我更喜欢用 filter。比如在创建卷时打了 label只清理没有 label 的卷docker volume prune --filter label!keep也可以只清理悬空卷之外那些 label 为 temp 的docker volume prune --filter labeltemp还有 label 不存在的情况docker volume prune --filter label!keep需要理解这里的过滤语法和 docker volume ls 基本一致。执行前我习惯先不带 -f 跑一遍让它列出所有预备删除项查看一遍再敲 -f 真正清掉。这招看着多花一步但能极大避免误删。如果发现列表里有不该出现的卷说明标签打错了或者有隐藏依赖需要先停下来排查。2.5 创建卷时的预留标签是后期清理的救命稻草前面反复提到 label这是 Docker volume 清理里特别实用的小技巧。很多人在 docker volume create 时根本不传 label等卷多了之后哪些能删哪些不能删全靠猜。但如果一开始就规划好比如docker volume create --label ownerproject-a --label keeptrue myapp-data那以后用 docker volume ls --filter labelkeeptrue 或者 docker volume prune --filter label!keep 就能精准操作。尤其当团队里有其他人也会创建卷时统一的 label 规范能让清理工作减少大量不该有的思考成本。我自己就吃过没打标签的亏后来把 create volume 的这些参数写进部署脚本里以后清理直接跑命令就好。3. 容器生命周期与数据卷的绑定关系不搞清楚就敢删卷3.1 为什么删了容器卷还在匿名卷和具名卷差别很大这可能是初学者最迷惑的地方。先明确一个概念具名卷named volume是指你在 docker run 时写明了卷名比如docker run -d -v mydata:/data nginx这里的 mydata 是具名卷。匿名卷则是只写容器内路径不写卷名比如docker run -d -v /data nginx执行完这条命令Docker 会自动生成一个随机字符串的卷名但这个卷不容易被直接记住。关键区别在于容器删除时的行为。docker rm 默认不会删除任何卷无论具名还是匿名卷都会保留。但如果删除容器时带上了 -v 参数docker rm -v 容器名那么该容器挂载的匿名卷会被自动删除而具名卷不会被删除必须手动删。这一点经常让人踩坑用 docker rm -v 删了容器以为卷都清干净了结果发现具名卷还在白白占着空间很多天。反过来如果删容器时没加 -v匿名卷也会残留这也是悬空卷的主要来源之一。那 docker run 加 --rm 参数的情况呢当容器退出时Docker 会自动清理容器文件系统以及匿名卷但具名卷同样保留。如果你希望容器生命周期结束就把卷也带走唯一的办法是配合临时匿名卷使用并且在启动时就不要以具名方式挂载。这个机制很多人理解不到位才会出现“我明明删了容器磁盘空间还是不释放”的疑问。3.2 容器删除后卷要怎么收尾假设你已经决定彻底删除一个项目只靠 docker rm 是不够的。整个过程应该是停止容器docker stop 容器名删除容器docker rm 容器名查看卷列表docker volume ls删除对应卷docker volume rm 卷名这里要考虑一个细节docker stop 只是发送停止信号容器还在去掉所有引用后 docker volume prune 才能把悬空卷清掉。很多人做完第 2 步就以为大功告成等到执行 prune 时发现空间并没有减少多少原因就是卷没有删。所以收尾一定要把卷的删除也纳入清单。如果你的容器是用 docker-compose 管理的那删除项目时最好用docker compose down -v这个 -v 参数表示同时删除 compose 文件中定义的卷。需要留意的是它删除的是 compose 管理的具名卷但不会删除 Dockerfile 或 Compose 文件中用匿名卷方式定义的卷。我遇到过某些服务里同时用 volume: 定义具名卷、又用 image 的 VOLUME 指令产生匿名卷即便执行了 down -v匿名卷仍然在还是不释放空间。这时候就要回到手动方式处理。3.3 不要让数据卷和容器生命周期纠缠不清的规划思路说了一些命令核心是想引出这个结论与其让卷在容器删除后悬在外面不如从一开始规划好卷的生命周期。哪些卷需要长期保存哪些只是临时缓存创建时就通过 label 区分。临时缓存用的卷尽量做成匿名卷并且配合 --rm 使用长期数据如 MySQL 的数据文件老老实实用具名卷并打好标签。这套思路落实之后清理就不再是每次都要临时动脑的事。比如我现在的习惯是数据库、消息队列的持久化数据具名卷标签 labelpersisttrue本地开发中间产物、临时缓存匿名卷容器用 --rm 运行跨容器共享的临时数据具名卷但标签 labeltemptrue清理的时候直接跑docker volume prune --filter label!persist -f既能把所有非持久化的卷清走又不会误删数据库数据。这套方式看着简单但比每次手动确认哪个卷能不能删要安全得多。4. 实操流程与典型踩坑案例回顾4.1 一个完整的清理流程照着做就行假设现在要清理一套已废弃的旧项目这套项目当初是用 docker run 启动的使用了一个具名卷 oldapp_data以及若干匿名卷。完整流程如下第一步先看卷当前有没有被引用docker volume inspect oldapp_data如果返回的容器列表为空那可以直接删。如果不为空先停容器docker ps -a --filter volumeoldapp_data docker stop 容器ID或名称第二步删除容器docker rm 容器ID或名称第三步删除卷docker volume rm oldapp_data第四步清理其他悬空卷docker volume prune -f这四步做完可以用 docker system df 再对照一下释放的空间。整个过程没啥高深技巧但很多人会卡在第二步容器已经退出docker rm 执行却报错“conflict: unable to remove repository reference”。这种情况通常是容器名冲突或者镜像被多个容器引用先清理掉引用它的容器就行。另一个常见情况是容器处于 paused 状态要先 docker unpause 再删除。4.2 踩坑实录一docker compose down 不带 -v卷残留下好几个月我之前帮朋友排查一台测试服务器磁盘占用 80% 以上。朋友信誓旦旦说 docker compose down 已经把测试环境删了还在 docker ps -a 里看不到任何容器说不可能有遗留。结果我一看 docker volume ls好家伙几十个测试环境的卷全都在nacos 的数据、redis 的 rdb、mysql 的 ibdata加起来快 40GB。原因就是 down 的时候没带 -vCompose 默认只删除容器和网络卷就是留给你的得你手动删。处理办法先看是不是还有残留容器docker ps -a docker volume ls确认没有容器引用之后执行docker volume prune -f一下释放了几十GB。这个坑实在太普遍了尤其在习惯了 docker compose down 之后再手动清理其他资源的人眼里特别容易漏掉。从那次起我自己写项目脚本都会把清理明确写成 docker compose down -v而不是让维护者自己琢磨到底还要不要删。4.3 踩坑实录二卷删不掉其实是被一个“已退出的容器”占着又一次真实操作中我删一个日志服务的卷报错提示卷正在使用中。docker ps 一看根本没有运行中的容器。折腾了好一会儿才反应过来有些容器处于 exited 状态但没被删除它们同样持有卷引用Docker 判定为“还在使用”。这时候 docker system df 也不会告诉你具体是哪一环的问题只能去查所有容器。排查命令较快的写法docker ps -a --filter volumemy_log_volume或者直接遍历所有容器看挂载for c in $(docker ps -aq); do echo $c; docker inspect -f {{json .Mounts}} $c | grep my_log_volume echo found in $c; done一旦找到是哪个容器还占着直接删除该容器卷就变成悬空状态可以被删除了。这个坑提醒我清理卷之前先看看有没有“假引用”的容器别急着怀疑系统故障。4.4 踩坑实录三df 显示容器目录还是满但卷已经清干净了有一次我清理完 volumes执行 docker system df 发现卷占用确实降下来了但 df -h 一看宿主机磁盘剩余空间没怎么变。查来查去才发现问题根本不在数据卷而是 Docker 的 overlay2 目录里积累了太多构建缓存。构建缓存虽然不会出现在 docker volume 的统计里但会占据大量磁盘空间。这时候正确手段是docker builder prune -f或清理所有构建缓存docker builder prune -a -f另外还有一个磁盘黑洞就是容器日志文件。如果容器创建时没有设置 log-optsjson 格式的日志会一路疯涨主要集中在 /var/lib/docker/containers/容器ID/ 下的 -json.log 文件里。Docker 不会自动清理这些日志因为它是默认写在那里的。处理方式要么在 daemon.json 里配{ log-driver: json-file, log-opts: { max-size: 10m, max-file: 3 } }要么手动 truncate 明显过大的日志文件。上面这些内容本质上是容器文件系统的一部分不是 volume但排查磁盘满问题时经常被骗到。4.5 使用脚本实现半自动清理但一定给人留确认机会清理 volumes 这个操作主要风险在误删。手动跑命令已经够繁琐写脚本自动清理又怕误删重要数据。我的折中方案是脚本先扫描生成待清理列表再人工确认执行。脚本大概长这样#!/bin/bash echo 容器卷使用情况 docker system df echo echo 悬空卷列表 docker volume ls --filter danglingtrue echo echo 非 persist 标签卷列表 docker volume ls --filter label!persist echo read -p 是否清理所有悬空卷? (y/N) confirm if [ $confirm y ] || [ $confirm Y ]; then docker volume prune -f echo 清理完成 else echo 跳过清理 fi这样既能快速看到一次全貌也不会在无意识的情况下删掉东西。说白了清理 volumes 是要养成习惯的而不是等到磁盘告警了才临时抱佛脚。把脚本放到 cron 里当然也行但我始终觉得清理动作最好留一个人工确认环节特别是不能确定标签规范是否完美的时候。5. 常见问题与排查技巧整理5.1 排查乱局的速查表故障现象可能原因解决办法docker volume rm 报 volume is in use卷被未删除的容器引用包括已退出的容器先 docker ps -a --filter volumexxx 找到容器删除容器后再删卷docker system df 显示卷占用小但磁盘满build cache 或容器日志占用大执行 docker builder prune -f并检查容器 json 日志大小卷删除了但空间释放不明显文件可能已被删除但仍被进程占用用 lsof L1 查找已删除但仍未释放句柄的进程重启该进程docker volume prune -a 误删了想留的卷没有用 label 或 filter 区分创建卷时打好标签清理用 filter 而非盲目 -acompose 项目清理后卷还在docker compose down 没有带 -v确认不再需要持久化数据后使用 docker compose down -vbind mount 目录下文件被当作卷清掉把宿主机绝对路径的挂载误认为 Docker volume明白 bind mount 不由 volume 管理检查时注意别用 volume 命令去清它这张表基本覆盖了我日常最常遇到的情况。只要照着排查大部分问题都能定位到几个固定环节。5.2 高风险操作提醒docker volume prune -a 是高风险操作。平时我不太推荐一上来就加 -a默认 prune 只清理悬空卷已经能解决大部分问题。-a 会把所有没有被容器引用的具名卷也一并删除哪怕你想留着备份。另外如果卷由第三方驱动创建比如某些 NFS 插件或云厂商卷插件docker volume rm 的行为可能是删除远端存储上的数据未必只在本地删个目录。远程驱动上的数据一旦删除恢复难度极大尤其是有物理备份策略缺失的团队。命令行操作前一定先看卷的 Driver 是什么不是 local 驱动就格外谨慎最好查一下该驱动的文档确认 rm 到底做了什么。还有一点容易被忽略容器挂载卷的时候如果宿主机目录因误操作被移动或删除可能导致卷信息不一致。比如 /var/lib/docker/volumes/xxx/_data 下面某个子目录被手动挪走了Docker 内部元数据里却还认为卷正常。这类情况通常需要把容器停了重新校验卷内容甚至 recreate 容器不建议直接在运行中操作 underlying 目录。数据卷目录本来就是 Docker 管理的内部状态不是让你随便扒拉的地方贸然手动改文件可能造成卷对应关系错乱。5.3 用 lsof 排查磁盘层面“删了但空间不释放”的怪问题有时提示卷删除成功docker volume ls 也看不到它了但磁盘空间就是不下降。这种问题往往和进程占用有关。某个容器进程可能还在使用旧的文件句柄导致文件虽然在目录结构里没了进程却依然占用空间。排查lsof L1这个命令会列出所有“已删除但被进程占用”的文件。找到对应进程 ID 和文件后重启该进程或者 kill 掉空间才会真正释放。Docker 场景下容器里的进程崩溃重启时如果没自动回收句柄就可能出现这个问题。实际上这种情况在 Elasticsearch、MySQL 这类长时间不重启的服务里偶尔能碰到。5.4 定期清理的完整建议清理 volumes 本身不难难的是怎么形成习惯且不误删。我见过很多人折腾半天最后答案就是四个字标签 定期。创建卷的时候认真打标签把清理逻辑固化到脚本里再配合 crontab 定期跑一次 dry run就足够了。比如每周一的凌晨执行一遍docker volume prune --filter label!persist -f但前提是 persist 标签真的只打在那些需要长期保留的卷上。我建议在 cron 执行前先跑一遍不带 -f 的预览版并把输出追加到日志里docker volume prune --filter label!persist日志放在固定目录哪个卷被清了、什么时候清的都有据可查。不要小看这个动作真的出了问题能帮你回溯什么时间删了什么。6. 个人体会与这次清理工作的收尾清理 volumes 这件事说到底不是一锤子买卖而是一个持续的管理习惯。我当年第一次大规模清理因为没搞懂匿名卷和具名卷的差别误删了一个原本还想保留的数据卷从那以后我就把“创建卷时打标签”当成了铁律。后来每次遇到磁盘告警我都能第一时间排除“卷占用”这个大变量专心去查容器日志和 build cache 这些其他因素。还有个小技巧值得分享每次执行 prune 前不管多急都先跑一遍不带 -f 的预览列表截图或者记下输出。这样清理完成之后你可以对比实际释放空间和预览时的预估空间一旦对不上说明还有别的进程占着文件句柄可以立刻顺着排查。很多所谓“清理无效”的假象都是因为忽略了这个反差信号。如果后面你还需要迁移宿主机或者重建 Docker 环境volumes 这边的处理逻辑也类似先将容器全部停止并导出需要保留的数据再清理不需要的卷最后逐项恢复。别指望容器自动把卷带走Docker 的设计意图就是卷的生命周期与容器解耦理论上你删掉所有容器后卷依然可以是安全的备份持有者。我现在清理 Docker 空间的基本流程就是docker system df 总览、docker volume ls --filter danglingtrue 定位悬空卷、再按标签分批 prune。这套流程没什么高深技术但每次都能快速见效把空间腾出来也保证不误伤该留的数据。希望这篇文章里写的这些习惯和教训能让你少走一些我走过的弯路。