首页
/ Docker Compose Provider 扩展机制深度解析:用 provider 把外部资源纳入服务模型

Docker Compose Provider 扩展机制深度解析:用 provider 把外部资源纳入服务模型

2026-09-05 19:15:49作者:姚月梅Lane

Docker Compose 的应用模型中,service 不仅可以用 Docker Engine 管理的容器实现,还可以通过 provider 属性对接第三方运行时(如云数据库、主机原生服务)。本文基于仓库中的 extension.md 与对应源码实现,系统讲解 Compose 的 Provider 扩展架构:如何声明 provider 服务、实现 compose up/down/stop 子命令、通过 JSON 行协议与 Compose 通信(info/error/setenv/rawsetenv/debug),以及利用 metadata 子命令提供参数自描述能力。读完后你能够为任意外部服务(云服务、主机服务等)编写一个完整的 Compose Provider,并理解 Compose 在生命周期各阶段如何调用和管理它。

一、设计背景:service 抽象与 provider 属性

Compose 的应用模型把 service 定义为"管理(应用需求的)一个子集的计算单元"的抽象,这些单元之间通过网络彼此交互。Docker Compose 默认通过 Docker Engine(Moby)API 将 service 实现为容器,但这个抽象同样可以覆盖其他运行时——典型如云服务、或由主机原生提供的服务。

因此,Compose 的可扩展性模型的设计目标就是:把 service 的支持扩展到能通过第三方工具访问的运行时。其核心机制是 service 定义中的 provider 属性,它用于声明实际负责管理该服务所需资源的外部二进制程序:

services:
  database:
    provider:
      type: awesomecloud
      options:
        type: mysql
        size: 256
        name: myAwesomeCloudDB

其中:

  • provider.type 指定 provider 二进制,解析规则为二选一:

    1. 另一个 Docker CLI 插件(例如 type: model 会运行 docker-model);
    2. 用户 PATH 中的可执行文件。

    从源码看,这一优先级在 plugins.gogetPluginBinaryPath 中实现:先通过 manager.GetPlugin 按 Docker CLI 插件解析,若返回 not-found 再回退到 exec.LookPath 查找 PATH 中的同名可执行文件(Windows 下会追加 .exe 后缀,见 plugins_windows.go)。若两者都解析不到,Compose 会报错并中断 up 命令。另外源码还显式禁止 type: compose 作为 provider 类型,避免与 Compose 自身冲突。

  • provider.options 中的每个键值对会被翻译成命令行 flag 传给 provider(详见下文 Up lifecycle)。

二、Provider 命令接口规范

要成为一个合法的 Compose 扩展,provider 命令**必须(MUST)**接受一个 compose 子命令(可以是隐藏命令),并提供:

子命令 是否必须 对应 Compose 命令
compose up 必须 docker compose up
compose down 必须 docker compose down
compose stop 可选 docker compose stop(opt-in,见第六节)
compose metadata 可选 提供参数自描述,见第七节

仓库中的 docs/examples/provider.go 给出了一个完整可运行的参考实现(基于 cobra),它注册了 updownstopmetadata 四个子命令;该示例同时被用作端到端测试的二进制:Makefile 中提供了 make example-provider 目标,直接 go build -o bin/build/example-provider docs/examples/provider.go 构建。

三、Up 生命周期:options 如何变成命令行

执行应用的 up 生命周期时,Compose 会运行 provider 的 compose up 命令,传入项目名称、服务名以及额外的选项。provider.options 会被逐条翻译为 --key=value 形式的命令行 flag。例如上面 awesomecloud 的例子,Compose 实际执行:

awesomecloud compose --project-name <NAME> up --type=mysql --size=256 "database"

注意: provider **应当(should)**利用 project-name 为该项目分配的所有资源打标,这样后续 down 子命令执行时能够释放属于该项目的全部分配资源。

源码层面,这条命令的构造在 plugins.gosetupPluginCommand 中完成:

  • 参数基线为 compose --project-name=<项目名> <up|down|stop>
  • 遍历 provider.Options,以 --k=v 追加(且当 metadata 声明了参数时,只有 metadata 中声明过的 option 才会被传递);
  • 最后追加服务名作为位置参数。

