Docker Compose Provider 扩展机制深度解析:用 provider 把外部资源纳入服务模型
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 二进制,解析规则为二选一:- 另一个 Docker CLI 插件(例如
type: model会运行docker-model); - 用户
PATH中的可执行文件。
从源码看,这一优先级在 plugins.go 的
getPluginBinaryPath中实现:先通过manager.GetPlugin按 Docker CLI 插件解析,若返回 not-found 再回退到exec.LookPath查找PATH中的同名可执行文件(Windows 下会追加.exe后缀,见 plugins_windows.go)。若两者都解析不到,Compose 会报错并中断up命令。另外源码还显式禁止type: compose作为 provider 类型,避免与 Compose 自身冲突。 - 另一个 Docker CLI 插件(例如
-
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),它注册了 up、down、stop、metadata 四个子命令;该示例同时被用作端到端测试的二进制: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.go 的 setupPluginCommand 中完成:
- 参数基线为
compose --project-name=<项目名> <up|down|stop>; - 遍历
provider.Options,以--k=v追加(且当 metadata 声明了参数时,只有 metadata 中声明过的 option 才会被传递); - 最后追加服务名作为位置参数。
此外还有两个容易忽略的实现细节:
- 必需参数校验:如果 provider 实现了
metadata,Compose 在组装命令前会用CheckRequiredParameters校验options是否覆盖了所有required: true的参数,缺失时直接报required parameter %q is missing from provider %q definition错误,而不是等到执行 provider 才失败。 - 环境变量传递:
prepareShellOut(shellout.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)**包含 type 和 message 两个属性:
{ "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.go 的 executePlugin:用 json.Decoder 逐行解码 stdout 直到 EOF,按 msg.Type 分发处理。几个值得注意的行为细节:
error消息会立即返回错误,且事件中只取消息的第一行(firstLine辅助函数会截断多行消息);setenv/rawsetenv的message必须是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 生命周期相对更晚加入,规则更细致:
- 用户执行
docker compose stop时,Compose 按逆依赖顺序对每个 provider 服务调用<provider> compose --project-name <NAME> stop <SERVICE>。逆依赖顺序由 stop.go 中的InReverseDependencyOrder保证,即先停依赖方、后停被依赖的 provider 服务。 - provider 应当暂停(pause)资源而不释放它,以便后续的
docker compose up能够恢复。注意docker compose start只会重启已存在的容器,不会触发 provider hook。 stop期间返回的任何setenv/rawsetenv消息都会被忽略,因为依赖服务同样在停止(源码中runPlugin对stop命令直接跳过变量注入逻辑)。- opt-in 机制:Compose 只有在 provider 的
metadata子命令输出中声明了stop块时才会调用 stop hook;未声明stop(甚至完全未实现metadata)的 provider 在docker compose stop期间被静默跳过,以此保持与旧版 provider 的向后兼容。源码实现即setupPluginCommand中cmdOptionsMetadata.Stop == nil时返回nil, nil(不执行命令)。 docker compose stop的--timeout只作用于容器服务;provider stop hook 不受该超时约束,需要自行管理关停时长。
该行为的端到端验证见 providers_test.go 的 TestProviderStopHook:示例 provider 的 stop 子命令会在环境变量 PROVIDER_STOP_MARKER 指定的路径写一个哨兵文件,测试断言 compose stop 后该文件存在(对应 compose 文件见 TestProviderStopHook/compose.yaml)。
七、metadata 子命令:参数自描述与校验
Compose 扩展**可以(MAY)**可选地实现 metadata 子命令,描述 up 和 down(及可选 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 |
参数类型(string、integer、boolean 等) |
default |
默认值(可选,仅用于非必填参数) |
enum |
参数允许取值列表,以 , 分隔(可选,仅用于取值受限的参数) |
这套元数据让 Compose 和其他工具能够理解 provider 的接口,从而提供校验、自动补全和文档生成等更好的用户体验。
源码侧,metadata 的处理流程在 getPluginMetadata(plugins.go)中:Compose 执行 <provider> compose metadata,把输出反序列化为 ProviderMetadata;失败(命令不存在或输出非法)时只记录 debug 日志并视为"无 metadata",保持向后兼容。此外还有一个工程上的副产品:metadata 会被持久化到 Docker 配置目录下的 compose/providers/<type>.json,供 Docker LSP 工具读取,从而在编辑器里对 provider.options 提供补全。对应地,示例 provider 的 metadataCommand(provider.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进度消息,最后分别发出setenv(URL=https://magic.cloud/<service>)和rawsetenv(CLOUD_REGION=us-east-1);down子命令:演示error消息(打印Permission error);stop子命令:读取PROVIDER_STOP_MARKER环境变量并写哨兵文件,用于测试断言;metadata子命令:遍历各子命令的 pflag flag,借助 cobra 的必填标记注解生成符合第七节格式的 JSON。
要本地验证整套机制,可以按仓库自带的端到端测试路径操作(查看即可,无需修改仓库):
- 构建示例 provider:
make example-provider(产物为bin/build/example-provider,见 Makefile 第 92–94 行); - 将其所在目录加入
PATH,编写形如 TestProviderStopHook/compose.yaml 的 compose 文件(一个 provider 服务 + 一个depends_on它的容器服务,容器命令用env回显注入结果); - 运行
docker compose up,观察容器内收到PROVIDER_URL=...(setenv 加前缀)与CLOUD_REGION=us-east-1(rawsetenv 原样注入)。
这些场景在 e2e 套件中有系统性覆盖(providers_test.go):多 provider 依赖注入(TestDependsOnMultipleProviders)、两种 setenv 语义(TestProviderRawSetEnv)、覆盖用户环境/继承变量时的警告(TestProviderRawSetEnvOverridesUserEnv 等),可以按测试注释中的断言逐条比对输出。
九、实现一个 Provider 的检查清单
综合原文档与源码行为,一个健壮的 Compose Provider 应满足:
- 命令面:提供
compose up、compose down(必须),按需提供compose stop与compose metadata(可选); - 二进制可解析:作为 Docker CLI 插件或
PATH中可执行文件存在,且不与保留名compose冲突; - stdout 协议纪律:只输出 JSON 行(或把非协议输出导向 stderr),
type只用协议约定值,setenv/rawsetenv的message严格保持KEY=VALUE单行格式; - 幂等 up:重复执行
up必须设置相同的环境变量; - project-name 打标:用
--project-name为资源打标,保证down能精确释放; - stop 语义:暂停而非释放资源,并只在
metadata中声明stop块以开启 hook;自行管理关停时长,不要依赖--timeout; - metadata 校验:利用
required参数让 Compose 在本地提前报错;避免rawsetenv键名冲突,理解覆盖会触发警告且并发 provider 下结果不确定。
掌握以上机制后,你就可以把任意"非容器化"的基础设施(云托管数据库、消息队列、主机原生代理等)无缝接入 Compose 的 up/down/stop 生命周期,并通过 setenv/rawsetenv 让容器化服务像依赖内部组件一样依赖它们。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00