Docker镜像清理与磁盘空间回收:从分层存储到prune实战指南
2026/9/16 19:53:46 网站建设 项目流程

前几天接手一台测试服务器,df -h一看根分区已经红了,/var/lib/docker占掉一大部分,里面躺着七十几个镜像,有旧的构建残留,还有一堆<none>悬空层。当时有个同事说:“直接用docker rmi $(docker images -q)不就行了?”我赶紧把他拦住了。这句话看着简单,真在生产环境里执行,轻则删掉正在用的镜像导致容器起不来,重则把带旧标签的历史镜像全清掉,后面想回滚连参照物都没有。后来我花了一晚上把 Docker 镜像的删除与清理完整捋了一遍,才意识到大部分“删不掉”“删了没释放空间”的困惑,根源都是没有理解镜像的分层存储和引用关系。这篇文章就按我当时排查的顺序来写,从定位空间去向、单个删除、批量清理到全量回收,最后是常见报错和一份防误删经验,希望对被 Docker 磁盘占用折磨的人有点用。

1. 先搞清楚镜像到底占在哪、为什么删了还占空间

1.1 分层存储:镜像不是一个大文件

Docker 镜像本质上是一组只读层的叠加,每一层对应 Dockerfile 里的一条指令。你执行一次docker pull nginx:latest,拉回来的不是单个大文件,而是很多层分别下载,再通过联合文件系统(overlay2 是当前最常见驱动)挂载成一个可用的根文件系统。

这里就引出一个关键概念:层是可以被多个镜像共用的。比如你基于同一个ubuntu:22.04基础镜像构建了 A、B、C 三个业务镜像,那么 A、B、C 的底层会尽量复用那几层系统文件。此时你删除 A 镜像,只是把 A 的顶层引用删掉,公共的基础层并不会跟着删,只有当一个层完全没有被任何镜像和容器引用时,Docker 才会真正把它从磁盘回收。

理解了这一点,很多怪现象就说得通了:明明删了好几个镜像,/var/lib/docker却没明显变小,大概率是底层被其他镜像或容器“拽住”放不掉。所以删除镜像前,先别急着敲命令,拿docker system df看一眼全局账单,永远是第一步。

1.2 先记账:用 docker system df 给空间“对账”

在 Docker 较新版本里,执行docker system df会输出一张表,大致长这样:

TYPETOTALACTIVESIZERECLAIMABLE
Images721841.23GB28.76GB (69%)
Containers3454.58GB4.58GB (100%)
Local Volumes1246.31GB5.02GB (79%)
Build Cache9608.76GB8.76GB

这张表就是整个磁盘占用的“科目余额”。Images 代表镜像占用的底层数据总量,Containers 代表所有容器可写层的占用,Local Volumes 是数据卷,Build Cache 是构建缓存。前三列是比较直接的占用,最后一列 RECLAIMABLE 才是你真正能安全回收的空间。