此外还有两个容易忽略的实现细节:

  1. 必需参数校验:如果 provider 实现了 metadata,Compose 在组装命令前会用 CheckRequiredParameters 校验 options 是否覆盖了所有 required: true 的参数,缺失时直接报 required parameter %q is missing from provider %q definition 错误,而不是等到执行 provider 才失败。
  2. 环境变量传递prepareShellOutshellout.go)会把项目环境变量注入子进程,同时删除 DOCKER_CLI_PLUGIN 变量——这让同一个 Docker CLI 插件也能以独立程序身份运行;还会把 OpenTelemetry 上下文传播给子进程。

up 的执行计划中,provider 服务被建模为一个独立的 OpRunProvider 计划节点(reconcile.go 中为其分配 provider:<service> 资源 ID 并挂接依赖边,executor.go 中最终调用 runPlugin(..., "up")),因此 provider 服务与容器服务一样参与依赖排序。

四、与 Compose 通信:stdout 上的 JSON 行协议

Provider 以 stdout 作为通道与 Compose 交互,发送 JSON 行分隔(JSON line delimited)消息。每条消息**必须(MUST)**包含 typemessage 两个属性:

{ "type": "info", "message": "preparing mysql ..." }

支持的 type 取值:

type 语义 Compose 的处理
info 状态更新 渲染为该服务的状态,显示在进度 UI 中
error 出错报告 作为服务失败原因渲染给用户,命令随即失败
setenv 告知依赖服务如何访问所创建的资源 注入依赖服务环境变量,变量名自动加上服务名前缀
rawsetenv setenv,但变量按原样注入,不加前缀 适用于应用要求精确变量名的场景(如密钥名)
debug 调试信息 默认不渲染;Compose 以 --verbose 启动时显示

原文档中的交互时序如下:

sequenceDiagram
    Shell->>Compose: docker compose up
    Compose->>Provider: compose up --project-name=xx --foo=bar "database"
    Provider--)Compose: json { "info": "pulling 25%" }
    Compose-)Shell: pulling 25%
    Provider--)Compose: json { "info": "pulling 50%" }
    Compose-)Shell: pulling 50%
    Provider--)Compose: json { "info": "pulling 75%" }
    Compose-)Shell: pulling 75%
    Provider--)Compose: json { "setenv": "URL=http://cloud.com/abcd:1234" }
    Compose-)Compose: set DATABASE_URL
    Provider--)Compose: json { "rawsetenv": "SECRET_KEY=xxx" }
    Compose-)Compose: set SECRET_KEY (as-is)
    Provider-)Compose: EOF (command complete) exit 0
    Compose-)Shell: service started

源码中,消息解码循环位于 plugins.goexecutePlugin:用 json.Decoder 逐行解码 stdout 直到 EOF,按 msg.Type 分发处理。几个值得注意的行为细节:

  • error 消息会立即返回错误,且事件中只取消息的第一行firstLine 辅助函数会截断多行消息);
  • setenv/rawsetenvmessage 必须是 KEY=VALUE 形式,= 缺失时报 invalid response from plugin
  • 出现未识别的 type 会直接报错,保证协议双方版本兼容是显式约定的;
  • 命令退出后,Compose 还会检查进程退出状态,非零退出会报告 failed to <action> service provider 并触发对应的 error 事件。

五、依赖注入:setenv 与 rawsetenv

Compose 应用中的服务可以声明依赖一个由外部 provider 管理的服务:

services:
  app:
    image: myapp
    depends_on:
       - database

  database:
    provider:
      type: awesomecloud

当 provider 发出 setenv 消息时,Compose 会把变量注入到所有依赖它的服务中,并自动加上依赖服务名(大写)作为前缀。例如 awesomecloud compose up 返回:

{"type": "setenv", "message": "URL=https://awesomecloud.com/db:1234"}

app 服务会在其运行环境中收到 DATABASE_URL 环境变量。

当 provider 发出 rawsetenv 消息时,Compose 按原样注入变量、不加任何前缀:

{"type": "rawsetenv", "message": "SECRET_KEY=xxx"}

app 服务收到的就是精确的 SECRET_KEY,与 provider 服务名无关。这适用于注入密钥或框架要求的、不能改名的配置值。

