Docker Compose存在一个认知上的问题。如果你问一群平台工程师对它的看法,你很可能会听到类似这样的回答:“它非常适合本地开发使用,但我们在实际工作中会使用Kubernetes。”

我理解这种观点。多年来我也一直持这种看法。Compose只是我在笔记本电脑上用来创建Postgres数据库的工具,并不认为它可以用于测试环境,更不用说那些需要GPU支持的工作负载了。

然而,2024年和2025年发生了变化。Docker推出了一系列功能,这些功能让Compose从一个仅为开发者提供便利的工具,变成了能够处理复杂部署场景的工具。配置文件允许你通过一个文件来管理多个环境;“观察模式”彻底解决了导致基于容器的开发效率低下的重建循环问题;GPU支持为机器学习工作负载打开了大门;而一系列其他的小改进(如更完善的健康检查机制、与Bake工具的集成、结构化的日志记录功能)也弥补了之前让Compose显得不够强大的那些缺陷。

在这篇文章中,我将介绍以下内容:如何使用Docker Compose配置文件来管理多个环境;如何设置“观察模式”以实现开发过程中的实时代码同步;如何为机器学习工作负载配置GPU支持;如何正确配置健康检查机制和启动顺序,以避免服务在初次启动时出现故障;以及如何利用Bake工具来衔接本地开发流程与生产环境的构建过程。同时,我也会指出Compose仍然存在的不足之处,以及在这些情况下应该选择其他工具来使用。

先决条件

你应当已经熟悉Docker的基本知识,并且之前曾经编写过compose.yaml文件。你需要安装Docker Compose v2版本。具体需要哪个最低版本,取决于你想要使用哪些功能:service_healthy依赖条件要求使用v2.20.0+版本,“观察模式”需要v2.22.0+版本,而gpus:配置选项则要求使用v2.30.0+版本。你可以运行docker compose version命令来检查自己当前安装的是哪个版本。

目录结构

现代的Compose文件:发生了哪些变化

如果你最近没有查看过Compose文件,首先会注意到version字段已经消失了。Docker Compose v2完全忽略了这个字段,如果仍然包含它,系统会发出弃用警告。现代的compose.yaml文件直接从描述服务的内容开始,不需要任何前置说明。

但结构上的变化远不止这些。对于典型的Web应用程序栈来说,现代的、适用于生产环境的Compose文件应该如下所示:

services:
  api:
    image: ghcr.io/myorg/api:${TAG:-latest}
    env_file: [configs/common.env]
    environment:
      - NODE_ENV=${NODE_ENV:-production}
    ports:
      - "8080:8080"
    depends_on:
      db:
        condition: service_healthy
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "1.0"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 10s
      timeout: 5s
      retries: 3

  db:
    image: postgres:16-alpine
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      retries: 5

volumes:
  db-data:

看看其中包含的内容:资源限制、带有依赖条件的健康检查机制、合理的卷管理功能。这些并不是可有可无的功能,它们正是让Compose能够在笔记本电脑之外也能正常使用的关键要素。

尤其是健康检查功能,它解决了Compose最古老也是最令人困扰的问题之一:即Web服务器在数据库尚未准备好接受连接之前就启动了。如果你曾经在启动脚本中添加过sleep 10这段代码,并且祈祷它能正常工作,那你肯定能理解我的意思。

如何使用配置文件来管理多种环境

正是这个功能彻底改变了我对Compose的使用方式。在配置文件出现之前,管理不同环境意味着必须在两种繁琐的方法之间做出选择:要么维护多个Compose文件(如docker-compose.ymldocker-compose.dev.ymldocker-compose.test.ymldocker-compose.prod.yml),并处理它们之间不可避免的差异;要么使用一个庞大的文件,在其中根据不同的环境需求来注释掉某些服务的配置。这两种方法都存在缺陷,而且都会导致“在我的机器上可以正常运行,但在其他环境中却不行”这类问题。

