Docker 基础体系 · 第 55/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。
Compose 多环境治理:开发、测试、生产差异、Secret 和不可变制品
Compose 的难点不在于“把多个容器写进一个 YAML 文件”,而在于:同一套服务如何在开发、测试和生产中保持一致的运行语义,同时允许环境拥有合理差异;敏感信息如何进入容器而不进入镜像和普通配置;构建出的制品如何被验证、晋级、回滚,而不是每个环境重新构建一份“看起来相同”的镜像。
本文以现代 Docker Engine、BuildKit 和 Compose Specification 为范围,默认运行边界是 Linux 容器。Compose 可以很好地描述单机或一组 Docker Engine 上的应用,但它本身不是完整的生产调度器,也不自动提供灰度流量治理、跨节点故障转移或高可用存储。
1. 先建立正确模型:Compose 最终运行的不是某个 YAML 文件
一个 Compose 应用可以抽象为三层:
- 制品(artifact):镜像及其内容,通常用镜像摘要标识。
- 应用模型(application model):服务、网络、卷、Secret、依赖关系、健康检查等。
- 环境输入(environment inputs):变量插值、外部 Secret、挂载目录、运行时平台资源。
最终启动结果可以写成:
其中:
- 是 Compose 文件集合;
- 是用于变量插值的输入;
- 是 Compose CLI 解析、合并并插值后得到的应用模型;
- 是 Docker Engine 及其本地资源;
- 是实际创建的容器、网络、卷和 Secret 挂载。
这个模型解释了几个常见现象:
- 修改
.env可能改变容器环境变量,即使 YAML 没变; - 修改
compose.prod.yml可能只改变端口和资源限制,但不改变镜像; - 镜像摘要不变,不代表运行结果完全不变,因为外部配置、卷内容和 Secret 仍然可能变化;
docker compose config验证的是解析后的应用模型,不是应用是否能正确启动,也不是数据库迁移是否成功。
一个更适合交付的约束是:
这里 表示应用镜像的内容摘要,而不是镜像标签。允许环境不同的部分应被限制在配置、资源和外部依赖上:
其中 是环境, 是非敏感配置, 是 Secret, 是数据卷或外部数据。
因此,“开发和生产使用同一个镜像”并不意味着两者使用同一个数据库、同一个日志级别或同一个公开端口;它意味着构建步骤只发生一次,环境差异在部署阶段表达。
2. 多环境设计:共享基线,环境文件只表达差异
一个可维护的目录可以如下组织:
project/
├── compose.yaml
├── compose.dev.yaml
├── compose.test.yaml
├── compose.prod.yaml
├── .env.example
├── Dockerfile
├── app/
└── ops/
└── secrets/
├── dev/
└── test/
基础文件描述所有环境都必须具备的服务语义:
# compose.yaml
name: demo
services:
api:
build:
context: .
target: runtime
image: registry.example.com/demo/api:${APP_TAG:-dev}
environment:
APP_NAME: demo-api
LOG_LEVEL: ${LOG_LEVEL:-info}
DATABASE_HOST: db
DATABASE_PORT: "5432"
DATABASE_USER: app
DATABASE_NAME: app
secrets:
- db_password
depends_on:
db:
condition: service_healthy
networks:
- backend
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_DB: app
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 5s
timeout: 3s
retries: 20
volumes:
- db-data:/var/lib/postgresql/data
networks:
- backend
secrets:
db_password:
file: ${DB_PASSWORD_FILE:-./ops/secrets/dev/db_password}
volumes:
db-data:
networks:
backend:
这个基础文件包含:
api和db两个服务;- 容器之间通过服务名
db通信,而不是通过宿主机localhost; - PostgreSQL 从
/run/secrets/db_password读取密码; api等待数据库健康检查通过后再启动;- 数据库数据位于命名卷,而不是容器可写层。
需要注意,depends_on.condition: service_healthy 只表达 Compose 启动顺序上的等待。它不能保证数据库已经完成业务初始化、迁移或接受所有类型的请求。应用仍应实现连接重试,迁移也应作为明确的交付步骤处理。
开发环境:增加源码挂载和调试能力
# compose.dev.yaml
services:
api:
build:
target: development
environment:
LOG_LEVEL: debug
ports:
- "8080:8080"
volumes:
- ./app:/workspace/app
启动开发环境:
docker compose -f compose.yaml -f compose.dev.yaml up --build
多个 -f 文件从左到右合并,后面的文件通常覆盖或扩展前面的模型。这里的结果是:
api仍然使用基础文件中的 Secret、网络和数据库依赖;target从runtime改为development;- 增加端口映射和源码挂载;
LOG_LEVEL被改为debug。
开发环境可以使用源码挂载,因为开发目标是快速反馈;但不能把源码挂载带进生产,否则容器中的程序内容不再由镜像完全决定,且宿主机文件权限、换行符和文件监听行为都会成为运行时变量。
测试环境:使用构建制品,而不是开发挂载
# compose.test.yaml
services:
api:
image: registry.example.com/demo/api:${APP_TAG:?APP_TAG is required}
build: null
environment:
LOG_LEVEL: warn
ports:
- "18080:8080"
运行测试:
APP_TAG=git-3f2a1c7 docker compose \
-f compose.yaml \
-f compose.test.yaml \
config
这里使用 build: null 的目的,是在合并后的模型中移除基础文件的 build 配置,避免测试人员误以为 up --build 会产生应该被测试的镜像。测试环境应该拉取 CI 已经构建并推送的镜像:
APP_TAG=git-3f2a1c7 docker compose \
-f compose.yaml \
-f compose.test.yaml \
pull api
APP_TAG=git-3f2a1c7 docker compose \
-f compose.yaml \
-f compose.test.yaml \
up -d
如果测试结果依赖“测试机重新构建的镜像”,那么测试验证的就不是将要晋级到生产的制品。
生产环境:固定摘要,不接受模糊标签
生产文件可以直接固定镜像摘要:
# compose.prod.yaml
services:
api:
image: registry.example.com/demo/api@sha256:REPLACE_WITH_REAL_DIGEST
build: null
environment:
LOG_LEVEL: info
ports:
- "127.0.0.1:8080:8080"
restart: unless-stopped
read_only: true
tmpfs:
- /tmp
实际交付时不能保留 REPLACE_WITH_REAL_DIGEST,应由发布系统生成完整文件或使用经过审计的模板渲染。启动前执行:
docker compose -f compose.yaml -f compose.prod.yaml config -q
docker compose -f compose.yaml -f compose.prod.yaml pull
docker compose -f compose.yaml -f compose.prod.yaml up -d
config -q 成功只表示 Compose 配置能够解析,不能证明镜像存在、Secret 可读、端口可绑定或应用健康。随后仍需检查:
docker compose -f compose.yaml -f compose.prod.yaml ps
docker compose -f compose.yaml -f compose.prod.yaml logs --tail=100 api
3. Compose 文件合并:覆盖不是简单的文本替换
Compose 的多个文件合并是在 YAML 解析之后对 Compose 模型进行合并,不是把两个文本文件直接拼接。因此,以下写法并不可靠:
# 错误思路:把完整 YAML 当成字符串替换
services:
api:
ports:
- "8080:8080"
环境文件真正改变的是 api 服务模型中的 ports 字段。合并规则需要按字段类型理解。
3.1 标量字段通常由后者覆盖
例如:
# 基础
services:
api:
image: example/api:v1
restart: unless-stopped
# 覆盖
services:
api:
image: example/api:v2
结果近似为:
services:
api:
image: example/api:v2
restart: unless-stopped
image 是标量,后一个值替换前一个值。
3.2 映射字段通常按键合并
# 基础
services:
api:
environment:
LOG_LEVEL: info
HTTP_PORT: "8080"
# 覆盖
services:
api:
environment:
LOG_LEVEL: debug
结果为:
environment:
LOG_LEVEL: debug
HTTP_PORT: "8080"
这很适合按环境修改少量配置,但也有一个风险:删除操作不是自然发生的。如果生产文件不再希望继承某个环境变量,仅仅不写它并不会删除基础文件中的键。
现代 Compose 实现支持特殊 YAML 标签控制合并行为:
services:
api:
environment:
FOO: !reset null
!reset 用于将继承值重置为空或默认值;!override 用于完全替换某个字段。它们属于 Compose 对 YAML 合并的扩展,使用前应确认部署环境中的 Compose 版本支持,并在 CI 中执行 docker compose config 验证,而不能假设所有旧版客户端都能解析。
3.3 列表字段不应假设都是“覆盖”或都是“追加”
ports、volumes、secrets、configs 等具有唯一键语义的资源,在合并时需要按资源身份处理。例如端口的身份通常由目标端口、协议等属性决定,而不是简单按文本行号决定。
基础文件:
services:
api:
ports:
- "8080:8080"
开发覆盖:
services:
api:
ports:
- "18080:8080"
结果通常包含两个不同的端口映射,因为宿主机端口不同。若基础文件和覆盖文件都声明了同一个目标端口,结果则取决于 Compose 的唯一资源合并规则,不能把它当成普通列表追加。
对于命令字段要特别谨慎:
services:
api:
command: ["./server", "--port", "8080"]
覆盖文件写:
services:
api:
command: ["./server", "--debug"]
工程上应把它理解为替换完整命令,而不是把两个数组拼接成:
./server --port 8080 ./server --debug
每次合并后都应查看真实结果:
docker compose \
-f compose.yaml \
-f compose.dev.yaml \
config
config 输出的是插值和合并之后的模型,是排查“为什么这个变量或端口仍然存在”的首要工具。
4. include 与 extends:解决不同层次的问题
4.1 include 是跨文件装配
include 用于把另一个 Compose 文件作为一个完整的 Compose 模型引入。例如把监控组件单独维护:
# observability.yaml
services:
metrics:
image: prom/prometheus:v2.53.0
networks:
- backend
networks:
backend:
name: demo_backend
主文件:
# compose.yaml
include:
- observability.yaml
services:
api:
image: registry.example.com/demo/api@sha256:...
networks:
- backend
networks:
backend:
name: demo_backend
include 适合团队把一组相关资源封装成可复用组件,例如日志、监控或本地依赖。它不是简单的文本宏;被引入文件本身也可能有服务、网络、卷和 Secret,命名冲突和资源边界需要通过 config 检查。
include 属于较新的 Compose Specification 能力,具体可用性依赖 Compose CLI 版本。生产流水线必须固定 CLI 版本,并在目标环境用同一版本验证,而不能只在开发者机器上验证。
4.2 extends 是服务级继承
extends 用于一个服务复用另一个服务的配置:
# compose.yaml
services:
api-base:
image: registry.example.com/demo/api@sha256:...
environment:
LOG_LEVEL: info
networks:
- backend
api:
extends:
service: api-base
ports:
- "8080:8080"
networks:
backend:
它表达的是“api 继承 api-base 的服务定义”,而不是“整个项目引入另一个 Compose 应用”。extends 可能让服务之间的依赖关系变得不明显,因此不应把它当成面向所有环境的默认组织方式。
一个实用区分是:
- 多个
-f:同一个应用的环境覆盖; include:引入一个较完整的 Compose 组件;extends:复用某个服务的定义;- YAML anchor:同一文件内的结构复用,不等价于 Compose 模型继承。
5. 变量插值:配置值的来源必须可追踪
Compose 变量插值发生在模型解析阶段,例如:
services:
api:
image: "${IMAGE_REPOSITORY:?IMAGE_REPOSITORY is required}:${APP_TAG:-dev}"
environment:
LOG_LEVEL: "${LOG_LEVEL:-info}"
表达式含义:
${VAR}:变量存在时使用其值,不存在时通常为空;${VAR:-default}:变量未设置或为空时使用默认值;${VAR:?message}:变量未设置或为空时直接报错;${VAR-default}和${VAR?message}:只在变量未设置时处理,变量为空时保留空值。
生产镜像不应使用无声默认值:
image: "${IMAGE_REPOSITORY}:${APP_TAG:-latest}"
因为 APP_TAG 拼写错误或发布系统未注入变量时,可能意外部署 latest。生产更适合:
image: "${IMAGE_REPOSITORY:?required}@${IMAGE_DIGEST:?required}"
Compose 的变量插值输入主要来自:
- 执行命令的 shell 环境;
--env-file指定的文件;- 项目目录中的
.env等 Compose 环境文件。
精确优先级和选项会随 Compose CLI 能力变化,应通过实际命令确认:
docker compose \
--env-file .env.test \
-f compose.yaml \
-f compose.test.yaml \
config --environment
env_file 服务字段是另一个概念:
services:
api:
env_file:
- ./app.env
它主要用于把变量注入容器环境,并不等价于用于 Compose 文件插值的全局环境文件。下列两个变量的来源不同:
services:
api:
image: "example/api:${TAG:-dev}" # Compose 解析时插值
env_file:
- ./runtime.env # 容器启动时注入
其中 runtime.env 中的 TAG 不一定会参与 image 字段的解析。
验证配置时建议使用:
docker compose \
--env-file .env.test \
-f compose.yaml \
-f compose.test.yaml \
config -q
以及:
docker compose \
--env-file .env.test \
-f compose.yaml \
-f compose.test.yaml \
config --images
前者检查模型是否能解析,后者帮助确认实际会使用哪些镜像。注意:如果把 Secret 写进变量并通过 config 输出,敏感值可能出现在终端、CI 日志或审计记录中。Secret 不应通过普通插值传递。
6. Secret:文件挂载不是“加密”,但比普通环境变量更适合运行时密码
Compose Secret 的基本结构是:
secrets:
db_password:
file: ./ops/secrets/dev/db_password
services:
db:
image: postgres:16
secrets:
- source: db_password
target: db_password
uid: "999"
gid: "999"
mode: 0400
容器中默认可见路径是:
/run/secrets/db_password
应用或数据库通过读取文件取得 Secret:
cat /run/secrets/db_password
但生产中不应执行这个命令,也不应把它写入日志。PostgreSQL 官方镜像支持:
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
这比:
environment:
POSTGRES_PASSWORD: "plain-password"
更不容易被 docker inspect、进程环境转储和调试输出直接暴露。
6.1 Compose 文件 Secret 的真实边界
在普通 Docker Compose、非 Swarm 的 Linux Docker Engine 上,下面这种声明:
secrets:
db_password:
file: ./ops/secrets/prod/db_password
通常意味着 Compose 从宿主机读取文件,并将其以文件形式提供给容器。它解决的是:
- 不把密码写进镜像层;
- 不把密码直接写进容器环境变量;
- 让应用使用固定文件路径读取凭据;
- 可以通过文件权限和部署主机权限控制访问。
它不自动意味着:
- Secret 在宿主机上被加密保存;
- Secret 在 Compose YAML 中被加密;
- Secret 不会被拥有宿主机权限的管理员读取;
- Secret 会自动轮换;
- Secret 会自动从云端密钥管理系统获取。
external: true 适合引用平台已存在的 Secret:
secrets:
db_password:
external: true
name: demo_db_password
但外部 Secret 的可用性取决于部署后端。Docker Swarm 的 Secret 管理与普通 Docker Compose 的文件型 Secret 不是同一套运行模型;不能仅因为 YAML 中写了 external: true,就认为独立 Docker Engine 已经具备 Swarm Secret 存储能力。
6.2 Secret 不应进入构建上下文
错误做法:
FROM alpine
COPY ops/secrets/prod/db_password /run/secrets/db_password
即使之后删除文件,它也可能已经存在于某个镜像层中。正确做法是让 Secret 在运行阶段挂载。
如果构建阶段确实需要私有包仓库凭据,应使用 BuildKit 的 secret mount,而不是 ARG 或 ENV:
# syntax=docker/dockerfile:1
FROM python:3.12-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN --mount=type=secret,id=pip_token \
TOKEN="$(cat /run/secrets/pip_token)" && \
pip install --no-cache-dir \
--index-url "https://token:${TOKEN}@packages.example.com/simple" \
-r requirements.txt
Compose 构建配置可以写成:
services:
api:
build:
context: .
secrets:
- pip_token
secrets:
pip_token:
file: ./ops/secrets/dev/pip_token
该 Secret 是 BuildKit 构建会话中的临时挂载,不应进入最终镜像层。仍需注意:
- 构建日志不能打印令牌;
- 构建上下文中不能包含无关敏感文件;
- CI 运行器本身仍需要受到保护;
- 使用的 Compose CLI 和 BuildKit 版本必须支持对应的
build.secrets语法。
6.3 Secret 轮换必须考虑进程生命周期
假设宿主机上的 Secret 文件从旧值替换为新值:
install -m 0400 new_password ./ops/secrets/prod/db_password
容器中的应用是否立即看到新值,不能只靠 Compose 文件推断。不同挂载实现和应用读取方式可能不同:
- 应用启动时读取一次:文件变了,内存中的密码仍是旧值;
- 应用每次请求读取:可能读到新值,但通常不推荐这样做;
- 应用支持信号或管理接口重载:可以设计平滑轮换;
- 数据库密码轮换:还必须协调数据库端修改、连接池重连和旧密码失效顺序。
因此 Secret 轮换流程应明确为状态转换:
旧凭据有效
-> 写入新凭据
-> 修改依赖系统接受新凭据
-> 让应用重新读取或滚动重建
-> 验证新连接
-> 失效旧凭据
若在“应用重建”之前就失效旧密码,连接池可能全部失败;若永不失效旧密码,则轮换只完成了一半。
7. 不可变制品:摘要比标签更接近真实身份
镜像标签是可变指针:
registry.example.com/demo/api:prod
今天它可能指向摘要 A,明天可以被推送到摘要 B。镜像摘要是内容寻址标识:
registry.example.com/demo/api@sha256:abcdef...
发布流程应接近:
源代码提交
-> CI 构建
-> 单元测试和安全检查
-> 推送镜像
-> 获取 digest
-> 用 digest 部署测试
-> 验证
-> 用同一 digest 部署生产
构建示例:
docker buildx build \
--platform linux/amd64 \
--tag registry.example.com/demo/api:git-3f2a1c7 \
--push \
.
查询摘要:
docker buildx imagetools inspect \
registry.example.com/demo/api:git-3f2a1c7
输出中会出现类似:
Name: registry.example.com/demo/api:git-3f2a1c7
Digest: sha256:abcdef...
生产 Compose 使用:
services:
api:
image: registry.example.com/demo/api@sha256:abcdef...
build: null
这里的 build: null 防止生产命令意外依据本地上下文重新构建。更严格的发布系统还会在部署前检查:
docker image inspect \
registry.example.com/demo/api@sha256:abcdef...
并将以下信息作为发布记录:
- Git 提交;
- 镜像摘要;
- Dockerfile 和依赖锁文件摘要;
- 构建平台;
- 测试结果;
- Compose 渲染后的非敏感配置摘要;
- 部署时间和操作者。
7.1 多平台镜像的边界
如果生产主机是 linux/amd64,而 CI 构建机是 ARM 主机,不能只写:
docker build .
然后假设产物适用于所有平台。应显式构建目标平台,或构建多平台镜像索引:
docker buildx build \
--platform linux/amd64,linux/arm64 \
--tag registry.example.com/demo/api:git-3f2a1c7 \
--push \
.
生产主机最终拉取的是目标平台对应的镜像内容,但发布记录仍应记录索引摘要和实际平台摘要。Linux 容器不能直接解决 Windows 内核容器的兼容问题;本文的网络、权限、挂载和文件路径示例均以 Linux 容器为边界。
7.2 不可变制品不等于不可变运行环境
即使镜像摘要固定,以下对象仍可能变化:
- 外部 Secret;
- 命名卷中的数据库数据;
- 宿主机 bind mount;
- DNS 和外部 API;
- Docker Engine、内核和 cgroup 配置;
- Compose 文件中的环境变量。
所以完整审计身份应至少是:
其中 Secret 的值不应记录到日志,但可以记录其版本号或不可逆摘要。
8. 验证:把“能解析”与“能运行”分成不同层次
一个可靠的验证流程至少分四层。
第一层:Compose 语法和合并模型
docker compose \
--env-file .env.test \
-f compose.yaml \
-f compose.test.yaml \
config -q
失败示例包括:
- YAML 缩进错误;
- 必需变量未设置;
include文件不存在;- 合并结果不满足 Compose 模型;
- Secret 源文件路径无效。
第二层:镜像和平台
docker compose \
-f compose.yaml \
-f compose.prod.yaml \
pull
docker compose \
-f compose.yaml \
-f compose.prod.yaml \
images
此时要验证镜像能否拉取、架构是否匹配、摘要是否符合发布记录。
第三层:容器和依赖健康
docker compose \
-f compose.yaml \
-f compose.test.yaml \
up -d
docker compose ps
docker compose logs --tail=200 api
docker compose ps 显示的是容器状态和健康状态;running 不等价于业务可用。健康检查应尽量验证真实依赖,例如 HTTP /health/ready 可以检查数据库连接和必要迁移状态,而不是只检查进程存在。
第四层:业务验证
curl --fail http://127.0.0.1:18080/health/ready
curl --fail http://127.0.0.1:18080/version
/version 最好返回 Git 提交或构建版本,但不应返回 Secret。这样可以确认“正在运行的容器”确实来自预期制品。
9. 故障路径:启动失败时如何判断是配置、Secret 还是制品问题
一个典型启动时序如下:
sequenceDiagram
participant CI as CI/CD
participant R as Registry
participant H as Docker Engine
participant C as Compose CLI
participant A as API
participant DB as PostgreSQL
CI->>R: 推送镜像并取得 digest
CI->>C: 提供 Compose 文件和环境变量
C->>C: 插值、合并、config 校验
C->>R: 请求指定 digest
R-->>H: 返回目标平台镜像
C->>H: 创建网络、卷、容器
H->>DB: 挂载 Secret 并启动数据库
DB-->>H: healthcheck 通过
H->>A: 启动 API 并挂载同一 Secret
A->>DB: 使用 /run/secrets/db_password 连接
DB-->>A: 接受连接
A-->>CI: readiness 验证成功
故障点可以按阶段定位:
| 阶段 | 典型表现 | 首要检查 |
|---|---|---|
| 插值 | required variable is missing |
shell、--env-file、变量名拼写 |
| 合并 | 端口或环境变量不符合预期 | docker compose config |
| 拉取 | manifest unknown、权限错误 |
registry、digest、登录凭据、平台 |
| Secret 创建 | 文件不存在或权限错误 | Secret 源路径、宿主机权限、目标用户 |
| 数据库启动 | 容器退出、密码认证失败 | docker compose logs db、Secret 内容格式 |
| API 启动 | 连接拒绝、解析失败 | 服务名、网络、健康检查、连接重试 |
| 业务验证 | HTTP 非 2xx | 应用日志、迁移、外部依赖 |
Secret 文件尤其容易出现隐藏换行问题。创建密码文件时应确认是否包含结尾换行;应用通常应读取后去除换行,但不能假设所有程序都会处理:
printf '%s' 'correct-password' > ./ops/secrets/dev/db_password
chmod 0400 ./ops/secrets/dev/db_password
不要用:
echo 'correct-password' > ./ops/secrets/dev/db_password
除非已经确认目标程序允许结尾换行。
10. 生产交付:回滚的是已验证摘要,而不是重新构建
假设当前生产版本为:
api@sha256:AAA
新版本为:
api@sha256:BBB
部署前应先在测试环境使用 BBB 完成验证。生产发布失败时,回滚操作应恢复到已知可用的 Compose 文件和摘要:
services:
api:
image: registry.example.com/demo/api@sha256:AAA
build: null
然后:
docker compose -f compose.yaml -f compose.prod.yaml pull api
docker compose -f compose.yaml -f compose.prod.yaml up -d api
docker compose -f compose.yaml -f compose.prod.yaml ps
这类回滚通常会重建或替换 api 容器,但不会自动回滚数据库卷。数据库迁移因此必须满足至少一个条件:
- 向后兼容,使旧应用仍能读取新结构;
- 迁移分成可扩展、可收缩的阶段;
- 有明确的数据备份和恢复流程。
如果 BBB 执行了不可逆迁移,单纯把镜像改回 AAA 可能导致旧应用无法启动。镜像回滚和数据回滚是两个不同的操作,不能合并成一个 docker compose up 命令。
灰度的真实边界
单个 Compose 项目不会自动完成生产灰度。要进行灰度,至少需要:
- 两个可独立启动的应用副本或两个 Compose 项目;
- 反向代理、服务发现或负载均衡;
- 明确的流量权重切换;
- 独立的健康指标和停止条件。
例如:
proxy
├── api-blue -> digest AAA
└── api-green -> digest BBB
先启动 api-green,通过 readiness、错误率和关键业务检查,再让代理把少量流量转给 green。若失败,将代理切回 blue。Compose 负责创建和管理服务容器,但不会替代理解外部流量系统的状态。
11. 容量和运行手册:Compose 能描述什么,不能保证什么
生产配置中的资源限制应与容量假设相匹配:
services:
api:
cpus: "2.0"
mem_limit: 1g
pids_limit: 512
这些字段约束容器在 Docker Engine 上的资源使用,但不自动完成容量规划。容量判断需要把请求量、并发、单请求资源、数据库连接和宿主机余量放在同一个模型中。
例如,若每个 API 容器最多使用 100 个数据库连接,运行 3 个副本,理论连接上限至少是:
其中:
- 是数据库允许的总连接数;
- 是迁移任务占用的连接;
- 是运维和监控预留连接。
如果只增加 API 容器数量,却不调整数据库连接池,扩容可能把数据库直接推入连接耗尽,而不是提升吞吐。
一份可执行的 Compose 运行手册至少应包含:
# 查看解析后的最终模型
docker compose -f compose.yaml -f compose.prod.yaml config
# 查看服务状态
docker compose -f compose.yaml -f compose.prod.yaml ps
# 查看最近日志
docker compose -f compose.yaml -f compose.prod.yaml logs --tail=200 api
# 重启单个无状态服务
docker compose -f compose.yaml -f compose.prod.yaml up -d --no-deps api
# 停止项目,但保留命名卷
docker compose -f compose.yaml -f compose.prod.yaml down
# 删除卷:高风险,仅在确认数据已备份且确实要销毁时执行
docker compose -f compose.yaml -f compose.prod.yaml down -v
down -v 会删除项目声明的命名卷,数据库场景下通常意味着数据损失风险。生产手册必须明确哪些命令允许执行、执行前需要什么备份、执行后如何验证恢复,而不能只写“出问题就重启”。
12. 常见误解及其失败表现
误解一:.env 就是 Secret 管理
.env 适合保存本地开发的非敏感默认值,例如:
APP_TAG=dev
LOG_LEVEL=debug
它不适合作为生产密码库,因为文件可能进入 Git、CI 工作目录、备份系统或错误日志。敏感值应通过 Secret 管理系统、受限文件或部署平台凭据机制提供。
误解二:depends_on 能保证应用可用
它最多控制 Compose 创建和启动顺序,并可配合健康检查等待依赖达到健康状态。它不能处理:
- 数据库迁移;
- 外部 API 限流;
- DNS 暂时不可用;
- 应用启动后连接池初始化失败;
- 依赖启动后再次故障。
应用必须有超时、重试和失败退出策略,健康检查也应反映实际服务能力。
误解三:使用同一个标签就等于使用同一个制品
api:prod 可能在 registry 中被重新推送。跨环境交付应记录 digest,并在测试和生产使用同一个 digest。
误解四:固定镜像摘要后就可以随意修改卷和配置
摘要只固定镜像内容。数据库卷、Secret、环境变量、网络和宿主机内核仍会改变运行结果。发布记录需要同时管理制品和运行配置。
误解五:Compose 生产环境等价于 Kubernetes
Compose 可以在单机 Docker Engine 上完成相当清晰的服务编排和交付,但它不天然提供:
- 跨节点调度;
- 自动副本重调度;
- 原生滚动升级控制器;
- 服务级流量权重;
- 分布式 Secret 管理;
- 自动故障转移存储。
当这些能力成为硬性要求时,应评估 Swarm、Kubernetes 或云平台托管服务,而不是继续堆叠 Compose 文件。
13. 一套最小但完整的交付检查
发布一个 Compose 应用时,可以把流程固化为以下顺序:
# 1. 在 CI 中构建并推送一次
docker buildx build \
--platform linux/amd64 \
-t registry.example.com/demo/api:git-3f2a1c7 \
--push .
# 2. 获取并记录 digest
docker buildx imagetools inspect \
registry.example.com/demo/api:git-3f2a1c7
# 3. 用目标测试配置渲染并验证
APP_TAG=git-3f2a1c7 \
docker compose \
-f compose.yaml \
-f compose.test.yaml \
config -q
# 4. 测试环境拉取同一版本
APP_TAG=git-3f2a1c7 \
docker compose \
-f compose.yaml \
-f compose.test.yaml \
pull api
APP_TAG=git-3f2a1c7 \
docker compose \
-f compose.yaml \
-f compose.test.yaml \
up -d
# 5. 执行健康和业务验证
curl --fail http://127.0.0.1:18080/health/ready
# 6. 将已验证的 digest 写入生产模型,再渲染
docker compose \
-f compose.yaml \
-f compose.prod.yaml \
config -q
# 7. 生产部署和验证
docker compose \
-f compose.yaml \
-f compose.prod.yaml \
pull api
docker compose \
-f compose.yaml \
-f compose.prod.yaml \
up -d api
curl --fail http://127.0.0.1:8080/health/ready
这套流程的核心不是命令数量,而是三个不变量:
- 环境差异显式存在于 Compose 模型,而不是隐藏在开发者机器状态中;
- Secret 通过运行时输入进入容器,不进入镜像和普通配置日志;
- 测试、灰度和生产使用同一个已记录的不可变镜像摘要。
Compose Specification 的文件模型、合并、变量插值、Secret、include 和 extends 规则,应以当前 Compose 文档和规范为准;部署系统还必须锁定 Compose CLI、Docker Engine、BuildKit 和目标平台,因为解析能力、特殊合并标签和构建选项都可能存在版本差异。只有把配置解析、制品身份、Secret 生命周期和运行验证放在同一条交付链中,多环境才不会退化为几份互相漂移的 YAML 文件。
系列导航与关联阅读
- 系列入口:Docker 完整学习路线:从镜像与容器到安全、可观测和生产交付
- 上一篇:Compose 配置合并:Override、include、extends、变量插值和验证
- 下一篇:Compose 扩容与并发:Replica、端口、负载均衡、共享状态和边界
- 延伸:Docker 生产交付体系:CI、灰度、回滚、容量和运行手册
官方资料
本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。

评论
0 条讨论