Docker 基础体系 · 第 15/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。

Docker Registry 与镜像分发:Tag、Digest、认证、缓存和清理

Docker 镜像分发并不是“把一个 tar 文件上传到服务器,再按名称下载”这么简单。现代 Docker Engine、BuildKit 和 Compose 使用的是一套围绕 OCI/Docker 镜像格式建立的内容寻址分发系统,至少包含以下对象:

  • 镜像引用(image reference):例如 registry.example.com/team/api:1.4.2
  • Tag:例如 1.4.2,本质上是一个可变名称。
  • Digest:例如 sha256:...,标识某个具体内容对象。
  • Manifest:描述配置对象和镜像层。
  • Manifest list / Image index:描述多个平台的 Manifest。
  • Blob:配置 JSON 和压缩镜像层等内容对象。
  • Registry:保存并通过 HTTP API 分发这些对象的服务。
  • 认证与授权:决定客户端能否读取、写入或删除某个仓库。
  • 缓存与清理:决定重复拉取的成本,以及无引用内容何时真正释放。

本文讨论 Linux 容器场景下的 Docker Registry 与镜像分发。Registry 本身只负责存储和传输镜像对象;容器运行时的隔离、命名空间、cgroup、seccomp 和 Linux 内核安全边界,不由 Registry 提供。


一、先建立镜像引用、Tag 和 Digest 的模型

1. 镜像引用由哪些部分组成

常见镜像引用可以写成:

registry.example.com/platform/api:1.4.2

它通常包含:

[registry-host[:port]/][namespace/]repository[:tag]

例如:

docker.io/library/nginx:1.27
registry.example.com/team/api:1.4.2
registry.example.com:5000/team/api:1.4.2

省略 Registry 主机名时,Docker 会按客户端规则解释为 Docker Hub;省略 Tag 时,通常使用 latest。但这两个默认行为都可能掩盖配置错误:

docker pull nginx

通常等价于:

docker pull docker.io/library/nginx:latest

latest 不是“最新版本”的特殊语义,也不保证稳定。它只是一个普通 Tag,维护者可以把它移动到任意新 Manifest。

2. Tag 是可变指针,不是版本内容

可以把 Registry 中的 Tag 抽象为一个映射:

T:tagmanifest digestT : \text{tag} \rightarrow \text{manifest digest}

例如,某一时刻:

api:prod  -> sha256:aaa...

发布新版本后,Tag 可能变成:

api:prod  -> sha256:bbb...

因此,下面两次拉取并不一定得到相同内容:

docker pull registry.example.com/team/api:prod
# 周一执行

docker pull registry.example.com/team/api:prod
# 周五执行

即使 Tag 字符串完全相同,Registry 返回的 Manifest 也可能已经改变。

Tag 适合表达“人类可读的发布通道”:

1.4.2
stable
prod
canary
latest

但 Tag 本身不能作为不可变供应链身份。生产系统如果只记录:

registry.example.com/team/api:prod

就无法仅凭这个字符串证明当时实际运行的是哪份内容。

3. Digest 是内容寻址标识

Digest 通常写作:

sha256:<64 位十六进制字符串>

它是对某个内容对象进行哈希计算后的结果。对于 Registry 分发而言,常见对象包括:

  • 镜像 Manifest;
  • 多架构 Image Index;
  • 配置 JSON;
  • 压缩镜像层 Blob。

镜像引用可以使用 Digest:

docker pull registry.example.com/team/api@sha256:0123456789abcdef...

这里的 Digest 通常指向 Manifest 或 Image Index,而不是某一层。客户端拿到 Manifest 后,再根据其中的配置和层 Digest 拉取其他对象。

内容寻址的核心条件是:

d=H(b)d = H(b)

其中:

  • bb 是对象的字节序列;
  • HH 是哈希函数,现代 Registry 常见为 SHA-256;
  • dd 是对象 Digest。

客户端收到对象后重新计算:

H(breceived)=dexpectedH(b_{\text{received}}) = d_{\text{expected}}

如果两者不相等,就不能把它视为预期对象。这为传输完整性和对象去重提供了基础。

但需要区分两个概念:

  1. Digest 提供内容身份和完整性校验
  2. Digest 不自动证明内容来源可信或内容安全