配置文件为人们提供了一条更加简洁的解决方案。你可以将服务分配到不同的组中:没有配置文件的 service 会始终被启动;而带有配置文件的 service 只有在明确激活了相应的配置文件后才会被启动。你也可以通过环境变量COMPOSE_PROFILES来激活配置文件,而无需使用命令行参数,这对于持续集成流程来说非常方便(具体语法请参阅官方文档)。

下面是一个具体的示例:

services:
  api:
    image: myapp:latest
    # 没有配置文件 = 始终被启动

  db:
    image: postgres:16
    # 没有配置文件 = 始终被启动

  debug-tools:
    image: busybox
    profiles: [debug]
    # 只有在使用--profile debug参数时才会被启动

  prometheus:
    image: prom/prometheus
    profiles: [monitoring]
    # 只有在使用--profile monitoring参数时才会被启动

  grafana:
    image: grafana/grafana
    profiles: [monitoring]
    depends_on: [prometheus]

现在,你们的团队可以使用简单且容易记住的命令来开展工作:

# 开发环境:仅使用核心服务栈
docker compose up -d

# 带有监控功能的开发环境
docker compose --profile monitoring up -d

# 持续集成环境:仅使用核心服务栈(不包含监控功能)
docker compose up -d

# 完整的服务栈环境,支持调试功能
docker compose --profile debug --profile monitoring up

只需要一个 Compose 文件,无需担心配置冲突,也无需猜测应该使用哪个配置文件。

我实际使用过的配置模式

有四种模式是我经常使用的:

“仅基础设施”模式。这种模式适用于那些在本地主机上运行应用程序代码,但需要将数据库、消息队列和缓存等基础设施服务放在容器中的开发人员。在这种情况下,可以将基础设施服务配置放在单独的配置文件中,而将应用程序服务配置放在另一个文件中。后端开发人员只需运行 docker compose up 命令,就可以获取 Postgres 和 Redis 等服务,然后直接在本地主机上启动应用程序,并使用他们喜欢的调试工具进行测试。

“模拟环境与真实环境”模式。可以在 dev 配置文件中配置 payments-mock 服务,在 prod 配置文件中配置真实的支付网关服务。使用同一个 Compose 文件,根据不同的配置环境,系统的行为会完全不同。这种模式多次帮助我的团队避免了在开发过程中不小心调用真实支付 API 的情况。

“持续集成优化”模式。像 Selenium 浏览器这样的复杂服务以及监控相关组件可以被放在单独的配置文件中,这样持续集成流程就可以跳过这些部分。这样一来,测试套件的运行速度会更快,而且只有在进行端到端集成测试时才会加载这些服务。

“人工智能/机器学习工作负载”模式。依赖 GPU 的服务(如推理服务器、模型训练容器)可以被放在 gpu 配置文件中。没有 GPU 的开发人员也可以继续使用其他服务,而不会遇到任何问题。

有一个实用的建议可以帮你们避免很多麻烦:在项目的 README 文件中详细说明各种配置文件的用途。这听起来似乎很显而易见,但当新成员运行 docker compose up 命令时,如果他们不明白为什么监控面板无法启动,那么这个文档就能为他们提供答案。只需制作一个表格,列出每个配置文件及其包含的内容,就可以避免每次新成员加入团队时都有人重复提出同样的问题。

如何使用监视模式来快速结束重建过程

如果配置文件解决了环境管理方面的问题,那么监视模式则解决了开发人员在使用这些工具时的体验问题。

你们可能熟悉传统的基于容器的开发工作流程:修改代码后,运行 docker compose build 命令构建容器镜像,然后运行 docker compose up 启动容器进行测试。如果发现错误,就需要再次修改代码并重新构建容器,之后再重新进行测试。每次这样的循环都会耗费 30 秒到 1 分钟的时间。在一天繁忙的开发工作中,仅仅因为等待构建日志的滚动而浪费的一个小时甚至更长时间,实在是非常可惜的。

