Docker Compose 很容易因为错误理由被采用。团队看到一个 YAML 文件、两条命令和一个能跑起来的本地栈,随即把 Compose 当作玩具,或者当作小号 Kubernetes。两种理解都错过了它最强的工作形态。Compose 最适合把 compose.yaml 放在应用的本地合约位置上:有哪些服务,它们如何构建或拉取,哪些端口和卷值得关心,哪些依赖必须先达到健康状态,哪些辅助服务只是可选项,以及这条边界停在哪里。
这条边界如今比旧式 docker-compose.yml 习惯所暗示的更明确。Compose Specification 把应用定义为由网络、卷、配置和 secrets 连接起来的一组服务,同时提醒不同实现会支持不同属性,并且应按模式对不支持的属性给出警告或拒绝。[1] Docker 的服务参考同样说明问题:Compose 文件必须声明顶层 services map,每个服务可以使用镜像和运行时参数,build 是可选项;如果平台没有实现 deploy,对 deploy 的支持也是可选的。[2] 文件没有魔法。它是一份模型,由某个具体实现把它转换成容器。
因此,这是一份采用说明,而不是对更简单 YAML 的喝彩。当团队需要可重复的本地栈、范围很窄的自托管服务、CI 冒烟测试环境,或者希望把开发者入门流程从 README 中搬出来时,Compose 是好的迁移目标。当团队真正需要调度、滚动更新、自动扩缩容、多节点服务发现或集群策略时,它就是弱目标。只要文件诚实保留这一区分,Compose 就经得起使用;一旦它开始把生产编排愿望藏进本地默认值里,缩进会变成技术债。
迁移栈,而不是迁移部署平台
第一个迁移问题不该是“这能不能在 Compose 下运行?”大多数容器化应用总能想办法启动。更好的问题是,这个栈的形状是否小到足以让单一应用模型帮助人理解它。一个 Web 服务、worker、数据库、缓存、对象存储模拟器、迁移任务和调试 UI,可以放进同一个 compose.yaml。一组按区域扩展的工作负载、发布策略、身份 sidecar、入口控制器和 autoscaler,通常放不进去。
Compose 的服务模型应被读作一组所有权声明。services.web.build.context 表明镜像来自本地源码。services.db.image 表明数据库是运行时依赖,不属于应用构建的一部分。ports 说明哪些主机可见入口是有意暴露的。volumes 把可丢弃容器和在 docker compose down 之后仍要保留的数据分开。configs 和 secrets 让配置与敏感值在模型里成为一等概念,即便某个本地实现提供它们的方式不同于 swarm 或云平台。[1][2]
这就是迁移价值。新开发者打开文件,就应能看懂系统的本地依赖图,而不用先读五页 wiki。CI 应能用同一套依赖形状启动集成测试。运行小型内部工具的运维人员,在碰升级之前,应能看见状态存放在哪里。Compose 应把“按这个顺序运行这些命令”替换成有版本记录的栈合约。
反模式是超大文件。compose.yaml 一旦成为所有生产愿望的倾倒处,没人还知道哪些部分是真正在用的。第一份文件应保持朴素:应用服务、本地依赖、命名卷、健康检查和显式端口。可选工具放到 profiles 后面。大型子域只有在所有权边界已经清楚时,才放到 include 后面。Compose 迁移成功的标志,是文件随时间变得更易读,而不是积累 Docker 能解析的每一个旋钮。
健康检查划出顺序与就绪之间的差别
最常见的 Compose 错误,是把启动顺序当成就绪状态。Docker 关于启动顺序的文档把这一区分讲得很直接。depends_on 可以表达 service_started、service_healthy 或 service_completed_successfully;标记为 service_healthy 的依赖必须先通过自己的 healthcheck,依赖它的服务才会被创建。[3] 官方示例使用 PostgreSQL 和 pg_isready,间隔为 10 秒,重试 5 次,启动宽限期为 30 秒,超时为 10 秒,随后才允许 web 面向 db 启动。[3]
这个例子不只是语法。它是一条可靠性边界。如果 Web 应用在 Postgres 接受连接之前就启动,故障通常会出现在应用日志里。如果迁移容器因为 Redis 还在启动而提前退出,故障会被看成脚本问题。如果测试套件与数据库初始化路径赛跑,开发者会学会重跑测试,修正系统反而被推迟。带有 condition: service_healthy 的 depends_on 把这种时序假设写进配置。
也不要夸大它。Compose 健康检查不能替代应用层重试、数据库迁移纪律或生产就绪探针。它是一份本地合约,表达的是“在这条命令成功之前,这个服务不应被邻近服务视为可用”。这足以移除大量入门和 CI 抖动,也让真实依赖显形。如果 api 离开 db、redis 和 migrate 就启动不了,YAML 应把这个事实写出来,不能把它交给团队记忆。
最重要的一条规则,是检查客户端真正需要的东西。对 Postgres 来说,pg_isready 好过“容器进程存在”。对 HTTP 服务来说,一个小的 /healthz 端点好过“端口已打开”。对一次性 setup 容器来说,service_completed_successfully 比让应用自己发现 setup 没有发生更诚实。[3] Compose 在这里不提供生产控制平面;它提供的是可读的本地失败边界。
Profiles 让可选工具免于变成必选服务
Profiles 是 Compose 在保持紧凑时仍能诚实表达差异的位置。Docker 的 profile 指南说明,服务可以通过 profiles 属性分配到 profile 中;没有 profiles 属性的服务始终启用。它自己的提示就是实用规则:核心应用服务不应分配 profile,因此它们始终启用并自动启动。[4]
这条规则应积极使用。数据库、消息代理和应用进程多半属于核心部分。数据库管理 UI、Mailpit、Jaeger、本地 S3 浏览器、假支付网关、负载生成器或基准测试 worker,多半属于可选项。把可选工具放在 debug、observability、admin 或 bench 这类名称之下,再要求开发者用 --profile 明确启用。
这样可以减少两类混乱。第一,docker compose up 保持为最小可用栈。第二,辅助工具不会看起来像生产依赖。如果某个服务只是因为某个团队偶尔需要才存在,把它藏在 profile 后面,比让每台笔记本都启动它、让每位新工程师都猜应用是否依赖它要清楚。
Profiles 也能降低成本和噪音。本地观测栈可以有价值,但不是每个分支都需要它。浏览器自动化 sidecar 对端到端测试有用,却不该陪着一个 CSS bug 编辑过程启动。采用 Compose 应降低默认认知负担。Profiles 是少数能让单个文件服务多个工作流,同时不把默认工作流变成队列的功能之一。
Watch 和 include 是强力工具,不是借口
Compose Watch 是一个有用信号,说明这个项目已经越过“启动几个容器”的阶段,仍在继续演进。Docker 的 Watch 文档说,watch 属性在 Compose 2.22.0 及之后版本可用,可以随着代码变更更新运行中的服务;规则可以依据路径、目标、忽略项和初始同步行为执行 sync、rebuild 或 restart。[5] 文档也划出重要边界:Watch 面向用 build 从本地源码构建的服务设计;如果服务只依赖预构建的 image,它不会跟踪变更。[5]
这个范围正合适。Watch 对本地开发循环最有用,尤其在 bind mount 太粗的时候。同步源文件、忽略 node_modules/,再在 package.json 变化时重建,比挂载整个项目、期待主机和容器对原生构件意见一致要更像合约。[5] 它让 Compose 文件描述代码如何进入容器,而不只是描述某个目录被挂载。
include 解决的是另一个问题。Docker 的 include 文档说,它要求 Compose 2.20.0 及之后版本;它会把另一个 Compose 文件作为带有自身项目目录的应用模型加载,把资源定义复制到当前模型中,对资源名冲突发出警告,并且不会尝试合并冲突资源。[6] 这是伪装成便利性的治理功能。
当某个子域有自己的负责人时,使用 include:共享观测 sidecar、通用本地身份服务、可复用测试依赖,或平台提供的模拟器。不要用它隐藏没人想维护的复杂性。被 include 的文件保留自己的相对路径和环境默认值,这一点有用,因为它保留了所有权边界。[6] 冲突只警告而不自动合并,这一点也有用,因为它阻止两个团队悄悄以不同方式定义同一个资源。[6]
保守规则很简单:watch 应让编辑运行循环更快,include 应让所有权更清楚。任何一个功能让人更难预判 docker compose up 会做什么时,文件就从合约走进了迷宫。
知道编排边界何时到来
Compose 不该被拉伸成调度器,因为调度器本来承担另一种工作。以 Kubernetes Deployments 为例,它描述 Pods 和 ReplicaSets 的期望状态;Deployment controller 会按设定节奏把实际状态改向期望状态,在更新期间创建新的 ReplicaSets,并在发布过程中逐步扩缩旧 ReplicaSets 和新 ReplicaSets。[7] 这属于协调系统的工作,已经超出本地栈关注范围。
这就是最清晰的生产边界。如果应用需要多节点放置、受控发布、pod 替换、水平扩展、集群服务发现、准入策略、网络策略或租户级资源治理,就不要要求 Compose 变成这套系统。把 Compose 用于本地开发、冒烟测试或小型单主机运行,然后把生产合约翻译进 Kubernetes、Nomad、systemd units,或另一个具备相应控制平面的真实运行时。
独立的 Okteto 文档有参考价值,因为它显示了生态从另一侧如何理解这条边界。Okteto 实现并扩展 Compose Specification,让开发者可以在 Kubernetes 中使用 Docker Compose 应用,同时免去直接处理 Kubernetes manifest 复杂性的负担。[8] 这个例子证明不了 Compose 和 Kubernetes 是同一件事。它证明的是,Compose 可以作为面向开发者的模型,同时由另一个平台拥有部署行为。
成熟团队可以同时使用两者。Compose 定义本地栈。Kubernetes manifests、Helm charts、Kustomize overlays 或平台模板定义集群状态。纪律在于避免假装一个文件就是另一个文件。端口号、环境变量、镜像名称和健康端点应相互对齐。发布语义、放置、ingress、secrets 集成和策略,应放在运行时能够强制执行的位置。
一条实际迁移路线
从一个可以启动最小有用应用的 compose.yaml 开始。使用规范服务名:web、api、worker、db、redis、migrate,不要用内部绰号。必须保留的状态优先使用命名卷。把所有主机可见端口放在一处,删掉任何没人或测试真正需要的端口。先加健康检查,再加可选工具。如果应用在这之后仍需要 README 里的命令顺序,Compose 文件还没有完成。
接着用 profiles 切分可选性。让 docker compose up 保持朴素可靠。只为调试 UI 和本地检查工具添加 docker compose --profile debug up。只有在当前任务需要追踪、指标或日志探索时,才添加 docker compose --profile observability up。除非应用离开辅助服务就会失败,否则把辅助服务留在核心路径之外。[4]
然后改善编辑循环。只给从本地源码构建的服务添加 develop.watch,并为依赖目录或不应跨越主机/容器边界的生成产物写出明确的 ignore 规则。[5] 如果依赖变更要求重建,就在 watch 规则里说清楚,别把手动重启时机留给开发者记忆。
最后,记录退出标准。如果栈是单主机、小规模、可检查,并且主要目标是可重复启动,Compose 仍适合作为中心。当故障恢复、发布控制、服务路由、策略执行和规模成为主要工作时,它就不再适合作为中心。好的迁移计划会提前写出这条阈值。
Docker Compose 的价值仍在于它拒绝变得光鲜。它给团队一份共享的本地应用模型,为服务、网络、卷、配置、secrets、健康状态、profiles、watches 和 includes 提供足够结构。采用它的收益不在于 YAML 替代运维,而在于本地栈不再靠口口相传。让 compose.yaml 保持为合约,Compose 会在第一次成功 up 之后继续有用。
Sources
- Compose Specification, "The Compose Specification" - 应用模型、服务、网络、卷、配置、secrets,以及实现支持差异的提醒。
- Docker Docs, "Define services in Docker Compose" -
services作为必需顶层 map、服务定义、build,以及可选的deploy行为。 - Docker Docs, "Control startup and shutdown order in Compose" -
depends_on、service_healthy、service_completed_successfully,以及 PostgreSQL 健康检查示例。 - Docker Docs, "Using profiles with Compose" -
profiles属性、始终启用的服务、profile 命名规则,以及保持核心服务不设 profile 的建议。 - Docker Docs, "Use Compose Watch" -
develop.watch、Compose 2.22.0 要求、sync/rebuild 行为、ignore 规则,以及 build 与 image 的边界。 - Docker Docs, "Use include to modularize Compose files" - Compose 2.20.0 要求、被 include 的应用模型、项目目录行为,以及冲突警告。
- Kubernetes Docs, "Deployments" - 期望状态控制、Pods 与 ReplicaSets、发布行为,以及由 controller 管理的更新。
- Okteto Docs, "Docker Compose Reference" - 独立平台文档,展示 Compose 作为面向开发者的模型被实现并扩展,用于 Kubernetes 开发工作流。
- Wikimedia Commons, "Shipping containers in a port (Unsplash).jpg" - 文章图片所用的巴塞罗那港真实照片。