攻击者如果能合法推送恶意镜像,也可以获得该恶意镜像的 Digest。签名、来源证明、SBOM 和漏洞策略解决的是“是否信任、是否允许使用”,而不是 Digest 本身能否完成的事情。

4. Manifest、Config、Layer 和 Image Index

一个单架构镜像通常至少由以下对象组成:

Manifest
├── Config JSON
└── Layer Blob 1
    Layer Blob 2
    Layer Blob 3

Manifest 中会记录:

  • 配置对象的 media type 和 Digest;
  • 每个镜像层的 media type、Digest、大小;
  • 架构相关的 Manifest 字段。

可以用命令查看:

docker buildx imagetools inspect nginx:1.27

或者查看本地镜像的仓库 Digest:

docker image inspect nginx:1.27 \
  --format '{{json .RepoDigests}}'

需要注意:

docker image inspect nginx:1.27 \
  --format '{{.Id}}'

这里的 .Id 通常是本地镜像配置或本地镜像对象相关的 ID,不应简单当作 Registry 中的 Manifest Digest。要用于部署、审计和跨主机复现,应记录:

registry.example.com/team/api@sha256:...

而不是只记录本地 Image ID。

5. 多架构镜像中的 Digest 关系

一个 Tag 可能指向 Image Index:

api:1.4.2
  -> sha256:index...
       linux/amd64 -> sha256:amd64-manifest...
       linux/arm64 -> sha256:arm64-manifest...

在这种情况下:

docker pull registry.example.com/team/api:1.4.2

客户端会根据当前主机平台选择对应 Manifest。可以用:

docker buildx imagetools inspect \
  registry.example.com/team/api:1.4.2

查看该 Tag 指向的 Index 及其平台成员。

如果使用 Index Digest:

docker pull registry.example.com/team/api@sha256:index...

它固定的是整个多架构发布集合,但客户端仍会根据平台选择其中一个子 Manifest。这样可以同时固定:

  • 发布集合没有改变;
  • 当前平台使用该集合中的指定成员。

如果需要只固定某个平台的 Manifest,则必须记录对应平台 Manifest 的 Digest;不能把 amd64 Manifest Digest 当作 arm64 镜像使用。


二、一次 docker pull 到底发生了什么

Docker Engine 通常不直接把所有 Registry 逻辑放在 CLI 中。典型结构是:

sequenceDiagram
    participant C as docker CLI
    participant E as Docker Engine
    participant R as Registry
    participant T as Token Service

    C->>E: docker pull repo:tag
    E->>R: 请求 Manifest
    R-->>E: 401 + WWW-Authenticate
    E->>T: 请求带 scope 的 Bearer Token
    T-->>E: 返回 Token
    E->>R: 携带 Token 请求 Manifest
    R-->>E: 返回 Manifest 或 Image Index
    E->>R: HEAD/GET Config 与 Layer Blob
    R-->>E: 返回 Blob
    E->>E: 校验 Digest、解压、注册本地镜像
    E-->>C: 显示 Pull complete

1. Tag 解析和 Manifest 获取

执行:

docker pull registry.example.com/team/api:1.4.2

Engine 首先需要确定:

  1. Registry 主机名和端口;
  2. 仓库路径;
  3. Tag;
  4. 当前平台;
  5. Registry API 地址,通常为 /v2/

它随后请求类似:

GET /v2/team/api/manifests/1.4.2
Accept: application/vnd.oci.image.index.v1+json
Accept: application/vnd.oci.image.manifest.v2+json

客户端会通过 Accept 声明支持的 Manifest 类型。现代 Docker 通常支持 OCI Manifest、OCI Image Index 以及 Docker V2 Schema 2 等格式,但具体兼容性仍取决于 Registry、客户端版本和仓库配置。

如果响应是 Image Index,Engine 根据平台选择成员;如果响应是单架构 Manifest,则直接使用它。

2. 根据 Manifest 下载对象

Manifest 返回后,Engine 不会重新下载所有内容。它会先检查本地内容存储中是否已有对应 Digest 的对象。

对每个缺失对象,客户端会执行类似流程:

HEAD /v2/team/api/blobs/sha256:...
GET  /v2/team/api/blobs/sha256:...

下载完成后,Engine 校验对象大小和 Digest。已存在的相同层可以复用,因此两个镜像即使属于不同仓库,也可能共享底层 Blob。