监视模式(在Compose 2.22.0版本中首次引入,并在后续版本中得到了显著改进)会监控您的本地文件,当这些文件发生变化时,它会自动采取相应的行动。该模式支持三种同步策略,为不同情况选择合适的策略是确保其正常运行的关键。如果您想深入了解相关细节,官方的监视模式文档提供了完整的规范说明。

sync命令会将发生变化的文件直接复制到正在运行的容器中。这种同步方式对于Python、JavaScript、Ruby这类解释型语言,以及React、Vue、Next.js这类支持热模块重载的框架来说效果最佳。文件一旦被复制到容器中,相关框架就会立即检测到这些变化,用户的浏览器也会随之更新内容——无需重新构建应用程序或重启容器。不过,如果您使用的是Go、Rust或Java等需要编译的语言,sync命令就无法发挥作用,因为这些语言的代码需要重新编译才能生效。在这种情况下,请使用rebuild命令。

rebuild命令会触发整个镜像的重建以及容器的重启。当您修改了package.jsonrequirements.txt等依赖配置文件,或者直接修改了Dockerfile本身时,就需要使用这个命令。因为在这种情况下,仅仅同步文件是远远不够的,必须重新生成一个新的镜像才能确保应用程序能够正常运行。

sync+restart命令会先将文件复制到容器中,然后再重启主进程。这种模式非常适合用于配置文件的修改,比如调整nginx.conf或数据库配置文件。在这种情况下,应用程序需要重新加载才能应用新的设置,但镜像本身并不需要被重新生成。

以下是一个针对Node.js应用程序的实际监视配置示例:

services:
  api:
    build: .
    ports: ["3000:3000"]
    command: npx nodemon server.js
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
          ignore:
            - node_modules/
        - action: rebuild
          path: package.json
        - action: sync+restart
          path: ./config
          target: /app/config

您可以通过docker compose up --watch命令来启动这个监视模式,或者单独运行docker compose watch命令,这样就可以将文件同步事件与应用程序的日志分开处理。

在设置监视模式之前,有几点需要注意:该模式仅适用于那些具有本地build:配置的服务。如果您是从注册中心拉取预构建的镜像,那么Compose就没有什么可以同步或重建的内容了,因此监视模式会忽略这类服务。此外,您的容器还需要安装一些基本的文件操作工具(如statmkdir),并且容器的USER账户必须具有对目标路径的写入权限。如果您使用的是像scratchdistroless这样的最小化基础镜像,sync命令也是无法正常使用的。最后,如果您使用的是较旧版本的Compose,请先确认哪些同步操作是受支持的:sync+restartsync+exec这两个命令是在初始版本2.22.0之后才在后续的次要版本中添加的。

这是一个巨大的改进。修改源文件并保存后,对于支持热重载的框架来说,更改几乎会在一秒钟内就生效。无需切换上下文来执行构建命令,也无需等待,只需编写代码即可。

观察模式与绑定挂载

你可能会问一个合理的问题:多年来,绑定挂载就已经提供了一种实时重载的功能,那么为什么还需要观察模式呢?

虽然绑定挂载确实有效,但它们存在一些特定于平台的问题,这些问题长期以来一直困扰着Docker Desktop。在macOS和Windows系统中,绑定挂载需要通过主机操作系统与运行Docker的Linux虚拟机之间的文件系统共享层来工作。这种机制会导致权限设置上的问题、在处理大型目录时会出现性能瓶颈(你是否曾经见过macOS系统下的node_modules文件夹导致绑定挂载无法正常工作?),而且文件通知机制的不稳定性也会使热重载功能变得不可靠。

观察模式通过在应用程序层面直接同步文件来规避这些问题。它的运行更加稳定,能够在不同平台上保持一致的行为,并且让你能够更好地控制文件发生变化时会发生什么。

