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 抽象为一个映射:
例如,某一时刻:
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 拉取其他对象。
内容寻址的核心条件是:
其中:
- 是对象的字节序列;
- 是哈希函数,现代 Registry 常见为 SHA-256;
- 是对象 Digest。
客户端收到对象后重新计算:
如果两者不相等,就不能把它视为预期对象。这为传输完整性和对象去重提供了基础。
但需要区分两个概念:
- Digest 提供内容身份和完整性校验;
- 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 首先需要确定:
- Registry 主机名和端口;
- 仓库路径;
- Tag;
- 当前平台;
- 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 的“删除”经常被误解为删除一个文件。实际上至少存在三层含义:
- 删除 Tag 引用;
- 删除 Manifest;
- 删除没有任何引用的 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 垃圾回收可以抽象为:
- 从仍然可达的 Tag 或 Manifest 开始;
- 标记这些 Manifest 引用的 Config 和 Layer;
- 对仍被这些对象引用的其他对象继续标记;
- 删除未被标记的 Blob。
形式化地说,设全部对象集合为 ,从保留引用出发可达的对象集合为 ,则可回收对象为:
这个模型解释了三个现象:
- 删除 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. unauthorized 与 denied
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 完整学习路线:从镜像与容器到安全、可观测和生产交付
- 上一篇:Docker 软件供应链:SBOM、签名、扫描、可信基础镜像和策略门禁
- 下一篇:Docker 日志与监控:Logging Driver、Metrics、事件和容量告警
- 延伸:Docker 多架构构建:Buildx、QEMU、Manifest 与跨平台发布
官方资料
本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。

评论
0 条讨论