这也是为什么 Docker 镜像的“逻辑大小”和网络传输量不同:

  • 逻辑大小是 Manifest 引用的所有层大小之和;
  • 实际传输量还取决于本地已经缓存了哪些 Blob;
  • Registry 端是否已有这些 Blob,则影响推送时是否可以复用。

3. 拉取完成不等于容器已经安全运行

docker pull 主要完成:

  • 获取 Manifest 或 Image Index;
  • 获取 Config;
  • 获取所需层;
  • 校验并存入本地内容存储;
  • 创建本地镜像引用。

它不会自动完成:

  • 漏洞扫描;
  • 签名验证;
  • SBOM 生成;
  • 运行时权限限制;
  • 容器是否以非 root 用户运行的判断。

这些属于供应链和运行时策略。Digest 是后续做这些策略关联的稳定键,但不是策略本身。


三、一次 docker push 如何产生镜像

一次推送通常遵循“先 Blob,后 Manifest”的顺序:

flowchart LR
    A[本地镜像] --> B[读取 Config 与 Layer Digest]
    B --> C{Registry 是否已有 Blob}
    C -- 是 --> D[跳过上传或挂载 Blob]
    C -- 否 --> E[创建上传会话并上传 Blob]
    D --> F[上传 Manifest]
    E --> F
    F --> G[Tag 指向 Manifest]

1. 推送前的认证与仓库权限

docker login registry.example.com
docker push registry.example.com/team/api:1.4.2

docker login 保存的是认证凭据,具体保存位置取决于 Docker 的 credential helper 配置。生产环境不应把密码直接写在命令行参数中,因为命令行可能被 shell 历史或进程观察机制记录。

更安全的非交互形式是:

printf '%s' "$REGISTRY_PASSWORD" |
  docker login registry.example.com \
    --username "$REGISTRY_USERNAME" \
    --password-stdin

2. Blob 去重和跨仓库挂载

推送时,客户端会根据本地镜像中的 Digest 查询 Registry 是否已有对象。如果已有,Registry 可以让客户端跳过上传,或者在仓库之间执行 Blob mount。

如果不存在,客户端通常创建上传会话:

POST /v2/team/api/blobs/uploads/

随后上传内容,最后提交 Digest:

PUT /v2/team/api/blobs/uploads/<uuid>?digest=sha256:...

实际端点可能包含重定向,客户端需要按 Registry 的响应继续上传。大型镜像常使用分块上传,而不是一次性发送完整 Blob。

Manifest 通常在所有必需 Blob 可用后上传:

PUT /v2/team/api/manifests/1.4.2
Content-Type: application/vnd.oci.image.manifest.v1+json

只有 Manifest 成功提交,Tag 才能成为可拉取的完整引用。因此,“层上传成功但推送失败”可能留下暂时未被引用的 Blob;这些 Blob 后续需要依靠清理策略回收。

3. Tag 更新不是原子发布系统

Tag 的更新可以抽象为:

api:prod -> old manifest

被改写为:

api:prod -> new manifest

如果两个流水线并发执行:

流水线 A:构建 A -> push api:prod
流水线 B:构建 B -> push api:prod

最终 Tag 指向 A 还是 B,取决于最后一次成功提交 Manifest 的时序。Registry 不会替你理解“哪个构建更高级”。

如果发布系统需要可审计,应使用不可变版本 Tag,例如:

api:git-8f31c2a
api:1.4.2

并在部署阶段解析并记录 Digest:

docker buildx imagetools inspect \
  registry.example.com/team/api:1.4.2

更严格的部署输入直接写成:

registry.example.com/team/api@sha256:...

四、Registry 的认证、授权和 TLS

“认证”回答的是“你是谁”;“授权”回答的是“你能对哪个仓库执行什么操作”。

1. Registry v2 常见的 Bearer Token 流程

一个支持 Token 认证的 Registry 可能先返回:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer
  realm="https://auth.example.com/token",
  service="registry.example.com",
  scope="repository:team/api:pull"

客户端随后向 realm 请求 Token。Token 的权限通常带有仓库作用域,例如:

repository:team/api:pull
repository:team/api:pull,push

然后客户端携带:

Authorization: Bearer <token>