不过,对于许多使用场景来说,绑定挂载仍然非常适用,尤其是在使用原生Linux系统的环境中,因为这种情况下不会存在性能上的开销。而对于那些遇到了跨平台问题的团队来说,或者那些需要自动重建和重启功能的人来说,观察模式才是更好的选择。

如何为机器学习工作负载配置GPU支持

正是这个功能让我重新思考了Compose所能实现的功能。

多年来,Docker一直通过NVIDIA Container Toolkit以及--gpus参数支持单个容器的GPU直通功能。但在Compose配置文件中设置GPU访问权限时,过去往往需要使用一些繁琐的运行时声明方式,这些声明方式的文档资料很少,而且还会随着Compose版本的更新而发生变化。有时候你可能会找到2021年发布的Stack Overflow答案,尝试按照它来操作,却发现后来已经不再适用了。

现代版的Compose规范通过deploy.resources.reservations_devices块来简洁地解决这个问题:

services:
  inference:
    image: myorg/model-server:latest
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]

如果你使用的是Compose v2.30.0或更高版本,还可以使用更简洁的gpus:字段来配置:

services:
  inference:
    image: myorg/model-server:latest
    gpus:
      - driver: nvidia
        count: 1

这两种方法的作用是相同的。deploy.resources语法在较早期的Compose版本中也同样适用,并且能让你拥有更多的控制权(比如可以通过设置device_ids来指定使用特定的GPU)。而当只需要简单的GPU访问功能时,gpus:这种简写方式会更加方便。

如果你忽略了这个配置步骤,很可能会遇到问题:在所有这些设置生效之前,你的主机机器必须安装了正确的GPU驱动程序以及。首先在主机上运行nvidia-smi命令,如果该命令无法显示你的GPU信息,那么Compose也无法识别它们。对于需要使用CUDA的工作负载来说,应该使用官方的GPU基础镜像,比如或者PyTorch/TensorFlow对应的GPU镜像。Compose GPU访问配置指南会为你提供详细的设置步骤。

就是这么简单。当你运行 `docker compose up` 时,推理服务会使用一块 NVIDIA GPU。如果你希望所有可用的 GPU 都被分配给该服务,可以将 `count` 设置为 `”all”`;或者通过 `device_ids` 来指定将特定的 GPU 分配给特定的服务。

如何结合配置文件来使用多 GPU 资源

在这里,配置文件与 GPU 支持的功能就能发挥出极大的作用。以一个机器学习工作负载为例:你需要一个用于文本生成的 LLM、一个用于向量搜索的嵌入模型,以及一个向量数据库:

services:
  vectordb:
    image: milvus/milvus:latest
    # 在 CPU 上运行,因此不需要配置文件

  llm-server:
    image: ollama/ollama:latest
    profiles: [gpu]
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              device_ids: ["1"]
              capabilities: [gpu]
    volumes:
      - model-cache:/root/.ollama

  embedding-server:
    image: myorg/embeddings:latest
    profiles: [gpu]
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              device_ids: ["0"]
              capabilities: [gpu]

没有 GPU 的开发人员只需使用 `docker compose up` 即可开始编写应用程序逻辑。向量数据库会正常启动,他们也可以通过其 API 进行编程开发,一切都会顺利进行。当需要测试整个机器学习流程时,拥有多 GPU 工作站的人员就可以运行 `docker compose --profile gpu up`,这样系统就会使用指定的 GPU 资源来运行。

这种模式已经成为我们 AIOps 平台开发的核心。负责构建警报逻辑的团队并不需要 GPU,而那些用于训练异常检测模型的团队则确实需要 GPU。同一个 Compose 文件就可以同时满足这两支团队的需求。

如何配置健康检查、依赖关系以及启动顺序

Compose 中一个被人们忽视但非常实用的功能就是它对服务依赖关系的处理方式。`depends_on` 指令现在支持那些具有实际意义的条件设置(这需要使用 Compose v2.20.0 及更高版本,具体细节请参阅 启动顺序配置文档):

