遇见 <none>:<none> 不要慌:今天带你彻底理解 Docker 虚悬镜像

0 阅读6分钟

你是否遇到过这个奇怪的镜像?

在学习 Docker 的过程中,很多人都会遇到这样的情况。

执行:

docker images

突然发现:

REPOSITORY    TAG        IMAGE ID
<none>        <none>     a1b2c3d4
nginx         latest     e5f6g7h8

看到这里,很多人的第一反应是:

这个 <none>:<none> 是什么?是不是 Docker 出问题了?是不是垃圾镜像?可以直接删除吗?

别慌,你很有可能是遇到了虚悬镜像。


什么是 Docker 虚悬镜像?

在 Docker 中,一个镜像通常通过 RepositoryTag 被引用。

例如 nginx:latest 这个镜像名称指向了一个具体的 Image ID

其中:

  • Repository:镜像名称,例如 nginx
  • Tag:版本标签,例如 latest
  • Image ID:镜像真正的唯一标识

正常情况下,一个镜像至少有一个标签指向它。

而虚悬镜像的表现是:

<none>:<none>

这表示:这个镜像对象仍然存在,但是已经没有任何 RepositoryTag 指向它。

简单来说:

镜像还存在,但是已经失去了名字,所以 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

REPOSITORYTAG 均显示为 <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 会尝试强行移除该镜像的标签引用。

结果可能变成:

  • 镜像本体还在(因为容器仍在使用它)
  • 但原来的 RepositoryTag 被强制移除
  • 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 引用 + 没有任何容器引用

常见产生原因:

  1. build 重复构建覆盖旧标签
  2. pull 更新 latest 标签
  3. docker tag 覆盖标签
  4. 部分构建失败场景

所以:

不要看到 <none>:<none> 就害怕,也不要看到它就直接删除。

判断一个 Docker 镜像是否应该清理,关键不是它有没有名字,而是它背后的引用关系

理解镜像、标签、容器之间的关系,你就真正掌握了 Docker 镜像生命周期。