Docker 基础体系 · 第 59/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。
Docker 不可变制品晋级:Digest、环境配置、验收、灰度和回滚
在 Docker 交付流程中,“把镜像从测试环境推到生产环境”通常不是重新构建一次,也不是把某个 Tag 改成 latest。更可靠的模型是:
- 构建一个镜像制品;
- 将制品推送到 Registry;
- 记录它的内容摘要 Digest;
- 为每个环境单独注入配置;
- 在目标环境验收同一个 Digest;
- 通过灰度逐步增加流量;
- 出现问题时恢复到此前已验收的 Digest。
这里的关键是:制品身份、环境配置、运行状态和流量状态必须分开管理。只要其中一个概念被 Tag、容器名称或“当前正在运行的容器”替代,晋级和回滚就容易失去确定性。
本文以现代 Docker Engine、BuildKit 和 Compose 规范为基础,讨论 Linux 容器场景。Compose 可以编排同一台主机上的服务,但它本身不是完整的多主机发布平台,也不会自动提供生产级负载均衡、跨节点灰度或数据库回滚能力。
一、先定义“不可变制品晋级”
1. 制品不是 Tag,而是可定位的镜像内容
Docker Registry 中常见的镜像引用是:
registry.example.com/team/orders:2025.03.08
其中:
registry.example.com是 Registry 地址;team/orders是仓库名;2025.03.08是 Tag。
Tag 是一个可变的命名指针。Registry 可以在之后将同一个 Tag 指向另一份镜像清单。因此,下面两个时间点拉取的镜像可能不同:
docker pull registry.example.com/team/orders:2025.03.08
# 第一次拉取
# Registry 管理员重新推送同名 Tag 后
docker pull registry.example.com/team/orders:2025.03.08
# 第二次拉取可能得到不同内容
Digest 是对镜像清单内容计算出的内容寻址标识,通常形如:
sha256:4f3c...9a1b
使用 Digest 的完整引用是:
registry.example.com/team/orders@sha256:4f3c...9a1b
@ 后面的 Digest 不再是一个任意命名,而是对特定镜像清单内容的引用。只要 Registry 遵守内容寻址和分发协议,引用同一个 Digest 就是在请求同一份清单内容。
因此:
registry.example.com/team/orders:2025.03.08
表示“名为 2025.03.08 的可变入口”,而:
registry.example.com/team/orders@sha256:...
表示“由该摘要标识的具体内容”。
2. 镜像 Digest、配置 Digest 和层 Digest 不是一回事
Docker 镜像不是一个单独的 tar 文件。对于常见 OCI/Docker 镜像,可以抽象为:
镜像清单 Manifest
├── 配置对象 Config
└── 文件系统层 Layers
这些对象各自有 Digest:
- Manifest Digest:描述镜像配置对象和层对象的清单,通常用于
repo@sha256:...; - Config Digest:描述容器启动时使用的镜像配置,例如 Entrypoint、Cmd、Env、WorkingDir、用户等;
- Layer Digest:描述各个只读文件系统层。
在单架构镜像中,用户通常关心的是 Manifest Digest。多架构镜像还可能有一个 Manifest List 或 OCI Image Index:
Image Index Digest
├── linux/amd64 Manifest Digest
└── linux/arm64 Manifest Digest
因此,下面这个 Digest 可能是多架构索引的 Digest:
registry.example.com/team/orders@sha256:index-digest
Docker Engine 会根据主机平台选择 linux/amd64 或 linux/arm64 对应的子清单。若需要固定到具体平台,也应明确平台:
docker pull --platform=linux/amd64 \
registry.example.com/team/orders@sha256:index-digest
这并不意味着 Digest 失效,而是说明“制品身份”可能有两个层次:
跨平台制品身份 = Image Index Digest
具体平台制品身份 = Platform Manifest Digest
生产环境若混用 amd64 和 arm64,通常记录索引 Digest;若只允许一种架构,则可以进一步记录和验证平台清单 Digest。
3. 不可变制品的形式化表示
将一个待晋级制品表示为:
A = (repository, manifestDigest, platform)
例如:
A =
(
registry.example.com/team/orders,
sha256:4f3c...9a1b,
linux/amd64
)
环境配置不应直接塞进镜像制品身份,而应单独表示:
C_e = 某环境的非敏感配置
S_e = 某环境的敏感配置
e = 环境,例如 staging 或 production
一次部署可以写成:
Deployment(e) = Run(A, C_e, S_e, RuntimePolicy_e)
其中:
A决定运行什么软件;C_e决定该环境如何运行;S_e决定该环境使用哪些凭据;RuntimePolicy_e决定资源限制、网络、权限、健康检查和重启策略。
“不变”只约束 A,并不意味着容器运行时的所有内容都永远不变。环境变量、Secret、挂载卷、网络连接和容器运行状态仍然可能变化。
二、从构建到 Digest:制品如何产生
1. BuildKit 构建输出与 Registry Digest
常见的 BuildKit 构建流程是:
docker buildx build \
--platform=linux/amd64 \
--file Dockerfile \
--tag registry.example.com/team/orders:git-7f3c2a1 \
--push \
--metadata-file build-metadata.json \
.
这里的几个参数分别表示:
--platform=linux/amd64:构建目标平台;--tag:给结果绑定一个便于查找的 Tag;--push:直接将结果推送到 Registry;--metadata-file:保存 BuildKit 输出的元数据;.:构建上下文。
现代 Buildx 通常会在元数据文件中写入类似字段:
{
"containerimage.digest": "sha256:4f3c...9a1b"
}
可以用 jq 读取:
DIGEST="$(jq -r '."containerimage.digest"' build-metadata.json)"
test "$DIGEST" != "null" && test -n "$DIGEST" || {
echo "没有获得镜像 Digest" >&2
exit 1
}
echo "$DIGEST"
实际流水线还应从 Registry 侧再次确认该 Digest。例如:
docker buildx imagetools inspect \
registry.example.com/team/orders:git-7f3c2a1
该命令用于检查 Registry 中的镜像清单及其平台信息。不同 Docker Buildx 版本的输出格式可能不同,因此自动化脚本应尽量使用稳定的机器可读输出或 Registry API,而不是解析人类可读文本。
2. 为什么不能把本地 Image ID 当成 Registry Digest
执行:
docker image ls
看到的 IMAGE ID 常常来自镜像 Config 对象的标识,通常不是 Registry 中用于分发的 Manifest Digest。
可以分别观察:
docker image inspect registry.example.com/team/orders:git-7f3c2a1
以及:
docker image inspect \
--format '{{json .RepoDigests}}' \
registry.example.com/team/orders:git-7f3c2a1
.RepoDigests 可能输出:
[
"registry.example.com/team/orders@sha256:4f3c...9a1b"
]
前者包含本地镜像配置和层信息,后者表示该镜像被哪些仓库 Digest 引用。一个镜像可能有多个 Tag,但它们都指向同一个 Manifest Digest;反过来,同一个 Tag 也可能在不同时间指向不同 Digest。
3. 构建过程本身仍可能不具备完全可复现性
固定 Digest 只能保证“之后使用的是已经生成的那份内容”,不能自动保证这份内容当初一定可复现。
例如 Dockerfile:
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y curl
即使 Dockerfile 没变,以下因素也可能变化:
ubuntu:24.04Tag 后续指向了不同基础镜像;- APT 仓库中的软件包版本变化;
- 构建上下文中的未提交文件变化;
- 使用了当前时间、随机数或网络下载内容;
- 构建参数或秘密输入变化。
更严格的构建会固定基础镜像引用:
FROM ubuntu@sha256:<base-image-digest>
并固定软件包版本、锁文件和构建上下文。这里要区分两个目标:
构建可复现:相同输入尽量产生相同结果
制品不可变:已经生成的结果可由 Digest 精确引用
前者降低“为什么重新构建结果不同”的风险,后者保证“晋级和回滚使用的不是另一份结果”。
三、环境配置:不重新构建镜像,也不把环境写进镜像
1. 同一制品应通过配置适配不同环境
假设同一个镜像 Digest 要部署到测试和生产:
A = registry.example.com/team/orders@sha256:4f3c...9a1b
测试环境可能使用:
DATABASE_HOST=postgres.staging.internal
LOG_LEVEL=debug
生产环境可能使用:
DATABASE_HOST=postgres.production.internal
LOG_LEVEL=info
正确的区别是:
staging = Run(A, C_staging, S_staging, Policy_staging)
production = Run(A, C_production, S_production, Policy_production)
而不是:
为 staging 构建一份镜像
为 production 再构建一份镜像
后者会让“环境差异”和“代码差异”混在一起,验收结果不能直接推导到生产。
2. Compose 中的变量插值和容器环境不是同一件事
一个适合晋级的 Compose 文件可以写成:
services:
orders:
image: "${APP_IMAGE:?APP_IMAGE must be set}"
environment:
APP_ENV: "${APP_ENV:?APP_ENV must be set}"
DATABASE_HOST: "${DATABASE_HOST:?DATABASE_HOST must be set}"
LOG_LEVEL: "${LOG_LEVEL:-info}"
ports:
- "8080:8080"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/healthz"]
interval: 10s
timeout: 3s
retries: 6
start_period: 20s
对应的生产配置文件:
APP_IMAGE=registry.example.com/team/orders@sha256:4f3c...9a1b
APP_ENV=production
DATABASE_HOST=postgres.production.internal
LOG_LEVEL=info
启动前检查最终配置:
docker compose --env-file production.env config
docker compose config 会进行变量插值并输出解析后的 Compose 配置。它的价值在于:在真正创建容器前,先确认 image 是否是期望的 Digest、环境变量是否完整、端口和挂载是否正确。
启动:
docker compose --env-file production.env up -d
这里的 production.env 主要用于 Compose 文件中的变量插值。另一个常见写法是:
services:
orders:
env_file:
- production-runtime.env
env_file 的含义是将变量注入容器运行环境;它与 Compose CLI 用于插值的 .env 或 --env-file 并不完全等价。不要因为一个变量出现在 env_file 中,就假定它一定能替换 Compose 文件里的 ${VARIABLE}。
3. 配置校验必须在创建容器前完成
例如,部署脚本可以先检查:
set -eu
COMPOSE_FILE=compose.yml
ENV_FILE=production.env
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" config >/tmp/orders.compose.rendered.yml
grep -F 'image: registry.example.com/team/orders@sha256:4f3c' \
/tmp/orders.compose.rendered.yml >/dev/null || {
echo "Compose 最终配置不是预期 Digest" >&2
exit 1
}
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" config --quiet
config --quiet 主要验证配置是否能被解析;它不能验证数据库可用、应用接口正确或 Secret 内容有效。因此还需要后续运行时验收。
4. Secret 不应通过普通环境变量和镜像层传播
普通环境变量可能被以下位置看到:
docker inspect;- 进程环境;
- 崩溃转储;
- 日志或诊断输出;
- CI 日志。
密码、令牌和私钥应使用平台提供的 Secret 机制或受控文件挂载。Compose 的 Secret 能力适合在单机或明确的编排边界内使用,但不同 Compose 实现对 Secret 后端的支持和行为可能不同,不能把 Compose 文件中的 secrets 自动等同于生产密钥管理系统。
还应避免:
ARG NPM_TOKEN
RUN npm config set //registry.example.com/:_authToken=$NPM_TOKEN
即使之后删除配置文件,秘密也可能已经进入构建层或构建缓存。BuildKit 的 secret mount 可以减少这类泄露,例如:
docker buildx build \
--secret id=npmrc,src="$HOME/.npmrc" \
--tag registry.example.com/team/orders:git-7f3c2a1 \
--push .
Dockerfile 中:
# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm ci
这要求构建环境支持 BuildKit,并且应用依赖管理工具不会将凭据写入最终产物。
四、从构建到生产的晋级状态机
一个可审计的制品晋级流程可以表示为:
stateDiagram-v2
[*] --> Built
Built --> Pushed: 推送 Registry
Pushed --> Identified: 记录 Manifest Digest
Identified --> StagingRunning: 使用 Digest 部署
StagingRunning --> Accepted: 自动与人工验收通过
StagingRunning --> Rejected: 验收失败
Accepted --> ProductionCanary: 同一 Digest 灰度
ProductionCanary --> ProductionFull: 灰度指标通过
ProductionCanary --> RolledBack: 指标失败
ProductionFull --> RolledBack: 线上故障
RolledBack --> ProductionFull: 恢复旧 Digest
这里最重要的状态约束是:
ProductionCanary 的 Digest = Accepted 的 Digest
ProductionFull 的 Digest = ProductionCanary 的 Digest
不能在“晋级”步骤中重新执行:
docker build .
也不能根据 Tag 重新解析:
docker pull registry.example.com/team/orders:release
因为 Tag 解析是一个外部状态读取动作。正确做法是在流水线中保存明确的引用:
IMAGE_REF=registry.example.com/team/orders@sha256:4f3c...9a1b
并将该引用传给测试、灰度和生产部署。
推荐的数据流
flowchart LR
C[源代码与锁文件] --> B[BuildKit 构建]
B --> R[Registry]
R --> D[Manifest Digest]
D --> M[制品记录]
M --> S[staging: Digest + 环境配置]
S --> A[功能/协议/安全验收]
A --> G[生产灰度]
G --> P[生产全量]
P --> H[运行监控]
H -->|异常| RB[旧 Digest 回滚]
制品记录至少应包含:
{
"repository": "registry.example.com/team/orders",
"digest": "sha256:4f3c...9a1b",
"platform": "linux/amd64",
"sourceRevision": "7f3c2a1",
"buildTimestamp": "2025-03-08T12:00:00Z",
"configRevision": "prod-config-2025-03-08.2"
}
sourceRevision 和 configRevision 是审计信息,不应被误认为镜像身份。真正用于拉取制品的仍然是 repository + digest。
五、验收:证明“同一制品在此环境可运行”
1. 验收的对象不是只有镜像
一次部署验收应验证:
(A, C_e, S_e, RuntimePolicy_e)
而不是仅执行:
docker pull ...
docker run ...
因为镜像可以正确,但仍可能出现:
- 数据库地址错误;
- Secret 无效;
- 端口未监听;
- 健康检查过于宽松;
- 新版本协议与旧数据库不兼容;
- 容器能启动但请求全部失败。
2. 先验证实际运行容器的镜像身份
部署后检查:
docker compose --env-file production.env ps
docker compose --env-file production.env images
也可以直接查看容器:
CID="$(docker compose --env-file production.env ps -q orders)"
docker inspect "$CID" \
--format 'container={{.Name}} image_id={{.Image}}'
这里的 .Image 通常是本地镜像的 Config ID,不一定直接显示 Manifest Digest。为了核验 Registry Digest,应结合镜像的 RepoDigests:
IMAGE_ID="$(docker inspect "$CID" --format '{{.Image}}')"
docker image inspect "$IMAGE_ID" \
--format '{{json .RepoDigests}}'
更严格的验收应在部署前后同时记录:
docker compose --env-file production.env config > rendered.yml
docker compose --env-file production.env ps
docker image inspect registry.example.com/team/orders@sha256:4f3c...9a1b
如果平台、镜像仓库或 Docker Engine 对多架构清单做了平台解析,则还应核实主机平台与所选清单:
docker version
docker info
uname -m
3. 健康检查不是完整验收
Compose 的 healthcheck 只回答一个有限问题:
容器内的检查命令当前是否成功?
例如:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/healthz"]
它不能单独证明:
- 外部客户端能通过反向代理访问;
- 数据库写入正常;
- 消息队列消费正常;
- 权限和租户隔离正确;
- 新旧版本之间的 API 兼容;
- 延迟和错误率满足要求。
因此验收一般分层进行。
第一层:容器和进程
docker compose --env-file staging.env up -d
docker compose --env-file staging.env ps
docker compose --env-file staging.env logs --tail=100 orders
预期是服务处于运行状态,日志没有启动失败、配置解析失败或反复重启。
第二层:应用存活和就绪
curl --fail --silent --show-error \
http://127.0.0.1:8080/healthz
healthz 通常只表示进程存活;如果应用区分存活和就绪,应再检查:
curl --fail --silent --show-error \
http://127.0.0.1:8080/readyz
readyz 应包含真正接收业务流量所需的依赖检查,但不应因为一个可选依赖短暂不可用就让整个服务无限重启。
第三层:业务冒烟
curl --fail --silent --show-error \
-H 'Content-Type: application/json' \
-d '{"customer_id":"acceptance-test"}' \
http://127.0.0.1:8080/api/orders
预期结果不应只看 HTTP 200,还应检查响应结构、关键字段和数据副作用。验收数据应使用专用租户、测试订单或可清理数据,避免把验收脚本变成生产数据写入器。
第四层:回归和兼容性
对于有数据库迁移的应用,需要分别验证:
旧版本应用 + 新数据库结构
新版本应用 + 新数据库结构
如果迁移是破坏性的,例如删除旧列后旧版本无法启动,那么“镜像可回滚”并不等于“业务可回滚”。
六、灰度:同一 Digest,不同流量比例
1. 灰度的定义
灰度发布是让新版本只承接一部分流量,观察实际指标后再扩大范围。它同时控制两个变量:
制品变量:新版本 Digest
流量变量:发送到新版本的请求比例或请求集合
灰度期间应保持:
old = 旧 Digest
new = 新 Digest
而不是创建一个叫 orders-canary:latest 的 Tag,并假定它永远代表刚才验收的内容。
2. Compose 能做什么,不能做什么
在单机上,Compose 可以运行两个独立项目,例如:
docker compose -p orders-blue \
--env-file blue.env \
-f compose.yml up -d
docker compose -p orders-green \
--env-file green.env \
-f compose.yml up -d
两个项目需要使用不同的项目名和端口或网络配置。它们可以分别运行旧 Digest 和新 Digest。
但是,Compose 本身不会自动完成:
- 生产级请求按百分比分配;
- 按用户、地域或请求头路由;
- 跨多台主机调度;
- 灰度期间的容量自动调整;
- 多副本跨节点故障转移。
流量切换通常由反向代理、云负载均衡器、服务网格或其他部署平台完成。可抽象成:
flowchart LR
U[客户端] --> LB[负载均衡/网关]
LB -->|95%| O[旧版本: old Digest]
LB -->|5%| N[新版本: new Digest]
O --> DB[(共享数据库)]
N --> DB
O --> OBS[指标与日志]
N --> OBS
3. 灰度的中间状态和判定
假设:
旧版本错误率 = 0.4%
新版本错误率 = 1.2%
新版本流量 = 5%
不能只看新版本的绝对错误数,因为流量基数不同。可以比较错误率:
error_rate = failed_requests / total_requests
更有意义的灰度判定至少包含:
- 新版本错误率不显著高于旧版本;
- p95 或 p99 延迟没有超出阈值;
- 关键业务成功率正常;
- 容器重启次数没有异常增加;
- CPU、内存、连接池和线程池没有持续耗尽;
- 关键日志没有出现新的错误模式。
灰度扩容可以是:
0% → 1% → 5% → 25% → 50% → 100%
每一步都应有观察窗口和停止条件。百分比本身不是安全保证:如果新版本处理的是少量但关键的支付请求,1% 流量也可能造成严重影响;如果请求被长连接、缓存或固定用户路由,名义上的 5% 也未必等于实际 5%。
4. 灰度期间的状态兼容
最容易被忽略的是:旧版本和新版本会同时运行。若两者共享数据库、缓存或消息队列,必须考虑并发协议。
常用的数据库演进顺序是:
1. 先增加新版本和旧版本都能理解的结构;
2. 发布能够读写兼容结构的新版本;
3. 确认旧版本不再需要后,再删除旧结构。
例如,不能先删除 old_status 列,再期待回滚到仍然读取该列的旧镜像。镜像 Digest 可以恢复旧代码,但数据库结构已经不可逆地变化。
七、回滚:恢复到“旧制品 + 旧配置组合”
1. 回滚不是重新构建
如果当前版本是:
new = registry.example.com/team/orders@sha256:new...
此前已验收版本是:
old = registry.example.com/team/orders@sha256:old...
回滚应直接使用 old:
APP_IMAGE=registry.example.com/team/orders@sha256:old...
APP_ENV=production
DATABASE_HOST=postgres.production.internal
LOG_LEVEL=info
然后:
docker compose --env-file production.env config --quiet
docker compose --env-file production.env pull orders
docker compose --env-file production.env up -d --no-build orders
这里的 --no-build 明确表示不在部署阶段构建。对于 Digest 引用,pull 会请求该摘要对应的内容;如果本地已有内容,Docker 可能复用本地缓存。
回滚后验证:
docker compose --env-file production.env ps
docker compose --env-file production.env logs --tail=100 orders
curl --fail http://127.0.0.1:8080/readyz
如果使用了反向代理或外部负载均衡器,还要恢复流量路由,而不是只替换容器。
2. 回滚必须保存配置版本
只保存镜像 Digest 不够。假设新版本同时修改了:
DATABASE_HOST
FEATURE_FLAG
CACHE_SCHEMA_VERSION
如果回滚镜像却继续使用新配置,可能得到一个旧代码、新配置的未验收组合。
因此发布记录应至少保存:
旧镜像 Digest
旧环境配置版本
旧 Secret 版本或引用
旧流量路由配置
数据库迁移状态
发布时间和操作者
回滚目标应是一个已经验证过的组合:
RollbackTarget = (A_old, C_old, S_old, RuntimePolicy_old)
而不是只有:
RollbackTarget = A_old
3. 数据库迁移是回滚边界
镜像回滚能恢复应用代码,但不能自动撤销:
- 已提交的业务数据;
- 已执行的数据库迁移;
- 已发送的消息;
- 已调用的外部支付或物流接口;
- 已生成并被客户端缓存的响应。
因此需要区分:
应用回滚:恢复旧镜像和运行配置
数据回滚:恢复数据库或业务数据
外部副作用补偿:通过补偿事务或人工流程修复
数据回滚往往比应用回滚风险更高。生产发布策略应优先设计为“向前兼容并向前修复”,而不是依赖数据库快照恢复。
八、常见失败模式与诊断方法
1. Tag 被覆盖,环境之间运行的不是同一内容
失败表现:
staging 使用 orders:release
production 也使用 orders:release
但两个环境的 Config ID 或 RepoDigest 不同。
诊断:
docker image inspect \
--format '{{json .RepoDigests}}' \
registry.example.com/team/orders:release
修复方式是将发布记录改为 Digest,并让部署配置直接使用:
image: registry.example.com/team/orders@sha256:...
Tag 仍然可以用于人类检索,例如:
git-7f3c2a1
release-2025.03.08
但它不应作为生产身份的唯一依据。
2. Digest 在目标环境拉取失败
失败表现可能是:
manifest unknown
或:
no matching manifest for linux/arm64
排查顺序:
docker login registry.example.com
docker buildx imagetools inspect \
registry.example.com/team/orders@sha256:...
docker info
uname -m
可能原因包括:
- Registry 中该 Digest 不存在;
- CI 推送到了另一个仓库;
- 目标主机没有认证权限;
- Digest 对应的索引不包含目标平台;
- Registry 代理或缓存未同步;
- 镜像被错误清理。
认证、缓存和清理策略必须保留所有仍被生产部署记录引用的 Digest。不能仅按“最近没有 Tag”删除,因为 Digest 引用的对象可能仍在运行或用于回滚。
3. docker compose config 成功,但程序启动失败
config 成功只说明 Compose 文件和变量插值合法。以下情况仍可能失败:
- 环境变量语法正确但值错误;
- Secret 文件不存在;
- DNS 名称无法解析;
- 数据库凭据过期;
- 镜像内没有健康检查命令;
- 程序监听的是
127.0.0.1而不是容器对外地址; - 容器端口与服务端口理解不一致。
诊断命令:
docker compose --env-file production.env ps
docker compose --env-file production.env logs --tail=200 orders
docker inspect "$(docker compose --env-file production.env ps -q orders)"
docker exec "$(docker compose --env-file production.env ps -q orders)" \
getent hosts postgres.production.internal
最后一个命令要求镜像中包含 getent;精简镜像可能没有该工具,不能因为命令不存在就断定 DNS 失败,应使用专门的诊断容器或主机工具复核。
4. 容器“健康”,但业务已经不可用
一个过于简单的检查:
healthcheck:
test: ["CMD", "true"]
只能证明容器中存在一个可执行命令,不能证明应用可用。
即使检查 /healthz,也可能因为接口只检查进程而忽略数据库、队列或关键依赖。健康检查应服务于明确的流量接收语义:
存活检查:进程是否应该被重启
就绪检查:实例是否可以接收业务流量
业务验收:真实关键操作是否成功
三者不能由一个永远返回 200 的接口替代。
5. 旧 Digest 已经无法回滚
Registry 清理策略若只按 Tag 保留,可能发生:
生产正在运行 old Digest
release Tag 已指向 new Digest
清理任务删除了 old Digest 的未引用层
发生故障后无法拉取 old Digest
应建立保留规则:
保留当前生产 Digest
保留上一个或多个已验收 Digest
保留仍被发布记录引用的 Digest
保留与审计、合规要求相关的制品
具体清理能力取决于 Registry 产品。Docker Engine 客户端的本地 docker image prune 只影响本机缓存,不等于删除远程 Registry 内容;反过来,Registry 垃圾回收也不应在没有引用分析的情况下执行。
九、Linux 容器边界:不可变镜像不等于不可变运行环境
Docker Linux 容器共享宿主机内核。镜像主要提供用户态文件系统和默认启动配置,以下内容仍由宿主机或运行时决定:
- Linux 内核版本和内核能力;
- CPU 架构;
- cgroup 和 namespace 行为;
- seccomp、AppArmor、SELinux 等安全策略;
- 时区、DNS、网络和存储;
- Docker Engine、containerd、runc 等运行时版本;
- 挂载进容器的宿主机目录;
- 外部数据库、缓存和消息系统。
因此,更完整的运行对象是:
运行结果 =
镜像制品
+ 环境配置
+ Secret
+ 宿主机内核与运行时
+ 外部依赖状态
+ 数据状态
Digest 只固定第一部分。若要求跨环境得到更接近的行为,还要统一或记录:
- CPU 架构和平台;
- Docker Engine 与 Compose 版本;
- 容器用户、能力和安全配置;
- 资源限制;
- 网络和存储驱动;
- 外部服务的兼容版本。
这不是说 Digest 不够有用,而是要避免把“镜像内容确定”误解成“系统行为完全确定”。
十、一个最小但完整的晋级脚本骨架
下面的示例假设:
- CI 已经构建并推送镜像;
DIGEST来自构建元数据或 Registry 查询;- 目标主机已经登录 Registry;
compose.yml中的服务名为orders。
生成 staging 配置:
set -eu
REPOSITORY="registry.example.com/team/orders"
DIGEST="$(jq -r '."containerimage.digest"' build-metadata.json)"
case "$DIGEST" in
sha256:*) ;;
*)
echo "非法 Digest: $DIGEST" >&2
exit 1
;;
esac
cat > staging.env <<EOF
APP_IMAGE=${REPOSITORY}@${DIGEST}
APP_ENV=staging
DATABASE_HOST=postgres.staging.internal
LOG_LEVEL=debug
EOF
渲染并启动:
docker compose --env-file staging.env config --quiet
docker compose --env-file staging.env pull orders
docker compose --env-file staging.env up -d --no-build orders
执行基础验收:
docker compose --env-file staging.env ps
curl --fail --silent --show-error \
http://127.0.0.1:8080/readyz
curl --fail --silent --show-error \
-H 'Content-Type: application/json' \
-d '{"customer_id":"acceptance-test"}' \
http://127.0.0.1:8080/api/orders
只有这些步骤通过,流水线才应把以下信息写入发布记录:
repository = registry.example.com/team/orders
digest = sha256:...
environment = staging
config_revision = ...
acceptance_result = passed
之后生产环境只替换环境配置文件中的环境参数,保留完全相同的 APP_IMAGE:
APP_IMAGE=registry.example.com/team/orders@sha256:4f3c...9a1b
APP_ENV=production
DATABASE_HOST=postgres.production.internal
LOG_LEVEL=info
这一步体现了晋级的核心因果关系:
staging 验收通过的是 A
production 灰度部署的仍然是 A
production 全量部署的仍然是 A
变化的是环境和流量,不是制品本身。
十一、规范保证、实现行为与工程建议的边界
规范或机制层面的保证
- 镜像清单可以使用内容 Digest 引用;
repo@digest与 Tag 是不同的引用形式;- Docker 镜像由配置对象和文件系统层等内容组成;
- 多架构镜像可以通过索引选择平台清单;
- Compose 支持通过变量插值构造配置,并可以使用 Digest 形式的
image引用。
常见实现行为
docker image inspect展示本地对象、Config ID 和 RepoDigests;- Buildx 的元数据文件包含构建输出 Digest;
- Docker Engine 根据主机平台从多架构索引选择对应清单;
- Compose 在执行
up前解析变量并创建容器。
这些行为会受 Docker Engine、Buildx、Compose 实现和版本影响,自动化应优先使用机器可读输出,并在升级工具链后重新验证。
工程上的可靠约束
- 发布记录保存 Digest,而不是只保存 Tag;
- staging、灰度和生产使用同一个制品 Digest;
- 环境配置、Secret 和镜像制品分开版本化;
- 灰度由流量入口控制,不能把 Compose 的启动能力误当成流量治理;
- 回滚保存的是完整的“镜像 + 配置 + 路由”组合;
- Registry 清理任务必须保护仍可运行和可回滚的 Digest;
- 数据库迁移采用兼容优先的策略,避免把应用回滚误当成数据回滚。
最终可以用一个判定条件概括一次合格的制品晋级:
晋级有效
⇔
目标环境实际运行的 Digest = 已验收 Digest
∧ 环境配置满足该环境契约
∧ 运行时验收通过
∧ 灰度指标满足停止条件
∧ 回滚目标仍可获取
其中任何一个条件不成立,都不能仅凭“容器启动成功”认为发布是安全的。
系列导航与关联阅读
- 系列入口:Docker 完整学习路线:从镜像与容器到安全、可观测和生产交付
- 上一篇:Docker CI 构建流水线:缓存、并行、扫描、签名、推送和晋级
- 下一篇:Docker Swarm 基础与边界:Service、Task、Overlay、Secret 和滚动更新
- 延伸:Docker Registry 与镜像分发:Tag、Digest、认证、缓存和清理
- 延伸:Docker 生产交付体系:CI、灰度、回滚、容量和运行手册
官方资料
本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。

评论
0 条讨论