两种方式的冲突语义有本质区别(同样见于源码 runPlugin 的实现与 providers_test.go 的 e2e 断言):

  • setenv 通过自动加前缀避免冲突;
  • rawsetenv 的键名唯一性由 provider 自己负责。若与依赖服务已有变量冲突(包括用户在 environment 中声明的、以及其他 provider 发出的值),既有值会被覆盖,且 Compose 记录一条 provider %q overrides environment variable %q in service %q 的警告。e2e 测试 TestProviderRawSetEnvOverridesUserEnv 就用用户声明 CLOUD_REGION: user-defined-region 的 compose 文件(TestProviderRawSetEnvOverridesUserEnv/compose.yaml)验证了覆盖行为与警告输出;
  • 未被 depends_on 关联的 provider 可能并发运行,因此多个 provider 发出同名 rawsetenv 键时,最终值不确定,这一点需要 provider 作者自行规避。

注意: compose up 命令必须(MUST)是幂等的。如果资源已在运行,命令必须设置相同的环境变量,以保证依赖服务的配置一致。

六、Down 与 Stop 生命周期

Down 生命周期up 对偶,执行:

<provider> compose --project-name <NAME> down <SERVICE>

provider 负责释放该服务关联的全部资源。down.go 中对 Provider != nil 的服务直接调用 runPlugin(..., "down") 完成回收。

Stop 生命周期相对更晚加入,规则更细致:

  1. 用户执行 docker compose stop 时,Compose 按逆依赖顺序对每个 provider 服务调用 <provider> compose --project-name <NAME> stop <SERVICE>。逆依赖顺序由 stop.go 中的 InReverseDependencyOrder 保证,即先停依赖方、后停被依赖的 provider 服务。
  2. provider 应当暂停(pause)资源而不释放它,以便后续的 docker compose up 能够恢复。注意 docker compose start 只会重启已存在的容器,不会触发 provider hook。
  3. stop 期间返回的任何 setenv/rawsetenv 消息都会被忽略,因为依赖服务同样在停止(源码中 runPluginstop 命令直接跳过变量注入逻辑)。
  4. opt-in 机制:Compose 只有在 provider 的 metadata 子命令输出中声明了 stop 块时才会调用 stop hook;未声明 stop(甚至完全未实现 metadata)的 provider 在 docker compose stop 期间被静默跳过,以此保持与旧版 provider 的向后兼容。源码实现即 setupPluginCommandcmdOptionsMetadata.Stop == nil 时返回 nil, nil(不执行命令)。
  5. docker compose stop--timeout 只作用于容器服务;provider stop hook 不受该超时约束,需要自行管理关停时长。

该行为的端到端验证见 providers_test.goTestProviderStopHook:示例 provider 的 stop 子命令会在环境变量 PROVIDER_STOP_MARKER 指定的路径写一个哨兵文件,测试断言 compose stop 后该文件存在(对应 compose 文件见 TestProviderStopHook/compose.yaml)。

七、metadata 子命令:参数自描述与校验

Compose 扩展**可以(MAY)**可选地实现 metadata 子命令,描述 updown(及可选 stop)命令所接受的参数及其必填性。metadata 不接收参数,在 stdout 上输出一段 JSON:

awesomecloud compose metadata

期望的输出格式(完整示例见原文档,此处保留全量字段):

{
  "description": "Manage services on AwesomeCloud",
  "up": {
    "parameters": [
      {
        "name": "type",
        "description": "Database type (mysql, postgres, etc.)",
        "required": true,
        "type": "string"
      },
      {
        "name": "size",
        "description": "Database size in GB",
        "required": false,
        "type": "integer",
        "default": "10"
      },
      {
        "name": "name",
        "description": "Name of the database to be created",
        "required": true,
        "type": "string"
      }
    ]
  },
  "down": {
    "parameters": [
      {
        "name": "name",
        "description": "Name of the database to be removed",
        "required": true,
        "type": "string"
      }
    ]
  },
  "stop": {
    "parameters": [
      {
        "name": "name",
        "description": "Name of the database to be stopped",
        "required": true,
        "type": "string"
      }
    ]
  }
}

顶层元素:

  • description:provider 的人类可读描述;
  • up:描述 up 命令接受参数的对象;
  • down:描述 down 命令接受参数的对象;
  • stop:描述 stop 命令接受参数的对象(可选,且它的存在正是 stop hook 的 opt-in 声明)。

每个参数应包含的属性:

属性 说明
name 参数名(不带 -- 前缀)
description 人类可读的参数描述
required 布尔值,是否必填
type 参数类型(stringintegerboolean 等)
default 默认值(可选,仅用于非必填参数)
enum 参数允许取值列表,以 , 分隔(可选,仅用于取值受限的参数)

这套元数据让 Compose 和其他工具能够理解 provider 的接口,从而提供校验、自动补全和文档生成等更好的用户体验。

源码侧,metadata 的处理流程在 getPluginMetadataplugins.go)中:Compose 执行 <provider> compose metadata,把输出反序列化为 ProviderMetadata;失败(命令不存在或输出非法)时只记录 debug 日志并视为"无 metadata",保持向后兼容。此外还有一个工程上的副产品:metadata 会被持久化到 Docker 配置目录下的 compose/providers/<type>.json,供 Docker LSP 工具读取,从而在编辑器里对 provider.options 提供补全。对应地,示例 provider 的 metadataCommandprovider.go)直接从 cobra/pflag 的 flag 定义反射生成 metadata JSON,展示了"单一数据源"式的实现方式——flag 定义既是 CLI 契约又是 metadata 来源。

八、参考实现与本地验证

仓库提供了一个可直接构建运行的参考实现 docs/examples/provider.go,其要点:

  • compose 根命令带 TraverseChildren: true 与持久 flag --project-name(文档标注该值"未使用",实际应由 provider 自行消费以打标资源);
  • up 子命令:接收 --type(必填)、--size(默认 10)、--name(必填),按 size 循环打印 info 进度消息,最后分别发出 setenvURL=https://magic.cloud/<service>)和 rawsetenvCLOUD_REGION=us-east-1);
  • down 子命令:演示 error 消息(打印 Permission error);
  • stop 子命令:读取 PROVIDER_STOP_MARKER 环境变量并写哨兵文件,用于测试断言;
  • metadata 子命令:遍历各子命令的 pflag flag,借助 cobra 的必填标记注解生成符合第七节格式的 JSON。

要本地验证整套机制,可以按仓库自带的端到端测试路径操作(查看即可,无需修改仓库):

  1. 构建示例 provider:make example-provider(产物为 bin/build/example-provider,见 Makefile 第 92–94 行);
  2. 将其所在目录加入 PATH,编写形如 TestProviderStopHook/compose.yaml 的 compose 文件(一个 provider 服务 + 一个 depends_on 它的容器服务,容器命令用 env 回显注入结果);
  3. 运行 docker compose up,观察容器内收到 PROVIDER_URL=...(setenv 加前缀)与 CLOUD_REGION=us-east-1(rawsetenv 原样注入)。

这些场景在 e2e 套件中有系统性覆盖(providers_test.go):多 provider 依赖注入(TestDependsOnMultipleProviders)、两种 setenv 语义(TestProviderRawSetEnv)、覆盖用户环境/继承变量时的警告(TestProviderRawSetEnvOverridesUserEnv 等),可以按测试注释中的断言逐条比对输出。

九、实现一个 Provider 的检查清单

综合原文档与源码行为,一个健壮的 Compose Provider 应满足:

  1. 命令面:提供 compose upcompose down(必须),按需提供 compose stopcompose metadata(可选);
  2. 二进制可解析:作为 Docker CLI 插件或 PATH 中可执行文件存在,且不与保留名 compose 冲突;
  3. stdout 协议纪律:只输出 JSON 行(或把非协议输出导向 stderr),type 只用协议约定值,setenv/rawsetenvmessage 严格保持 KEY=VALUE 单行格式;
  4. 幂等 up:重复执行 up 必须设置相同的环境变量;
  5. project-name 打标:用 --project-name 为资源打标,保证 down 能精确释放;
  6. stop 语义:暂停而非释放资源,并只在 metadata 中声明 stop 块以开启 hook;自行管理关停时长,不要依赖 --timeout
  7. metadata 校验:利用 required 参数让 Compose 在本地提前报错;避免 rawsetenv 键名冲突,理解覆盖会触发警告且并发 provider 下结果不确定。

掌握以上机制后,你就可以把任意"非容器化"的基础设施(云托管数据库、消息队列、主机原生代理等)无缝接入 Compose 的 up/down/stop 生命周期,并通过 setenv/rawsetenv 让容器化服务像依赖内部组件一样依赖它们。

登录后查看全文
热门项目推荐
相关项目推荐