Compose.yaml 文件往往会逐渐演变成充斥着重复代码块、且启动依赖关系脆弱的冗长文件。Docker Compose 提供了一些内置功能,能让应用栈更整洁、更具可重用性且行为更可预测。
试想一个小型 Web 应用栈,其中包含 Python 应用程序、后台工作进程(worker)、PostgreSQL 数据库和 Nginx 代理,所有这些都定义在同一个”compose.yaml”文件中。随着应用栈的扩充,往往需要在各个服务间重复相同的配置。
更糟糕的是,在系统重启后,应用程序可能会在 PostgreSQL 尚未真正就绪时就启动,从而导致故障,直到所有服务都稳定运行。
常见的变通方法包括添加”sleep”命令,或者维护多个几乎完全相同的 Compose 文件。但这两种做法很快就会变得比应用栈本身更难维护。
Docker Compose v2 已经提供了更优的解决方案。诸如锚点(anchors)、健康检查(healthchecks)、配置集(profiles)、包含(include)和监视(watch)等功能,能够减少代码重复、妥善处理服务依赖关系,并简化开发工作流。
其中许多功能是在 Compose 2.17 到 2.24 版本之间引入或扩展的,因此很容易被忽略。
如何挑选这些 Docker Compose 特性
这些特性均基于 Docker Compose 的项目模型运作:在该模型中,Compose 会先整合并解析容器的配置,然后再创建容器。
这些特性均内置于 Compose 规范(Compose Specification)中,无需任何第三方插件。我们将通过位于示例服务器(IP 地址:192.168.1.1)上”/home/rultr/webapp”目录下的一个示例应用栈来演示这些特性。
检查 Docker Compose 版本
由于其中一些功能是在近期的 Compose 版本中引入或扩展的,所以需要首先确认使用的是 Docker Compose v2 插件,而不是旧版的 docker-compose 命令:
# docker compose version
典型输出如下:
Docker Compose version v5.6.0
不同的版本可能有所不同,对于教程中的示例,我们建议使用 Docker Compose v2.24 或更高版本,以确保具备所需的功能。
1. 使用 YAML 锚点和 x- 扩展字段重用服务设置
确认 Compose 版本后,最常见的问题是配置重复。如果 Web 服务和 Worker 服务使用相同的重启策略和日志限制,分别维护这些设置意味着每次更改都需要进行两次。
例如在 Debian 系统上,使用以下命令打开 Compose 文件:
# cd /home/rultr/webapp && vi compose.yaml
在文件中添加如下内容:
# yaml
# /home/rultr/webapp/compose.yaml
x-common: &common
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
services:
web:
<<: *common
build: .
ports:
- "8080:8000"
worker:
<<: *common
build: .
command: python worker.py
该语法的具体作用如下:
- x-common 是一个扩展字段,Compose 在创建服务时会忽略它,因此适合用于存储可复用的配置
- &common 创建了一个 YAML 锚点(anchor),为共享配置指定了一个可复用的名称
- <<: *common 将锚点引用的设置合并到各个服务中,直接在服务下定义的配置值会覆盖共享配置值
- max-size 和 max-file 将每个日志文件限制为 10 MB 并最多保留三个文件,从而防止容器日志占用过多磁盘空间
保存文件并退出编辑器,然后,让 Compose 生成最终的配置:
# docker compose config