重新请求 Registry。

这个流程产生几个重要边界:

  • 登录成功不表示可以访问所有仓库;
  • Token 可能只允许 pull,不允许 push
  • 推送需要对目标仓库拥有 push 权限;
  • 删除通常需要比 push 更高的管理权限;
  • 认证服务、Registry 和反向代理的时间、TLS、Header 配置不一致时,可能出现反复 401。

2. Basic Auth 与自建 Registry

在内部环境中,Distribution Registry 常与 Nginx、Traefik 或其他反向代理结合,使用 Basic Auth。一个最小实验可以使用官方 Registry 镜像:

docker volume create registry-data

docker run -d \
  --name registry \
  -p 5000:5000 \
  -v registry-data:/var/lib/registry \
  registry:2

此时本机可以访问:

curl http://127.0.0.1:5000/v2/

正常情况下会返回:

{}

这只是无认证实验,不应直接暴露到不可信网络。

生产环境至少需要:

  • TLS;
  • 认证;
  • 仓库级授权;
  • 审计日志;
  • 持久化存储;
  • 备份和恢复方案;
  • 反向代理或负载均衡的超时与上传大小配置。

3. TLS 和 insecure registry 的边界

如果 Registry 使用自签名证书,正确做法是将签发该证书的 CA 配置到 Docker Engine 的信任目录,而不是在生产环境长期启用明文或“跳过证书校验”。

对于某个 Registry 主机:

/etc/docker/certs.d/registry.example.com/ca.crt

配置完成后重启 Docker Engine,并验证:

docker login registry.example.com
docker pull registry.example.com/team/api:1.4.2

insecure-registries 会降低传输安全性,尤其在非本机网络中可能使凭据和镜像内容暴露或被篡改。它适合隔离的本地实验网络,不适合作为生产 TLS 配置的替代品。

4. 诊断认证问题

可以按请求阶段区分故障:

curl -i https://registry.example.com/v2/

常见结果:

  • 200:Registry 允许匿名访问 /v2/
  • 401:需要认证,检查 WWW-Authenticate
  • 403:身份可能已识别,但权限不足;
  • 404:可能是仓库不存在,也可能是代理隐藏资源;
  • TLS 握手失败:优先检查证书链、主机名、时间和 CA;
  • blob unknown:Manifest 引用的 Blob 在 Registry 中缺失,可能是存储损坏、错误清理或跨 Registry 迁移不完整。

Docker 客户端还可能缓存旧凭据。可以检查当前登录状态:

docker logout registry.example.com
docker login registry.example.com

如果仅某个仓库失败,应优先检查 Token scope,而不是反复重新登录。


五、缓存、镜像加速和 BuildKit Registry Cache

“缓存”至少有三种不同含义,混淆它们会导致错误配置。

1. Docker Engine 的 Registry Mirror

Docker Engine 可以配置 Registry Mirror,用于加速从指定上游获取内容。典型配置位置为:

{
  "registry-mirrors": [
    "https://mirror.example.com"
  ]
}

修改 /etc/docker/daemon.json 后需要重启 Docker Engine:

sudo systemctl restart docker

这个机制主要用于 Docker Hub 拉取加速,不能简单理解为“所有私有 Registry 的透明代理”。不同 Docker Engine 版本、镜像服务实现和企业网络策略可能存在差异。配置后应明确验证实际请求是否经过 Mirror,而不是只看配置文件。

2. Distribution Registry Pull-Through Cache

可以部署一个带上游代理能力的 Registry,使其在第一次请求时从上游拉取,之后从本地存储响应。概念配置类似:

version: 0.1

storage:
  filesystem:
    rootdirectory: /var/lib/registry

proxy:
  remoteurl: https://registry-1.docker.io

这类缓存的请求路径通常仍然是:

docker pull cache.example.com/library/alpine:3.20

第一次拉取:

客户端 -> cache Registry -> Docker Hub

后续拉取:

客户端 -> cache Registry

Pull-through Cache 需要特别注意:

  • 上游认证是否需要配置;
  • 上游限流是否仍然存在;
  • 缓存磁盘容量;
  • Tag 更新后的可见性;
  • 多架构 Index 和子 Manifest 是否都能正确缓存;
  • 私有仓库是否允许作为上游;
  • 缓存是否成为新的单点故障。

