你是否遇到过这个奇怪的镜像?
在学习 Docker 的过程中,很多人都会遇到这样的情况。
执行:
docker images
突然发现:
REPOSITORY TAG IMAGE ID
<none> <none> a1b2c3d4
nginx latest e5f6g7h8
看到这里,很多人的第一反应是:
这个
<none>:<none>是什么?是不是 Docker 出问题了?是不是垃圾镜像?可以直接删除吗?
别慌,你很有可能是遇到了虚悬镜像。
什么是 Docker 虚悬镜像?
在 Docker 中,一个镜像通常通过 Repository 和 Tag 被引用。
例如 nginx:latest 这个镜像名称指向了一个具体的 Image ID。
其中:
- Repository:镜像名称,例如
nginx - Tag:版本标签,例如
latest - Image ID:镜像真正的唯一标识
正常情况下,一个镜像至少有一个标签指向它。
而虚悬镜像的表现是:
<none>:<none>
这表示:这个镜像对象仍然存在,但是已经没有任何 Repository 和 Tag 指向它。
简单来说:
镜像还存在,但是已经失去了名字,所以 Docker 称它为“悬空”镜像。
什么情况下会产生虚悬镜像?
1. build 重复构建导致旧镜像失去标签
这是最常见的一种情况。
第一次构建:
docker build -t demo:v1 .
此时 demo:v1 指向镜像 A。
之后代码发生变化,再次构建:
docker build -t demo:v1 .
新的镜像 B 构建完成后,demo:v1 这个标签转而被指向镜像 B。
由于 demo:v1 这个标签只能指向一个镜像,所以旧镜像 A 就失去了标签,变成了:
<none>:<none>
如果没有容器使用镜像 A,那么它就成为真正的虚悬镜像。
2. pull 更新 latest 标签
当前系统中有 ubuntu:latest 指向镜像 A。
执行:
docker pull ubuntu:latest
Docker 拉取新的镜像版本 B,ubuntu:latest 转而指向镜像 B。
旧的镜像 A 不再被 ubuntu:latest 指向,于是可能变成:
<none>:<none>
如果没有容器引用它,也会成为虚悬镜像。
3. docker tag 覆盖标签
当前系统中 app:v1 指向镜像 A。
执行:
docker tag 镜像B的ID app:v1
现在 app:v1 指向镜像 B,原来的镜像 A 失去了标签,变为:
<none>:<none>
4. Docker build 构建失败或者中断
例如执行 docker build .,在构建过程中某一步(如 RUN apt install xxx)失败了。
Docker 可能会留下:
- 中间镜像层
- 构建缓存
- 部分未完成的镜像对象
部分情况下可能看到 <none>:<none>。
但是需要注意:现代 Docker 默认使用 BuildKit,很多构建失败产生的是 build cache,而不是传统意义上的虚悬镜像。
构建失败可能产生虚悬镜像,但不是一定产生。
如何判断一个镜像是否是真正的虚悬镜像?
看到 <none>:<none> 不要马上删除。
真正的虚悬镜像需要同时满足两个条件:
条件一:没有任何 tag
即 REPOSITORY 和 TAG 均显示为 <none>。
条件二:没有任何容器引用
检查是否有容器依赖该镜像:
docker ps -a --filter ancestor=<IMAGE_ID>
如果没有输出,说明没有容器依赖它。这时候,它才是可以安全清理的虚悬镜像。
如何清理虚悬镜像?
查看虚悬镜像:
docker images -f dangling=true
或者:
docker image ls --filter dangling=true
清理:
docker image prune
这个命令默认只删除 没有 tag + 没有容器引用 的镜像。
注意:产生 <none>:<none> 的不一定都是虚悬镜像
很多人有一个误区:看到 <none>:<none> 就认为一定是垃圾镜像。
实际上并不是。
情况一:容器仍在运行,但镜像被强制删除导致 <none>:<none>
此时容器仍然在使用某个镜像。但是如果你执行了强制删除:
docker rmi -f <image_id>
Docker 会尝试强行移除该镜像的标签引用。
结果可能变成:
- 镜像本体还在(因为容器仍在使用它)
- 但原来的
Repository和Tag被强制移除 - Docker 无法再用名称引用它,只能显示
<none>:<none>
即使容器还在运行或存在,只要镜像被
-f强制去标签,就可能出现<none>:<none>的情况。
这种情况下的镜像不能随意删除,因为仍然被容器依赖。
情况二:load 导入无 tag 镜像
docker load -i image.tar
如果 tar 文件中没有保存 repository:tag 信息,导入后可能出现 <none>:<none>。
但是它是否属于虚悬镜像,仍然取决于:是否有 tag,以及是否被容器引用。
情况三:多阶段构建(multi-stage build)遗留的中间镜像
这是很多人容易忽略的一种情况。
例如 Dockerfile:
FROM golang:1.20 AS builder
WORKDIR /app
COPY . .
RUN go build -o app
FROM alpine:latest
COPY --from=builder /app/app /app
CMD ["/app"]
正常情况下,你只会得到最终镜像 myapp:latest。
但在构建过程中:
builder阶段会生成中间镜像- 如果没有被最终阶段引用或没有打 tag
- 这些中间镜像就可能变成
<none>:<none>
尤其在以下情况更明显:
- 没有使用
--target指定构建阶段 - 没有开启 BuildKit 优化
- 构建缓存未被清理
这些中间镜像的特点是:可能体积不小、不会被最终容器使用、但仍然占用磁盘空间。
多阶段构建产生的 <none>:<none>,大多数情况下是“可清理的构建残留”,但仍需确认没有被其他构建依赖。
总结:三种情况对比
| 来源 | 是否可安全删除 | 说明 |
|---|---|---|
| 重复 build 覆盖旧标签 | ✅ 是 | 无容器引用时可删 |
| pull 更新 latest 标签 | ✅ 是 | 无容器引用时可删 |
| docker tag 覆盖标签 | ✅ 是 | 无容器引用时可删 |
| docker rmi -f 强制删除 | ❌ 否 | 容器可能还在使用 |
| docker load 无 tag 镜像 | ⚠️ 视情况 | 需确认是否被容器引用 |
| 多阶段构建中间镜像 | ✅ 大部分是 | 建议使用 docker image prune 清理 |
最终总结
Docker 中的 <none>:<none> 只是一个表象。
真正的虚悬镜像,必须满足:
没有任何 tag 引用 + 没有任何容器引用
常见产生原因:
build重复构建覆盖旧标签pull更新latest标签docker tag覆盖标签- 部分构建失败场景
所以:
不要看到 <none>:<none> 就害怕,也不要看到它就直接删除。
判断一个 Docker 镜像是否应该清理,关键不是它有没有名字,而是它背后的引用关系。
理解镜像、标签、容器之间的关系,你就真正掌握了 Docker 镜像生命周期。