如果你在旧版本 Docker 上执行docker system df没有 Build Cache 这一行,说明默认还是 legacy builder,配合docker builder prune也能清理。想要更细的维度,可以加-v参数,docker system df -v会把每个镜像、容器、卷的占用明细都列出来,方便你看哪些是大户。定位宿主机层面占用时,再用经典的du -sh /var/lib/docker/*按目录扫一遍,通常overlay2containers是最占空间的目录。

提示:排查空间问题,我习惯先docker system dfdu -sh /var/lib/docker/*,前者管容器引擎内部资源,后者管宿主机目录分布。两套数据一交叉,基本能锁定问题大头。

2. 基础操作:docker rmi 删除镜像的正确姿势

2.1 按标签删还是按 ID 删?先搞清引用关系

删除镜像最直接的命令是docker rmi。你可以按标签删,也可以按镜像 ID 删,但我强烈建议日常优先按标签删。

docker images docker rmi nginx:1.25

按标签删除的含义是:解除这个标签对镜像 ID 的引用。如果一个镜像 ID 只被这一个标签引用,解除后没有其他引用,该镜像的独有层才会进入回收流程;如果其他标签也指向同一个镜像 ID,那么这个镜像数据仍然保留,只是少了一个名字。

按 ID 删则容易踩坑。当你执行docker rmi <IMAGE_ID>时,如果这个 ID 同时被多个标签引用,Docker 会报错:

Error response from daemon: conflict: unable to delete <IMAGE_ID> (must be forced) - image is referenced in multiple repositories

这种时候,需要先逐个删除标签,最后再清理掉没有标签的残留镜像。我见过有人一看到 conflict 就直接加-f强制删,结果系统里留下一个悬空的镜像层,名字变成<none>:<none>,空间反而更难回收。还是那句话:删除镜像,本质上是删除“标签引用 + 没有引用的层”,绕开引用关系去强删,就是给自己埋坑。

2.2 删除失败:容器占用与 -f 强制删除的坑

镜像删除失败,最常见的原因是容器还在引用它。报错一般是:

Error response from daemon: conflict: unable to remove repository reference "nginx:1.25" (must force) - container 5f2c... is using its referenced image 6d7a...

注意,这里说的是“container is using”,而不是“container is running”。只要容器没有被真正删除,哪怕它是停止状态,镜像一样删不掉。所以正确的顺序是:先删容器,再删镜像。

docker rm 5f2c... # 或者 docker rm -f 5f2c... docker rmi nginx:1.25

如果手头有大量停止容器挡住了镜像,可以先批量清掉停止状态容器:

docker container prune

运行后会列出将被清理的容器列表并让你确认,输入y才会执行。这个命令我建议哪怕在开发机上也看清楚再确认,因为有些停止容器里可能有你需要保留的写层数据。

关于docker rmi -f强制删除,我的态度很明确:能不用就不用。-f会绕过引用检查,把一个还在被容器使用的镜像标记删除,此时容器文件系统里的底层可能已经变得不完整,容器虽然没停,但随时可能出诡异问题。生产环境里为了“必须删掉”而强删一个运行中容器的镜像,后续排查成本往往远大于省下的那点磁盘空间。

3. 专项清理:悬空镜像与未使用镜像

3.1 悬空镜像和未使用镜像,两种“垃圾”别搞混

很多人清理 Docker 时只会用docker rmi,却不清楚docker image prune系列命令背后的逻辑。要把它用明白,先分清楚两个概念:悬空镜像(dangling)和未使用镜像(unused)。

悬空镜像是指没有任何标签引用的镜像,在docker images输出里显示为<none>:<none>。它通常是重新构建镜像时产生的旧层残留,或者前面说的rmi -f之后留下的孤儿层。批量查看悬空镜像可以这样:

docker images -f dangling=true

未使用镜像则是指有标签、但当前没有被任何容器引用的镜像。比如你拉了一个mysql:8.0,暂时没跑容器,它就是一个 unused 镜像。这种镜像有名字、有版本,只是暂时闲置,不代表没用,尤其在生产环境里,可能随时要基于它回滚或者重新创建容器。

理解了这两个概念,你就能理解为什么docker image prune默认只删悬空镜像,因为相对安全;而加上-a会把所有未使用镜像也一并清理,风险等级马上不一样。

3.2 prune 命令怎么用才安全

docker image prune的常用姿势:

docker image prune # 只清理悬空镜像 docker image prune -a # 清理所有未被容器使用的镜像 docker image prune -a --filter "until=168h" # 只清理 7 天前创建且未使用的

有人习惯用docker rmi $(docker images -q)一键清空所有镜像,其实这个命令在生产环境很容易半路报错:只要有一个镜像被任何容器引用,整条命令就会中断,导致一部分删掉、一部分没删掉,局面很难看。相比之下,docker image prune -a的处理逻辑更明确:先检查引用关系,未被使用的才进候选列表,而且执行前会逐条列给你确认。

再补充一个我常用的安全组合拳:先清理死容器,再清悬空镜像,最后按过期时间清理未使用镜像。

docker container prune -f docker image prune -f docker image prune -a --filter "until=168h"

这里的--filter until只对镜像的创建时间生效,能避免误删最近几天还在用的镜像。如果团队里有人给镜像打了 label,也可以用--filter label=keep=true来保护关键镜像。总之,prune 系列命令比rmi $(...)更适合批处理,原因是它每一步都会做引用校验,不会像组合命令那样半路崩掉。

4. 全量清理:docker system prune 与构建缓存回收

4.1 system prune -a --volumes 到底会动什么

当磁盘空间告急,很多人会直接祭出docker system prune -a --volumes。这条命令的清理范围很大,执行前务必逐项看清:

docker system prune -a --volumes

它会清理的内容包括:所有停止的容器、所有未被容器使用的网络、所有悬空镜像、所有未被任何容器引用的镜像,以及所有没有被容器使用的卷(volume)。换句话说,这台机器上“当前没有被运行中容器使用”的 Docker 资源,基本都会被端掉。

这也是最容易出事的一条命令。尤其是--volumes,它会删除没有被容器引用的匿名卷,而很多中间数据、临时数据库文件都是存在卷里的,一旦删除不可恢复。我自己只在本地开发机上敢跑完整版,服务器上从来都是分步清理:

docker system prune -f # 清停止容器、悬空网络、悬空镜像、构建缓存 docker image prune -a --filter "until=168h" # 单独清闲置镜像,保留近期可能回滚的 docker volume prune # 单独处理卷,先看清楚列出的列表再确认

有人觉得分步麻烦,但分步的意义是每一步都有确认机会。尤其是 volume 和镜像不同,镜像删了可以重新拉取或重新构建,卷删了里面的数据就真的没了。不要为了省三分钟命令时间,把数据库备份和挂载数据一起端掉。

4.2 构建缓存回收:BuildKit 里最容易被忽略的一块

我在第一张docker system df表里特意标出了 Build Cache,因为很多人清理完镜像、容器、卷之后,发现空间释放不明显,回头一查,构建缓存可能占了几个 GB 甚至十几个 GB。

构建缓存是 Docker 构建镜像时留下的层缓存。它的价值是:下次构建时如果 Dockerfile 前面的步骤没有变化,可以直接复用缓存层,不用重新下载依赖。清理之后不会影响任何已有镜像的正常使用,副作用只是下次构建速度可能会变慢。

清理命令:

docker builder prune -f # 旧版 builder docker buildx prune -f # 新版 BuildKit docker builder prune -a --filter "until=168h" # 只清一周前的缓存

如果你用了 BuildKit(Docker 较新版本默认开启),docker system df里的 Build Cache 数据可能很大,但docker image prune并不会清理这部分,需要单独跑docker builder prunedocker buildx prune。这也是很多人“明明删了镜像但空间没回来”的一个重要原因。

另外提醒一点:清理构建缓存后,如果项目里依赖变化频繁,持续集成时第一次构建会比较痛苦。所以我在 CI 机器上通常保留最近 24 小时的缓存,只在空间告警时才会全清。

5. Docker Desktop 场景下的删除与清理

5.1 图形界面入口与磁盘镜像瘦身

如果你用的是 macOS 或 Windows 上的 Docker Desktop,除了命令行,Dashboard 里也提供了镜像管理入口。进入 Images 页面,列表右侧可以对镜像执行 Run、Pull、Remove 操作,按大小排序能很快找到占用靠前的大镜像。在清理镜像时,Docker Desktop 会同时给出“只删除虚悬镜像”或“删除所有未被使用的镜像”的选项,等同于命令行的docker image prunedocker image prune -a

但这里有个容易误判的地方:Docker Desktop 底层是一个轻量级虚拟机,镜像实际存储在这个虚拟机的磁盘文件里。你从图形界面删掉一批镜像后,容器引擎内部空间确实释放了,但宿主机上那个磁盘镜像文件(Windows 下常见的是 ext4.vhdx 或类似文件)不一定马上变小。也就是说,在 Docker Desktop 里看到空间已经回收到可以继续 pull 新镜像,但宿主机 C 盘的剩余容量可能仍然没变化。

这时候需要关注 Docker Desktop 的资源配置。在 Settings -> Resources 里可以调整虚拟磁盘大小上限,如果发现虚拟机镜像文件膨胀得厉害,建议先自己清理一遍容器和镜像,然后重启 Docker Desktop,而不是一味把磁盘上限调大。调大上限只是把问题往后推,并不会真正替你回收空间。

5.2 镜像之外的空间黑洞:容器、卷与日志

Docker Desktop 里磁盘占用的大头往往不是镜像本身,而是容器写层和日志文件。我在帮人排查询问题时经常看到一种情况:docker images加起来不到 10GB,但 C 盘已经红了。打开 Docker Desktop 的磁盘使用详情,才发现几十个停止容器和几百个 json 日志文件才是真凶。

容器只要存在,无论是否运行,它的可写层都会占用空间。批量清理停止容器,命令行是:

docker ps -a docker container prune

日志方面,Docker 默认的 json-file 日志驱动会把容器写到标准输出的内容全部追加到容器目录下的*-json.log文件里。一个高并发服务跑上几天,日志文件冲到几十 GB 很常见。删容器和删镜像都不会动日志文件,需要单独处理。

truncate -s 0 $(docker inspect -f '{{.LogPath}}' <容器名>)

或者更直接地定位大日志:

find /var/lib/docker/containers -name '*-json.log' -size +100M -exec du -h {} \;

这里强调一下:日志文件用truncate -s 0置空,比rm删除更稳妥。因为容器进程可能仍然持有文件句柄,直接删除文件不一定会马上释放空间,反而可能让 Docker 重新创建新文件,导致句柄错乱。如果是在 Docker Desktop 上,宿主文件路径和容器引擎路径不太一样,用docker inspect拿到的 LogPath 是最靠谱的。

6. 常见问题与排查实录

6.1 镜像删不掉?顺着引用链一步步排查

我整理了日常最容易碰到的三类“镜像删不掉”报错和对应解法:

报错现象根本原因处理思路
image is being used by container ...存在未删除的容器引用该镜像docker rm容器,再删镜像
image is referenced in multiple repositories同一镜像 ID 被多个标签引用按标签逐个删除,不要直接强删 ID
No such image: xxx镜像本身已被删除,但命令行参数写的是旧标签或旧 ID重新docker images确认再操作

在不确定是谁拽住镜像时,可以先看所有容器:

docker ps -a --filter ancestor=nginx:1.25

这条命令会列出基于某个镜像创建的容器,包括停止状态的。找到容器 ID 后,docker inspect可以进一步确认容器状态和挂载关系。删镜像之前先理清楚容器和镜像的父子引用,能避免很多“为什么删不掉”的疑问。

6.2 删完空间没释放?三个经常被漏掉的地方

第一种情况是容器写层。停止的容器仍然保留可写层,尤其是那些跑过数据库、缓存服务的容器,写层可能非常大。解决办法是确认不再需要后删除容器,而不是只docker stop

第二种情况是匿名卷。创建容器时如果没显式指定卷,Docker 可能自动创建匿名卷。容器删除后这些卷未必自动删除,docker system df里 Local Volumes 那一栏会显示可回收量。这时候用docker volume prune或者docker volume ls -f dangling=true进一步确认。

第三种情况是中间镜像层残留。前面反复强调过,镜像层只有在没有任何镜像和容器引用时才会被彻底回收。如果你删除时没有用 prune,而只是docker rmi某个标签,可能留下不少悬空层。再执行一次docker image prune就能把这些<none>层清掉。

还有一个经常被忽略的是:如果你之前用docker rmi -f强制删过仍被引用的镜像,容器引擎内部可能出现底层的“孤儿层”,这些层既看不到标签,也可能不在常规 dangling 列表里。这种情况一般只能靠重启 Docker daemon 后再次docker image prune整理,或者用docker system df -v去查无法回收的具体原因。

6.3 防误删:标签策略与清理脚本经验

清理镜像最怕的不是删不干净,而是手滑把还需要的镜像误删。我的方法是:生产环境里给关键镜像打上明确标签,并且统一加一层标签用于保护。

docker build -t myapp:1.4.0 --label keep=true .

清理时,使用过滤条件排除带保护标签的镜像:

docker image prune -a --filter "label!=keep=true"

这里有个细节:docker image prune的 filter 参数不是所有版本都支持label!=语法,使用前可以先docker image prune --help确认。如果没有这个选项,就退回到更保守的做法,先导出当前镜像列表备份,再分批清理。

我自己的习惯是写一个简单的清理脚本,每周在开发机上自动跑:

#!/bin/bash # 清理停止容器 docker container prune -f # 清理悬空镜像和过期未使用镜像 docker image prune -f docker image prune -a --filter "until=168h" # 清理一点构建缓存,保留最近一天 docker builder prune -f --filter "until=24h"

生产环境我不会跑带--volumes的全量清理,也不会自动执行docker image prune -a,因为生产环境里一个曾经用过的旧镜像可能就是回滚的唯一希望。宁可多留几天,也不要为了一点磁盘空间把退路断了。

最后说一个我踩过几次坑之后的体会:Docker 的镜像删除与清理,本质是对“引用”的管理。理解标签、镜像 ID、容器、层之间的关系,比背命令重要得多。命令就那么几个,真正决定你能不能安全腾出空间、不误删数据的,是你有没有在动手前先想清楚哪些资源还有引用、哪些数据删了无法恢复。希望这篇整理,能帮你下次面对红盘告警时,心里更有底。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询