depends_on:
  db:
    condition: service_healthy
  redis:
    condition: service_started

当你将这一机制与适当的健康检查结合使用时,就能避免许多 Compose 配置中普遍存在的“等待 10 秒再尝试”的问题。你的 API 服务会等到 PostgreSQL 确实开始接受连接请求后才会尝试启动——不仅仅是确保容器已经运行起来,还要保证其中的数据库进程通过了健康检查。

有一点需要注意:请仔细调整 `start_period` 的值。像 PostgreSQL 这样的数据库在首次启动时需要一定的时间来初始化,尤其是当它们正在执行数据迁移操作时。如果没有设置 `start_period`,健康检查会立即开始重试检测,从而导致服务在还未完成启动流程就被判定为“不健康”。对于大多数数据库服务来说,这样的配置都是非常合适的。

healthcheck:
  test: ["CMD-SHELL", "pg_isready -U postgres"]
  interval: 5s
  timeout: 2s
  retries: 10
  start_period: 30s

start_period为容器提供了30秒的缓冲时间,在这段时间内,失败的健康检查不会被计入重试次数中。

这看似只是一个细节,但如果你曾经开发过由八到十个相互关联的服务组成的系统,你就会知道在系统初次启动时,调试这些服务之间的连锁故障会浪费多少时间。正确的启动顺序能够避免这些问题,使你的本地开发环境更接近生产环境的实际运行状态。

如何使用Bake来构建生产环境的镜像

我之前提到过Bake的集成功能,它确实值得单独介绍,因为一旦你开始在本地开发之外使用Compose,就会遇到这样一个问题:你的开发用Compose配置文件与生产环境下的构建流程有着不同的需求。

在开发阶段,你需要快速构建镜像、利用本地缓存,并生成适用于单一平台的镜像;而在生产环境中,则需要将带有标签的镜像推送到注册中心、进行多平台构建,并确保构建过程的可验证性。如果试图将这两种需求都融合到同一个compose.yaml文件中,很快就会导致配置混乱。

Docker Bake(docker buildx bake)能够读取你的compose.yaml文件并据此生成构建目标,但你也可以通过单独的docker-bake.hcl文件来覆盖或扩展这些配置。这样既能保持开发工作流程的整洁性,又能让持续集成系统获得所需的配置选项。Bake的相关文档详细介绍了HCL语法以及与Compose的集成方式。

以下是一个简单的docker-bake.hcl示例:

group "default" {
  targets = ["api", "worker"]
}

target "api" {
  context    = "api"
  dockerfile = "Dockerfile"
  tags       = ["registry.example.com/team/api:release"]
  platforms  = ["linux/amd64"]
}

target "worker" {
  context    = "worker"
  dockerfile = "Dockerfile"
  tags       = ["registry.example.com/team/worker:release"]
}

这样,你的持续集成管道会执行docker buildx bake来生成发布用镜像,而开发人员则可以在本地继续使用docker compose up --build进行构建。这两种工作流程虽然使用相同的Dockerfile,但在需要时会有不同的配置设置。

我总结出的最佳实践是:在本地开发和持续集成测试环境中使用Compose,在持续集成过程中使用Bake来生成发布镜像,然后将这些镜像推送到团队所使用的部署目标上(比如预发布服务器、Kubernetes集群或边缘节点)。Compose能帮助你快速将代码转化为可运行的容器;而Bake则能确保生成的镜像带有正确的标签,并具备必要的验证信息,从而使其可以直接用于生产环境。

Compose的局限性

在这篇文章中,我一直在强调Compose已经发展得非常成熟了。但我也应该告诉你它有哪些不足之处。最好现在就由我来告诉你这些,而不是让你在生产环境中亲身体会到它们带来的麻烦。

Compose并不是一种用于管理容器部署的工具。它不会在多台主机之间调度任务,也不会自动进行故障转移。它无法实现零停机时间的滚动更新功能,也不支持服务网格网络技术。如果你需要这些功能,那么Kubernetes、Nomad或Docker Swarm才是更合适的选择。