缓存不会改变 Tag 的可变语义。客户端通过 Tag 访问时,仍然存在“这个名称当前映射到哪个 Manifest”的问题。若缓存长期保留旧 Tag 结果,可能出现开发者认为已经发布新版本、节点却仍获得旧内容的现象。因此部署时依旧应记录或直接使用 Digest。

3. BuildKit 的 Registry 构建缓存

BuildKit 的构建缓存不是普通运行时镜像。可以将缓存导出到 Registry:

docker buildx build \
  --platform linux/amd64 \
  -t registry.example.com/team/api:1.4.2 \
  --push \
  --cache-to type=registry,ref=registry.example.com/team/api:buildcache,mode=max \
  --cache-from type=registry,ref=registry.example.com/team/api:buildcache \
  .

这里有两个不同引用:

team/api:1.4.2       # 运行时镜像
team/api:buildcache  # BuildKit 缓存对象

BuildKit 缓存可能包含更多中间阶段,mode=max 通常比默认模式保留更多缓存信息,但会增加 Registry 存储占用。它不能替代最终镜像的 Tag、Digest、签名或 SBOM。

缓存失效由输入变化决定,例如:

  • Dockerfile 指令改变;
  • 相关构建上下文改变;
  • ARG 或环境输入改变;
  • 基础镜像引用解析到不同内容;
  • BuildKit 的缓存元数据不再匹配。

因此,构建缓存是性能优化,不是供应链身份。构建系统仍应固定基础镜像 Digest 或在构建记录中保存解析结果。


六、Compose 中如何使用 Tag 和 Digest

Compose 文件可以写 Tag:

services:
  api:
    image: registry.example.com/team/api:1.4.2

也可以写 Digest:

services:
  api:
    image: registry.example.com/team/api@sha256:0123456789abcdef...

Digest 引用没有 Tag,因此人类可读版本信息通常需要通过额外元数据表达,例如 Compose 项目变量、镜像 Label 或发布清单。

一个常见的发布方式是:

services:
  api:
    image: registry.example.com/team/api@sha256:${API_DIGEST}

启动前:

export API_DIGEST=0123456789abcdef...
docker compose config
docker compose pull
docker compose up -d

docker compose config 用于检查变量替换后的最终配置。若 Digest 缺失或格式错误,应在部署前失败,而不是让运行时临时解析一个 Tag。

需要区分两个动作:

docker compose pull
docker compose up -d

pull 获取并校验镜像;up 根据 Compose 配置创建或更新容器。仅使用 up 是否重新拉取、何时重建,取决于 Compose 实现和已有容器状态,不能把它当作严格的供应链锁定机制。生产部署通常应先显式拉取并验证,再启动。


七、删除 Tag、删除 Manifest 和垃圾回收

Registry 的“删除”经常被误解为删除一个文件。实际上至少存在三层含义:

  1. 删除 Tag 引用;
  2. 删除 Manifest;
  3. 删除没有任何引用的 Blob。

1. Tag 不是独立镜像副本

如果两个 Tag 指向同一个 Manifest:

api:1.4.2 -> sha256:m
api:stable -> sha256:m

删除其中一个 Tag,不应删除 Manifest 或层,因为另一个 Tag 仍在引用它。

如果只删除 Tag,底层 Manifest 和 Blob 是否立即消失,也取决于 Registry 实现。对于 Distribution Registry,常见清理流程是删除 Manifest 后,再运行垃圾回收。

2. Distribution Registry 的按 Digest 删除

启用删除能力的配置通常包含:

storage:
  delete:
    enabled: true

删除时应使用 Manifest Digest,而不是 Tag:

curl -i -X DELETE \
  "https://registry.example.com/v2/team/api/manifests/sha256:0123456789abcdef..."

Digest 必须是 Registry 中 Manifest 的 Digest。为了得到正确 Digest,可以查询响应 Header:

curl -sSI \
  -H 'Accept: application/vnd.oci.image.manifest.v1+json' \
  "https://registry.example.com/v2/team/api/manifests/1.4.2"

常见响应包含:

Docker-Content-Digest: sha256:...

如果目标是多架构 Tag,则应先以支持 Index 的 Accept 头查询,否则可能得到错误的对象类型或错误的 Digest:

curl -sSI \
  -H 'Accept: application/vnd.oci.image.index.v1+json' \
  -H 'Accept: application/vnd.docker.distribution.manifest.list.v2+json' \
  "https://registry.example.com/v2/team/api/manifests/1.4.2"

不要根据本地镜像 ID 猜测删除 Digest,也不要直接删除 Registry 存储目录中的文件。后者可能破坏 Manifest 与 Blob 的引用关系。

3. 垃圾回收的标记—清扫过程

Registry 垃圾回收可以抽象为:

  1. 从仍然可达的 Tag 或 Manifest 开始;
  2. 标记这些 Manifest 引用的 Config 和 Layer;
  3. 对仍被这些对象引用的其他对象继续标记;
  4. 删除未被标记的 Blob。

形式化地说,设全部对象集合为 OO,从保留引用出发可达的对象集合为 RR,则可回收对象为:

G=ORG = O - R

这个模型解释了三个现象:

  • 删除 Tag 后,若 Manifest 仍被其他 Tag 引用,相关层不能回收;
  • 多个镜像共享一个 Layer 时,删除一个镜像不会立即释放该 Layer;
  • 推送中断产生的孤儿 Blob 只有在确认不可达后才能回收。

Distribution Registry 常见的垃圾回收方式类似:

docker exec registry \
  registry garbage-collect /etc/docker/registry/config.yml

生产环境不应盲目对正在接收 push 的 Registry 执行清理。并发推送可能正处于“Blob 已上传、Manifest 尚未提交”的中间状态。如果 GC 在错误时机把它判断为不可达,可能造成推送失败或内容缺失。

较稳妥的操作条件包括:

  • 停止写入,或使用 Registry 提供的只读/维护模式;
  • 备份存储;
  • 先执行 dry-run 或列出待删除对象;
  • 确认保留策略覆盖正式 Tag、Digest 锁定版本、回滚版本和 BuildKit 缓存;
  • 清理后执行实际 pull 验证。

不同 Registry 产品的 GC 机制并不完全相同。云 Registry 通常提供自己的生命周期规则、软删除和异步清理接口,不能直接套用 Distribution Registry 的命令。

4. 清理策略必须保留回滚依据

例如,下面的策略并不等价:

保留最近 10 个 Tag

和:

保留最近 10 个发布版本对应的 Manifest Digest

原因是 Tag 可能被重写。如果 prod 被移动到新 Digest,旧版本可能没有任何 Tag,但仍然是回滚所需内容。

一个更可审计的发布记录应保存:

发布版本:1.4.2
镜像引用:registry.example.com/team/api@sha256:...
构建提交:8f31c2a
构建时间:...
平台:linux/amd64, linux/arm64
SBOM/签名关联:...

清理系统按照这些稳定身份保留对象,而不是仅按当前 Tag 列表判断。


八、故障路径与诊断方法

1. manifest unknown

典型错误:

manifest for registry.example.com/team/api:1.4.2 not found

可能原因:

  • Tag 拼写错误;
  • 推送只上传了 Blob,没有成功提交 Manifest;
  • Registry 地址或命名空间错误;
  • 客户端请求的平台没有对应的多架构成员;
  • 认证服务故意隐藏不存在仓库。

诊断顺序:

docker manifest inspect \
  registry.example.com/team/api:1.4.2

或者:

docker buildx imagetools inspect \
  registry.example.com/team/api:1.4.2

如果能看到 Index,但当前平台不在其中,问题不是 Tag 不存在,而是平台发布不完整。

2. unauthorizeddenied

unauthorized: authentication required

通常意味着没有有效凭据,或者 Token 没有对应 scope。

denied: requested access to the resource is denied

通常意味着已经进入授权判断,但没有 pull/push 权限,或者仓库路径不属于当前用户。

可以分别验证:

docker logout registry.example.com
docker login registry.example.com
docker pull registry.example.com/team/api:1.4.2

推送失败时还应确认本地 Tag 是否确实指向目标名称:

docker image tag local-api:latest \
  registry.example.com/team/api:1.4.2

docker push registry.example.com/team/api:1.4.2

Docker 不会因为本地镜像名是 local-api 就自动推送到目标 Registry。

3. blob unknown

Manifest 存在但层缺失时,客户端可能报:

blob unknown to registry

常见原因包括:

  • Registry 存储被手工删除;
  • GC 与推送并发;
  • 后端对象存储最终一致性或权限异常;
  • 迁移时只复制了 Manifest,没有复制 Blob;
  • Registry 返回了损坏或不完整的缓存结果。

此时不要只重新登录。应检查:

  • Manifest 中列出的所有 Digest;
  • Registry 后端存储;
  • 反向代理日志;
  • GC 操作时间;
  • 对象存储权限和可见性。

4. 拉取了旧镜像

如果使用的是 Tag,旧内容可能来自:

  • Tag 尚未更新;
  • Registry Mirror 或 Pull-through Cache 仍提供旧响应;
  • 节点本地已有相同 Tag 的旧镜像,Compose 没有触发拉取;
  • 发布流水线更新 Tag 的顺序不正确;
  • 不同 Registry 主机使用了不同命名空间。

最可靠的验证方法是直接比较 Digest:

docker image inspect \
  registry.example.com/team/api:prod \
  --format '{{json .RepoDigests}}'

并在 Registry 侧查询 Tag 对应的 Manifest Digest。部署系统应以 Digest 而不是 Tag 判断版本是否改变。


九、Tag、Digest 与供应链安全的组合方式

1. Tag 提供可读性,Digest 提供稳定身份

常见的发布流程是:

构建产物
  -> 推送不可变版本 Tag
  -> 获取 Manifest Digest
  -> 生成部署清单
  -> 生产使用 Digest

例如:

开发环境:
registry.example.com/team/api:1.4.2

部署清单:
registry.example.com/team/api@sha256:...

Tag 仍然用于搜索、沟通和发布页面;Digest 用于部署、审计和回滚。

2. Digest 不替代签名和策略

以下命题不能混为一谈:

Digest 相同

只能说明引用的是同一个对象内容。它不能证明:

  • 该对象由哪个 CI 任务构建;
  • 构建是否来自受信任提交;
  • 基础镜像是否符合组织要求;
  • 镜像是否含有高危漏洞;
  • SBOM 是否完整;
  • 发布者身份是否可信。

供应链系统通常会把签名、证明、SBOM 和扫描结果关联到 Manifest Digest。多架构镜像还要确认关联对象是:

  • Index Digest;
  • 子 Manifest Digest;
  • 或两者都具备。

如果策略只验证 amd64 子 Manifest,而部署时使用的是 Index Digest,则必须明确验证工具如何解析和匹配这两层身份。

3. 基础镜像 Tag 的隐含变化

Dockerfile:

FROM alpine:3.20

每次构建时,alpine:3.20 可能解析到不同 Digest。若要使构建输入更明确,可以写成:

FROM alpine:3.20@sha256:...

这会同时保留人类可读的 Tag 和不可变 Digest。Digest 应由实际 Registry 查询得到,不能手工编造。

但固定基础镜像并不意味着永久安全。漏洞修复后的新基础镜像会产生新 Digest。组织需要在“可复现”和“持续更新”之间建立明确流程,例如定期更新锁定 Digest,并重新生成 SBOM、扫描和签名。


十、一个完整的本地流程示例

下面示例假设:

  • 使用 Linux;
  • Docker Engine 已运行;
  • 本地存在 Dockerfile
  • Registry 运行在 127.0.0.1:5000
  • 该 Registry 仅用于隔离实验。

1. 构建并推送 Tag

docker build \
  -t 127.0.0.1:5000/demo/api:1.0.0 \
  .

docker push 127.0.0.1:5000/demo/api:1.0.0

构建会产生本地镜像;推送时 Docker 将缺失 Blob 上传,并提交 Tag 对应的 Manifest。

2. 查看 Registry 中的对象

curl http://127.0.0.1:5000/v2/_catalog

可能得到:

{"repositories":["demo/api"]}

查询 Tag:

curl http://127.0.0.1:5000/v2/demo/api/tags/list

可能得到:

{"name":"demo/api","tags":["1.0.0"]}

3. 获取 Tag 对应的 Manifest Digest

curl -sSI \
  -H 'Accept: application/vnd.oci.image.manifest.v1+json' \
  http://127.0.0.1:5000/v2/demo/api/manifests/1.0.0

从响应中读取:

Docker-Content-Digest: sha256:...

随后可以使用 Digest 拉取:

docker pull \
  127.0.0.1:5000/demo/api@sha256:...

Digest 只能替换为真实查询结果;示例中的省略号不是可执行值。

4. 验证 Tag 被移动后的行为

重新构建一个不同内容的镜像并覆盖同一 Tag:

docker build \
  -t 127.0.0.1:5000/demo/api:1.0.0 \
  .

docker push 127.0.0.1:5000/demo/api:1.0.0

再次查询 Manifest Digest。若 Digest 改变,说明:

同一个 Tag -> 不同 Manifest

而先前使用旧 Digest 的客户端仍然可以继续引用旧对象,只要旧 Manifest 和 Blob 尚未被删除并完成 GC。

这正是 Tag 与 Digest 行为差异的可验证算例。


十一、生产环境中的取舍

1. 什么时候使用 Tag

Tag 适合:

  • 开发人员手工拉取;
  • CI 中表示构建通道;
  • 发布系统展示版本;
  • Canary、Stable 等可移动环境指针。

Tag 不适合作为唯一的生产部署身份,尤其是:

latest
prod
stable

这些名称表达的是通道,不是内容版本。

2. 什么时候使用 Digest

Digest 适合:

  • 生产部署;
  • 回滚;
  • 审计;
  • 变更比较;
  • 供应链证明关联;
  • 节点间一致性验证。

使用 Digest 会增加更新管理成本:基础镜像和应用镜像更新后,需要重新生成部署清单。但这个成本正是显式控制变更的代价。

3. 何时部署缓存

缓存适合:

  • 多节点频繁拉取相同镜像;
  • 跨地域网络受限;
  • 上游 Registry 有限流;
  • 构建系统需要共享 BuildKit 缓存。

缓存不能替代:

  • Registry 高可用;
  • 镜像签名;
  • Digest 锁定;
  • 访问控制;
  • 离线备份。

应分别监控:

  • 运行时镜像缓存命中;
  • BuildKit 缓存命中;
  • 上游请求错误;
  • 本地存储增长;
  • 缓存 Tag 的新鲜度;
  • Registry 后端对象存储错误。

4. 何时执行清理

清理的前提不是“某个 Tag 看起来没用了”,而是确认对象图中不存在仍需保留的引用。正式版本、回滚版本、SBOM、签名、构建缓存和多架构成员都可能影响可达性判断。

清理前至少应回答:

哪些 Digest 仍被部署?
哪些 Digest 需要回滚?
哪些对象属于签名或 SBOM 关联?
哪些 Tag 会被重新发布?
哪些构建缓存仍有价值?
GC 是否会与 push 并发?
删除后如何恢复?

当 Registry 使用 OCI referrers 保存签名、SBOM 或证明对象时,清理工具还必须正确处理这些关联关系。不同 Registry 对 referrers、索引和 GC 的支持并不完全一致,不能只根据“没有 Tag”就认定整个关联对象树都可删除。


十二、需要牢牢记住的边界

  • Tag 是名称到 Manifest 的可变映射,不是不可变版本。
  • Digest 是对象内容的稳定身份,通常固定 Manifest 或 Image Index。
  • Index Digest 固定多架构发布集合,平台选择仍在拉取时发生。
  • Push 先上传 Blob、后提交 Manifest,中途失败可能留下不可达对象。
  • 认证不等于授权,登录成功不代表能 push、delete 或访问所有仓库。
  • TLS 保护传输和凭据,不能替代签名、扫描和来源证明。
  • Registry Mirror、Pull-through Cache 和 BuildKit Registry Cache 是不同机制
  • 删除 Tag 不等于立即删除层,真正释放空间通常需要基于可达性执行 GC。
  • Digest 不等于信任,供应链策略仍需结合签名、SBOM、扫描结果和构建证明。
  • Registry 只分发 Linux 容器镜像对象,不负责容器运行时的 Linux 隔离和权限安全。

当构建系统以 Tag 产生易读发布名,以 Digest 生成部署清单,再配合明确的认证授权、可验证缓存、可审计保留策略和受控垃圾回收时,镜像分发才从“能拉到镜像”提升为可复现、可追踪、可恢复的工程系统。


系列导航与关联阅读

官方资料

本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。