用 Docker 时间一长,最容易出事儿的还真不是容器跑不起来,而是你拉了一堆镜像,磁盘分区直接变红。尤其是做开发的朋友,Redis、MySQL、GitLab、各种中间件的镜像随手就docker pull,再加上每次改 Dockerfile 重新 build 产生的旧镜像层,/var/lib/docker目录轻轻松松就能吃掉几十个 G。这篇文章专门聊镜像删除和清理这档子事,从最基础的docker rmi讲到整体空间回收,一步步说清楚怎么把磁盘救回来,也把那些容易踩的坑一并排掉。适合被镜像占满磁盘困扰的开发者、运维和自学者,内容不假设你有多少 Docker 基础,照着敲就行。
1. 删除镜像之前,先把依赖关系理清楚
1.1 镜像分层与引用关系
很多人上来就是一通docker rmi,删不掉就开始怪命令不对,其实核心问题出在没理解 Docker 对象之间的引用关系。镜像本身是由多层只读层叠加出来的模板,容器是基于镜像创建出来的运行实例。用个生活化比喻:镜像是“安装包”,容器是“装好并打开的软件”。你想删安装包,前提是这个软件没在运行、也没保留在桌面上;同理,如果容器还活着,镜像就被容器引用着,Docker 不会让你轻易删掉。
所以删除镜像之前必须先搞清楚:哪些容器还在占用我想要的镜像?哪些镜像已经彻底没用了?如果带着引用关系硬删,要么报错给你看,要么用-f强删,但强删之后容器那边会留下一个“没有底层模板”的悬空状态,排查问题时照样头疼。
1.2 先用 docker system df 摸底
不论是想删单个镜像,还是想一次性清理磁盘,第一步都建议先跑一下docker system df,它会像磁盘分析工具一样,把 Docker 各个对象占用的空间一次性列出来。我一般会先执行这个命令,再决定下一步怎么清:
docker system df输出大概是这样的:
TYPE TOTAL ACTIVE SIZE RECLAIMABLE Images 12 4 28.3GB 19.8GB (69%) Containers 6 2 1.2GB 1.1GB (91%) Local Volumes 8 3 4.5GB 3.0GB (66%) Build Cache 35 0 6.7GB 6.7GB (100%)RECLAIMABLE 这一列非常关键,它表示当前状态下可以释放的空间。比如 Build Cache 的 RECLAIMABLE 是 100%,意味着只要缓存一清,6.7 G 全部回来。Images 回收率 69%,说明有些镜像有 tag 或被容器占用,暂时不能全清,但悬空镜像部分是可以回收的。看到这张表之后,再决定是删单个镜像,还是做整体清理,就不会像无头苍蝇一样乱灌命令了。
1.3 哪些镜像能删、哪些不能删
根据我的经验,可以把镜像分成三种状态:能直接删、需要先处理再删、暂时不要删。
- 能直接删:没有任何容器引用、也没有被其他镜像依赖的镜像,尤其是悬空镜像。
- 需要先处理再删:正在运行的容器或已停止但未删除的容器所引用的镜像,先删容器或
docker rm再删镜像。 - 暂时不要删:被多个容器同时使用的镜像、带有明确版本 tag 且业务还在用的镜像,删掉可能引发下次部署直接失败。
有多个 tag 的情况也要注意。同一个镜像 ID 可能同时挂了mysql:8.0和mysql:latest两个标签,删除其中一个标签时,镜像文件本身不会真的没了,只有最后一个标签也被删掉时,镜像才可能变成悬空状态,随后才能被清理。这个细节很多人第一次遇到会误以为“删了个寂寞”,其实底层逻辑就是这样。
2. 掌握 rmi:镜像删除的基础操作
2.1 命令基本写法和参数
删除镜像最核心的命令就是docker rmi,也可以写成长格式docker image rm,两者完全等价。最常用的写法有几种:
# 按名称和标签删除 docker rmi mysql:8.0 # 按镜像 ID 删除 docker rmi 0d64f46acfd1 # 一次删除多个镜像 docker rmi nginx:1.25 redis:7.0 mysql:8.0 # 强制删除 docker rmi -f mysql:8.0其中 ID 不一定要写完整,只要你输入的短 ID 能唯一匹配一个镜像就行。比如完整 ID 是0d64f46acfd1a3860b64d8d5d1f7f5c8e6f06b1d,通常写前四位到前六位就够了,Docker 会自己解析。
删除成功后,终端会输出被删掉的镜像层的 ID 列表。如果你看到Untagged: mysql:8.0,说明这个 tag 被删了;如果看到Deleted: sha256:...,说明镜像层数据真的被清理了。只出现 Untagged 而没出现 Deleted,就是前面说的多 tag 情况,镜像本体还躺在磁盘上。
2.2 按标签删、按 ID 删有什么区别
我见过不少新手问:我明明删了 tag,为什么docker images里还能看到这个镜像?这里要专门说一下。
如果两个 tag 指向同一个镜像 ID,你执行docker rmi mysql:latest,只是把 latest 这个标签从镜像记录里摘掉,镜像层还是被mysql:8.0引用着,所以docker images里依然能看见mysql:8.0,这不算把镜像删掉。
如果你直接用镜像 ID 删除,并且这个 ID 对应多个 tag,Docker 会拒绝执行,提示你“image is referenced in multiple repositories”,需要先删 tag 或使用-f。所以实际操作中,我推荐优先用“仓库名 + 标签”来删,这样你知道自己到底删了什么,不会误伤共用 ID 的其他 tag。
2.3 force 强制删除到底该不该用
docker rmi -f是个双刃剑。它的作用是在镜像被容器引用时强行解除引用并删除镜像数据,但容器本身并不会被删除,只是它依赖的镜像数据没了,后续想启动这个容器基本没戏。
我自己的原则是:能不用-f就不用。如果你只是想腾空间,先把容器处理干净,再正常删镜像,花不了几秒钟。什么情况下可以破例呢?比如你已经确认所有容器都不重要,明天就要全部重建,这时候docker rmi -f $(docker images -aq)能快速把所有镜像一次性干掉,省去先删容器的步骤。但前提是“你确认没问题”,一旦误删想恢复,只能重新 pull 或 build。
2.4 新手最容易搞混:docker rm 和 docker rmi 的区别
这个问题太常见了,必须单独拎出来讲。docker rm删的是容器,docker rmi删的是镜像。字母上就差一个 i,实际作用完全不是一回事。
可以这样记:先有镜像才有容器,所以清理时顺序是“先 rm 后 rmi”。你不可能在容器还存在的情况下把镜像删掉。另外docker ps默认只显示运行中的容器,已停止的容器要用docker ps -a查看,很多人以为停止了就等于删除了,其实停止的容器仍然占磁盘空间,也仍然引用着镜像,这也是“怎么删镜像都删不掉”的一大原因。
3. 系统级清理:一次搞定悬空镜像、无用容器和缓存
3.1 什么是悬空镜像,image prune 怎么用
悬空镜像,英文叫 dangling image,指的是没有 tag、也没有被任何容器引用的镜像层。它通常是这么产生的:你写了一版 Dockerfile,构建出镜像my-app:v1,然后改了代码又构建了一次my-app:v1,新镜像会覆盖旧镜像的 tag,旧的镜像就失去标签,变成<none>:<none>的悬空镜像。
单独清理悬空镜像用:
docker image prune -f加-f是跳过确认提示。如果想把所有“没有被容器使用的镜像”全部清理掉,无论是悬空还是有 tag 的,可以加-a:
docker image prune -a -f注意这个-a很危险。它会删除所有没有被任何容器引用的镜像,包括你刚 pull 下来准备之后用的centos:7、ubuntu:22.04等。如果只是想腾空间,而且不想下次部署时重新拉,我建议平时只清理悬空镜像就足够了。想看到底有哪些悬空镜像,可以先执行:
docker images -f dangling=true确认一下再清,心里更有数。
3.2 system prune 一键清理
如果你既想删悬空镜像,又想清理停止的容器、无用的网络和构建缓存,一条命令就能搞定:
docker system prune -f这是日常最推荐的清理命令。它不会删带有 tag 且没有被容器使用的镜像,也不会删数据卷,相对温和。如果磁盘已经红到发紫,想做得更彻底,可以用:
docker system prune -a --volumes -f这个组合拳的效果是:删除所有停止的容器、所有未被容器使用的网络、所有悬空镜像、所有未被容器使用的已标记镜像,以及所有未被容器使用的匿名数据卷。非常有效,但副作用也大,尤其是--volumes,会把没有容器引用的匿名卷里的数据一并清掉。如果你不太确定有没有重要数据,别加这个参数。
建议第一次执行时不要加-f,让 Docker 把准备删掉的资源列出来给你确认一遍,看清楚哪些会被删,再敲 y 回车。虽然多一步,但能防手滑。
3.3 构建缓存才是隐形杀手:builder prune 的用法
很多人清完了镜像,发现磁盘空间还是没怎么降,这时候八成是构建缓存(Build Cache)在作怪。每执行一次docker build,Docker 都会把中间层和缓存写入/var/lib/docker,日积月累,缓存比镜像本身还大一点也不奇怪。
单独清理构建缓存用:
# 清理不再使用的构建缓存 docker builder prune -f # 清空全部构建缓存 docker builder prune -a -f # 只清理指定时间之前的缓存,比如 24 小时前 docker builder prune -f --filter until=24h清缓存的影响是:下次重新构建同一个项目时,无法复用之前的层缓存,构建时间会变长。所以如果你之后还要频繁 build,时间比磁盘空间更值钱,那就别全清,用until=24h或until=48h只清旧缓存更合理。
还有一个相关小技巧:临时想要构建一个新镜像又不想污染缓存,可以docker build --no-cache,但它不是清理方案,只是跳过缓存构建,别和docker builder prune搞混了。
3.4 数据卷别乱删
镜像、容器、构建缓存都清理完了,还剩下 Local Volumes 可能占着好几 G。数据卷是用来持久化容器数据的,比如 MySQL 的数据库文件、GitLab 的仓库文件都会放在卷里。
清理数据卷用:
docker volume prune -f但这句话只会删除“没有被任何容器使用的匿名卷”。带名字的数据卷即使容器删了,卷也会保留下来,避免数据丢失。如果你确认某些命名卷也没用了,直接用docker volume rm <volume-name>删掉即可。
我在实际项目里见过不少把数据库文件放匿名卷里,然后docker rm容器时没加-v,导致卷残留、磁盘越来越满的情况。所以删容器时如果能确定不需要保留数据,可以直接用:
docker rm -v <container-id>把容器和它关联的匿名卷一起删掉,省得后面还要单独清理。但带上-v之前一定想清楚,卷里的数据不会再回来。
4. 实战复盘:从“磁盘红了”到“空间释放”的一次完整排查
4.1 场景描述:磁盘告警后的处理流程
有一次我在一台测试服务器上发现磁盘使用率到了 95%,监控直接报警。这台机器上装了 Docker,跑着好几个 API 服务,还经常构建新镜像。登录服务器后,我习惯性地先看整体磁盘情况:
df -h输出里能看到/var/lib/docker所在的分区已经用了 80 多 G,再看一眼docker system df,Images 占了 34 G,Build Cache 占了 22 G,Containers 占了 4 G,Local Volumes 占了 9 G,可回收空间总计超过 50 G。问题很明显了:这是一台典型的“镜像和缓存长期不清理”的机器。
4.2 用命令定位空间大户
如果还想进一步定位是哪个镜像占空间,可以用:
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"或者直接列出所有镜像按大小排序,用docker image ls配合sort。我会特别关注那些带<none>标签的悬空镜像,以及几个动辄 2 G 以上的大镜像。
在排查时不要只看 Images 和 Containers,还要留意日志文件。容器日志默认写在/var/lib/docker/containers/<container-id>/-json.log,如果某个服务疯狂打日志,这个文件能轻松涨到几个 G。想确认哪个容器日志最大,可以这样:
for id in $(docker ps -aq); do log_path=$(docker inspect --format='{{.LogPath}}' $id) size=$(du -sh "$log_path" 2>/dev/null | awk '{print $1}') echo "$id $size" done不过这是题外话,本次主要解决镜像和缓存,日志问题就不展开了。
4.3 逐步执行清理并对比前后空间变化
我的清理顺序是:先清缓存,再清容器,再清悬空镜像,最后清卷,尽量不影响正在运行的服务。
第一步,先清理已经停止的容器:
docker container prune -f系统提示删除了 4 个停止容器,收回了大约 1.8 G 空间。注意正在运行的容器不会被删除,所以这一步对线上服务没有影响。
第二步,清构建缓存:
docker builder prune -f --filter until=48h这一步释放了 15 G 空间,效果最明显。如果你平时构建很频繁,这里绝对是空间大头。
第三步,清理悬空镜像:
docker image prune -f删除了 6 个<none>镜像,释放了 4.3 G。我没有用-a,因为本地还放着一个 CentOS 7 镜像和一个 MySQL 8.0 镜像,虽然当前没被容器用,但过两天部署还会用到,没必要重新拉。
第四步,再跑一次docker system df确认结果。总占用从 69 G 降到了 47 G 左右,可回收空间还有 12 G,主要是暂时不能删的镜像和遗留的命名数据卷。磁盘使用率从 95% 降到了 82%,服务恢复健康水位。
4.4 一套适合定期执行的镜像清理策略
手动清理只能解一时燃眉之急,想长期不踩雷,我建议把这套操作固化成脚本。比如在服务器上放一个docker-clean.sh:
#!/bin/bash docker image prune -f docker container prune -f docker builder prune -f --filter until=24h docker system df > /var/log/docker-clean.log然后通过 crontab 每周六凌晨跑一次:
0 2 * * 6 /usr/local/bin/docker-clean.sh >> /var/log/docker-clean.log 2>&1这个策略的核心思路是:绝不在自动脚本里加-a和--volumes,只做安全清理。这一步能清掉绝大多数可回收空间,又不会误删你辛辛苦苦拉下来的业务镜像。如果你有特殊需求,比如某些镜像必须长期保留,可以给镜像打上特定标签,然后在脚本里加 filter,比如--filter label=keep,这样带 keep 标签的镜像永远不会被清理。
5. 常见问题速查与避坑笔记
5.1 明明删了镜像,磁盘空间却没有释放
这个问题出现频率极高。删了镜像但空间没释放,原因无非以下几种:
- 容器还在引用镜像,删除的只是镜像的 tag,底层数据没被真正清理。
- 停止的容器没有被删除,容器可写层和日志文件还占着空间。
- 数据卷没删,卷数据独立于镜像生命周期。
- 构建缓存占了大头,而你只删了镜像,没清 Build Cache。
- 你磁盘上的空间本来就有一部分来自日志文件或其他目录,不全是镜像。
排查顺序建议:先docker ps -a看有没有残留容器,再docker system df看缓存和卷的占用,最后看日志文件。绝大多数“删了没释放”的问题,都能在这几步里找到答案。
5.2 删除时报 image is being used by container
这个报错的意思很直白:有容器正在使用这个镜像,哪怕容器是停止状态也算。你可以先找到引用它的容器:
docker ps -a --filter ancestor=mysql:8.0找到后,如果这个容器确定没用,直接删掉:
docker rm <container-id>然后再删除镜像。如果容器特别多,可以一次性把所有停止容器都删掉:
docker container prune -f之后再docker rmi就不会报错了。强行docker rmi -f虽然能绕过这个限制,但不建议成为习惯,因为后续你会面临一堆无法启动的容器残留。
5.3 多个 tag 导致删不掉
前面提到过,同一个镜像 ID 挂了多个 tag,删的时候会提示 referenced in multiple repositories。处理方法是先逐个删 tag:
docker rmi repo1:tag1 repo1:tag2 ...等到只剩最后一个 tag 的时候,再删一次,镜像层才会真正被释放。如果不想这么麻烦,而且确认所有的 tag 都不要了,可以直接强制删镜像 ID:
docker rmi -f <image-id>然后docker images -a确认是否还有残留的<none>镜像,再用docker image prune -f清理掉。
5.4 用 :latest 标签带来的麻烦
我见过很多项目为了省事,拉镜像、启动容器全用:latest。时间一长,latest 被反复覆盖,旧镜像全部变成悬空镜像,占用大量空间。而且:latest不代表“稳定版本”,谁也无法保证它下个版本不会引入破坏性的变化。如果磁盘紧张,还容易造成明明清理过、过几天又满了的循环。
所以个人建议:生产环境或平时练习,都尽量把 tag 固定到具体版本上,比如nginx:1.25、mysql:8.0.36。这样至少在docker images里能一眼看出版本,也便于清理和回滚。构建自己项目镜像时,也尽量打上版本号,别只打个 latest。
我自己的习惯是:隔段时间就跑一下docker system df,看一眼可回收的空间,发现超过 10 G 就顺手清一轮。清理时只动悬空镜像、停止容器和旧构建缓存,绝不对命名的数据卷和正在使用的版本镜像下手。这样操作风险可控,而且每次基本都能把磁盘水位压回健康区间。Docker 的空间管理本质上不是高深技术,而是肯不肯养成定期看一眼的习惯。把这几个命令记熟,你的磁盘就不会再动不动就报警了。