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

Docker 不可变制品晋级:Digest、环境配置、验收、灰度和回滚

在 Docker 交付流程中,“把镜像从测试环境推到生产环境”通常不是重新构建一次,也不是把某个 Tag 改成 latest。更可靠的模型是:

  1. 构建一个镜像制品;
  2. 将制品推送到 Registry;
  3. 记录它的内容摘要 Digest;
  4. 为每个环境单独注入配置;
  5. 在目标环境验收同一个 Digest;
  6. 通过灰度逐步增加流量;
  7. 出现问题时恢复到此前已验收的 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/amd64linux/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.04 Tag 后续指向了不同基础镜像;
  • 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"
}

sourceRevisionconfigRevision 是审计信息,不应被误认为镜像身份。真正用于拉取制品的仍然是 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、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。