Compose并不能替代Helm或Kustomize。如果你正在使用Kubernetes进行部署,Compose文件并不能直接转换为Kubernetes所需的配置文件。虽然Docker提供了Compose Bridge来帮助转换这些文件,但该工具仍处于测试阶段,无法处理一些复杂的Kubernetes特定配置,比如自定义资源定义或入口规则等。

在生产环境中,Compose在处理敏感信息方面表现不佳。尽管Compose提供了相关功能,但其安全性相比HashiCorp Vault、AWS Secrets Manager或Kubernetes自带的秘密管理机制仍然有限。对于非测试环境而言,你显然需要使用外部 secret 管理工具。

现代环境中,Compose最适合用于本地开发环境、CI/CD测试环境、单节点测试环境,以及那些适合使用高性能机器进行部署的工作负载场景(尤其是需要GPU的计算任务)。在这些范围内,Compose确实非常有用;但一旦超出这些范围,使用Compose就会遇到诸多问题。

如果你确实在测试环境或单节点生产环境中使用Compose,还有几项配置是值得添加的:为每个服务设置restart: unless-stopped选项,这样在主机重启后容器能够自动重新启动;配置日志记录机制,确保日志能够被妥善保存而不会消失在docker logs中;同时还要为命名卷制定备份策略。这些虽然不是Compose特有的问题,但Compose也无法为你解决它们。

实际应用建议

如果你目前使用的是基础的Compose配置,并且想要开始利用这些高级功能,以下是我推荐的步骤顺序。每个步骤都是逐步进行的,而且相互兼容,单独执行也是很有意义的。你不需要一次性完成所有这些配置。

第1周:添加健康检查机制以及正确的depends_on条件设置。仅做这些就能有效解决最常见的问题:由于依赖项尚未准备好,导致服务在启动时崩溃。首先从数据库服务和主应用服务开始配置condition: service_healthy条件,你会立刻看到效果的变化。

healthcheck:
  test: ["CMD-SHELL", "pg_isready -U postgres"]
  interval: 5s
  timeout: 2s
  retries: 10
  start_period: 30s

第2周:引入配置文件模板。将监控工具配置放在monitoring模板中,调试工具配置放在debug模板中,然后删除那些多余的Compose配置文件。使用统一的配置文件模板,而不是四份内容几乎相同但又不完全一致的文件,会让管理变得更加简单。

第3周:为您最常编辑的服务启用监控模式。选择那些开发人员花费最多时间进行迭代修改的服务,首先在这些服务上启用监控模式。一旦团队看到实际效果——保存文件后变化能在不到一秒的时间内显现出来——他们就会要求在其他所有服务上也使用这种功能。

第4周:设置资源限制。为每一项服务设定内存和CPU的使用上限。这样就可以防止某个容器因资源消耗过多而影响其他容器的正常运行,同时也能让您真实地了解这些服务在实际生产环境中的表现。此外,这一措施还有助于及时发现内存泄漏问题。

deploy:
  resources:
    limits:
      memory: 512M
      cpus: "1.0"

总结

2026年的Docker Compose已经不再是几年前的那个工具了。配置文件、监控模式、对GPU的支持、完善的依赖管理功能以及与Bake工具的集成,使得它能够处理那些规模较为复杂的工作负载——只要这些工作负载能够在单个节点上运行即可。

它虽然不是Kubernetes,但也不应该试图成为Kubernetes的替代品。然而对于本地开发、持续集成流程、测试环境以及单台机器上的GPU相关任务来说,Docker Compose已经成为了不可或缺的工具。如果您曾经因为它的过去而忽视了它,那么现在的这个版本确实值得您重新考虑一下。

如果这篇文章对您有帮助,您可以在我的博客上找到我关于DevOps、容器技术以及自动化运维最佳实践的更多文章。

Comments are closed.