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 项目更易于维护、部署更安全,并能提升开发效率。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注