虽然我们只定义了一次服务设置,但输出显示两个服务均包含”restart: unless-stopped”和相同的日志配置。
2. 使用”depends_on”的”service_healthy”条件按顺序启动服务
既然服务已共享通用配置,接下来的问题就是启动顺序。仅使用基础的”depends_on”只能保证数据库容器在应用程序之前启动。
但它并不会等待 PostgreSQL 准备好接受连接,因此应用程序在启动过程中仍可能因”连接被拒绝”(connection refused)错误而失败。
再次打开”compose.yaml”,添加一个包含健康检查(healthcheck)的”db”服务,并让”web”服务依赖于它的健康状态:
# /home/rultr/webapp/compose.yaml
services:
db:
image: postgres:17
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
retries: 10
start_period: 10s
web:
<<: *common
build: .
ports:
- "8080:8000"
depends_on:
db:
condition: service_healthy
restart: true
关键配置如下:
- test 运行 pg_isready,当 PostgreSQL 准备好接受连接时,该命令会报告成功
- interval 每 5 秒检查一次数据库
- retries 允许最多连续 10 次检查失败,超过此次数后容器将被视为不健康
- start_period 为 PostgreSQL 提供 10 秒的初始化时间,在此期间健康检查失败不会计入重试次数限制
- condition: service_healthy 指示 Compose 仅在数据库健康检查成功后才启动 web 服务
- restart: true 指示 Compose 在显式重启 db 服务时,也重启依赖它的 web 服务
${DB_PASSWORD} 的值从同一项目目录下的 .env 文件中读取。保存该文件,然后使用 –wait 参数启动该服务栈:
# docker compose up -d --wait
“–wait”选项会让 Compose 等待服务进入运行或健康状态,然后再将控制权交还给 Shell。
如果数据库的健康检查始终无法通过,Compose 会报告依赖项失败,而不是允许”web”服务在数据库不可用的情况下启动;这样一来,我们面对的是明确的健康检查失败问题,便于排查,而不是应用程序陷入崩溃重启的循环。
3. 使用 Profile 按需运行可选服务
在核心服务栈顺利启动后,接下来要处理的是可选服务。例如,我们可能需要使用 Adminer 来排查数据库问题,但没有理由一直运行它的 Web 界面。
打开”compose.yaml”文件,并在”services”下添加以下服务:
# /home/rultr/webapp/compose.yaml
adminer:
image: adminer
ports:
- "8081:8080"
profiles: ["debug"]
配置中的 profile 设置将 Adminer 指定给了”debug” profile;因此,Compose 会跳过那些分配给未激活 profile 的服务,而未指定 profile 的服务则始终会启动。
因此,在正常启动时,Adminer 处于停止状态:
# docker compose up -d
当需要使用数据库管理界面时,启用该 profile 即可:
# docker compose --profile debug up -d
我们也可以通过”COMPOSE_PROFILES”环境变量来启用 profile。例如,在”.env”文件中添加以下内容即可启用”debug” profile,而无需在命令行中指定参数:
# COMPOSE_PROFILES=debug
这样既能避免将偶尔使用的排错工具包含在常规服务栈中,又能确保在需要时只需一条命令即可启用它们。
4. 使用 include 拆分大型 Compose 文件
随着”compose.yaml”文件不断变大,将所有服务都放在同一个文件中会变得难以管理和浏览;利用”include”元素,就可以将相关的服务拆分到独立的 Compose 文件中,同时仍将其视为同一项目的一部分。
创建一个”db”目录并新建一个 Compose 文件:
# mkdir -p /home/rultr/webapp/db && vi /home/rultr/webapp/db/compose.yaml
将上一个示例中的 db 服务移至此文件:
# /home/rultr/webapp/db/compose.yaml
services:
db:
image: postgres:17
env_file: db.env
volumes:
- ./data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
retries: 10
被包含的 Compose 文件中的路径是相对于该文件进行解析的。因此,”db.env”和”./data”分别指向”/home/rultr/webapp/db/db.env”和”/home/rultr/webapp/db/data/”。
现在,将以下内容添加到主”compose.yaml”文件中,并移除其中原有的”db”服务:
# /home/rultr/webapp/compose.yaml include: - db/compose.yaml
现有的”depends_on”配置可以继续引用包含进来的”db”服务:
depends_on: db: condition: service_healthy
Compose 会将包含进来的配置合并到项目模型中,因此即使”web”服务和”db”服务定义在不同的文件中,”web”服务仍然可以依赖”db”服务。
进行更改后,请验证生成的配置:
# docker compose config
如果不小心保留了另一个具有相同项目级名称的”db”服务,Compose 会报告冲突,而不会静默选择其中一个配置。
5. 使用 develop.watch 将代码实时同步到容器中
拆分 Compose 配置文件有助于保持项目条理清晰,但如果每次修改代码后都必须重新构建 Web 镜像,开发过程仍会变得繁琐重复。
Compose Watch 能够将本地更改直接同步到正在运行的容器中。打开”compose.yaml”文件,并在”web”服务中添加”develop”部分内容:
# /home/rultr/webapp/compose.yaml
develop:
watch:
- action: sync
path: ./app
target: /app
ignore:
- __pycache__/
- action: rebuild
path: requirements.txt
这两个 watch 动作各有不同的用途:
- action: sync —— 将 ./app 中的更改同步到 /app,而无需重新构建镜像
- ignore —— 排除 Python 字节码缓存,不进行同步
- action: rebuild —— 当 requirements.txt 发生变化时重新构建镜像,确保安装新添加的依赖项
保存文件并启动 Compose Watch:
# docker compose watch
可以看到,Compose Watch 专为开发工作流设计,因此请仅在实际修改应用程序源代码的机器上保留此配置。
6. 使用 !reset 和 !override 移除继承的值
在生产环境部署中,通常会在基础配置之上叠加第二个 Compose 文件。然而,Compose 默认会将来自多个文件的配置值进行合并。例如,添加新的端口(ports)条目并不会移除基础文件中定义的端口映射。
Compose 提供了 !reset 和 !override YAML 标签,用于显式控制这一行为。
创建生产环境的覆盖文件:
# /home/rultr/webapp/compose.prod.yaml
services:
web:
ports: !override
- "127.0.0.1:8080:8000"
adminer:
ports: !reset []
这些标签的行为各不相同:
- !override 会完全替换基础 Compose 文件中的值。在此例中,web 服务仅发布 127.0.0.1:8080,而不会将生产环境的端口与现有端口合并
- !reset [] 会移除继承的值。在此例中,Adminer 发布的端口被清除,因此该服务不会在生产环境中通过宿主机端口对外暴露
保存文件并查看合并后的配置:
# docker compose -f compose.yaml -f compose.prod.yaml config
7. 将配置文件内容直接嵌入到”configs”中
由于 Web 服务现在仅监听 “localhost”,因此该技术栈需要一个反向代理来接收外部请求。利用 Docker Compose 顶层的”configs”元素,就可以将 Nginx 配置直接写在”compose.yaml”文件中,而无需维护单独的配置文件。
打开”compose.yaml”并添加以下配置:
# /home/rultr/webapp/compose.yaml
configs:
nginx_conf:
content: |
server {
listen 80;
location / {
proxy_pass http://web:8000;
proxy_set_header Host $$host;
}
}
services:
proxy:
image: nginx:stable
ports:
- "80:80"
configs:
- source: nginx_conf
target: /etc/nginx/conf.d/default.conf
该配置的工作原理如下:
- content 字段将 Nginx 配置直接存储在 Compose 文件中;当服务启动时,Compose 会将其提供给容器
- source 引用了已命名的 Compose 配置,而 target 则指定了该配置在容器内的存放位置
- proxy_pass 指令将来自 Nginx 的传入请求转发至端口 8000 上的 Web 服务
Nginx 配置中有一个重要细节:Compose 会对 content 字段中的内容进行变量插值处理,因此 $host 会被解析为 Compose 变量。若将其写为 $$host,则会对美元符号进行转义,从而确保 $host 原样传递给 Nginx。
如果不使用 $$,Compose 可能会发出警告,提示 host 变量未设置并将其替换为空值,进而导致 Nginx 配置无效或出错。
以上这些功能无需额外工具即可解决常见问题,综合来看,这些功能使 Compose 项目更易于维护、部署更安全,并能提升开发效率。