Docker Compose alpha publish 深度解析:把 Compose 项目打包为 OCI 制品并发布到镜像仓库
本篇指南围绕 Docker Compose CLI 的实验性命令 docker compose alpha publish 展开,完整覆盖其命令用法、全部参数、发布前的安全预检、OCI 图层(layer)的组织方式,以及 OCI 1.0/1.1 清单兼容机制。读完后,你将掌握如何把一个 Compose 项目连同其环境文件、digest 锁定信息一起发布为标准 OCI 制品,并理解其在源码层面如何通过 pkg/compose/publish.go 和 internal/oci/push.go 实现这一流程。
一、命令定位:一条实验性命令
publish 命令挂载在 alpha 实验性命令组下,该组在源码中被显式标记为隐藏(Hidden: true)并带有 experimentalCLI: "true" 注解(见 cmd/compose/alpha.go),与 viz、generate 并列。其命令签名为:
docker compose alpha publish [OPTIONS] REPOSITORY[:TAG]
其中 REPOSITORY[:TAG] 为唯一必填的位置参数,指向目标 OCI 仓库及标签(如 myorg/myapp:1.0.0),CLI 要求精确一个位置参数(cmd/compose/publish.go)。
这条命令的核心价值在于:它发布的不是容器镜像,而是“Compose 项目”本身——Compose 文件作为 OCI 制品的图层被推送到任意 OCI 兼容注册表,供其他用户以单一制品的形式拉取并运行你的多容器应用。
二、完整参数说明
命令参考文档 docs/reference/compose_alpha_publish.md 列出了以下参数,结合 cmd/compose/publish.go 中 flag 的注册代码,完整参数表如下:
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
--dry-run |
bool |
以 dry-run 模式执行命令(只演练,不真正推送) | |
--oci-version |
string |
指定 OCI 镜像/制品规范版本(默认自动判定) | |
--resolve-image-digests |
bool |
将镜像 tag 固定(pin)为 digest | |
--with-env |
bool |
在发布的 OCI 制品中包含环境变量文件 | |
-y, --yes |
bool |
对所有交互提示自动回答 "yes" | |
--app |
bool |
发布完整 Compose 应用(包含所引用的镜像,见下文第五节) | |
--insecure-registry |
bool |
使用不安全的明文 HTTP 注册表(隐藏 flag,仅供测试) |
几个源码层面的补充细节:
--y已废弃:代码中的 flag 归一化函数会拦截--y并打印警告--y is deprecated, please use --yes instead(cmd/compose/publish.go)。--app会隐含启用 digest 解析:runPublish中将ResolveImageDigests设置为opts.resolveImageDigests || opts.app(cmd/compose/publish.go),即发布完整应用时必然携带 digest 锁定层。--insecure-registry是隐藏 flag:源码注释明确说明“仅供测试用途,我们不鼓励使用不安全注册表”,因此官方参考文档中不展示它。- 命令在加载项目时会拒绝包含本地
include的 Compose 文件,直接报错cannot publish compose file with local includes(cmd/compose/publish.go)。
三、发布主流程
publish 的 API 入口是 pkg/api/api.go 中定义的 PublishOptions:
// PublishOptions group options of the Publish API
type PublishOptions struct {
ResolveImageDigests bool
Application bool
WithEnvironment bool
OCIVersion OCIVersion
// Use plain HTTP to access registry. Should only be used for testing purpose
InsecureRegistry bool
}
核心实现 composeService.publish 按以下顺序执行:
- 加载全量 Profile:
project.WithProfiles([]string{"*"})确保带 Profile 的服务也一并被发布; - 安全预检(preChecks):逐项检查并交互确认,任何一项拒绝都会以
api.ErrCanceled终止(详见第四节); - 推送被引用的镜像:调用内部
s.push(...),选项为IgnoreFailures: true, ImageMandatory: true——先把 Compose 文件引用的镜像推送到注册表,保证使用者拉取制品后能直接运行; - 构建 OCI 图层:
createLayers将 Compose 文件、extends 父文件、env 文件、digest 锁定文件转换为描述符集合(详见第五节); - 推送制品清单:
pushComposeArtifact通过 oci.PushManifest 将空 config、各图层和 manifest 依次推送到注册表; - (仅
--app)推送应用镜像索引:见第六节。
整个过程包裹在名为 "publish" 的事件 bracket 中(Run(ctx, ..., "publish", s.events)),进度通过 s.events.On(...) 以 publishing / published 状态输出,--dry-run 模式下只走到图层构建为止,不会执行第 5 步。
四、发布前安全预检:防止泄露敏感信息
preChecks 是 publish 命令中交互逻辑最密集的部分,共包含四类检查,按顺序执行:
4.1 拒绝纯 build 服务
checkOnlyBuildSection 检查是否存在“只有 build 段而没有 image 字段”的服务。若存在,直接报错并列出服务名:
your Compose stack cannot be published as it only contains a build section for service(s):
- "serviceA"
因为发布的是项目定义而非构建上下文,发布者需要预先完成镜像构建与推送。
4.2 bind mount 声明确认
checkForBindMount 收集所有服务的 type: bind 卷声明并逐条列出,提示用户“只发布声明(declaration),不发布内容”,确认未挂载到敏感的用户目录后才继续(pkg/compose/publish.go)。
4.3 敏感数据扫描
checkForSensitiveData 使用 DefangLabs 的 secret-detector(scanner.NewDefaultScanner)扫描四类来源(pkg/compose/publish.go):
- 所有 Compose 文件(以未插值状态加载后序列化扫描);
- 各服务的
env_file文件; - 以文件定义的
configs; - 以文件定义的
secrets。
命中(如 AWS_SECRET_ACCESS_KEY=...)时会逐项列出类型、键与值并要求确认;用户拒绝则中止。单元测试 Test_preChecks_sensitive_data_detected_decline 验证了拒绝即返回 accept=false 的行为。
4.4 环境变量相关发现(env findings)
checkEnvironmentVariables 与 collectEnvCheckFindings(pkg/compose/publish.go)负责两类与 --with-env 相关的确认:
-
env_file 声明与疑似敏感字面量:服务中声明了
env_file、或环境变量的键名形如密码/密钥类(password、secret、token、api_key 等,由 keyword detector 判定)且值为字面量时,会聚合成一条提示,例如:you are about to publish env-related declarations within your OCI artifact. ... service "db": literal value for "MYSQL_ROOT_PASSWORD" Use --with-env to silence this prompt and always publish env declarations. Are you ok to publish these env declarations?关键点:插值值不会触发提示。形如
"${DB_PASSWORD}"、"$VAR"的值在发布产物中保持符号占位符,检测器的值正则会自动跳过;而$$转义出的字面$(如pa$$word)会被替换占位后正常判定,因此仍会被标记(对应测试见 pkg/compose/publish_test.go)。加上--with-env可静默此类提示。 -
config 内联
content字面量:config.content写的是字面内容(而非${VAR}插值)时始终单独提示——它与--with-env解耦,因为该 flag 的语义只覆盖环境变量(见 envCheckFindings 的注释)。
以上行为的端到端验证在 pkg/e2e/publish_test.go:拒绝 env 提示时以退出码 130 中止且输出 test/test published 前不出现发布完成字样;接受 bind mount 提示后正常发布。
五、图层结构:Compose 文件如何变成 OCI layer
createLayers 决定最终制品里有哪些层,各层的 media type 定义在 internal/oci/push.go:
| 常量 | 值 | 用途 |
|---|---|---|
ComposeYAMLMediaType |
application/vnd.docker.compose.file+yaml |
每个 Compose 文件一层 |
ComposeEnvFileMediaType |
application/vnd.docker.compose.envfile |
每个 env 文件一层(仅 --with-env) |
ComposeEmptyConfigMediaType |
application/vnd.docker.compose.config.empty.v1+json |
OCI 1.0 模式的 config 描述符 |
ComposeProjectArtifactType |
application/vnd.docker.compose.project |
OCI 1.1 模式的 artifactType |
路径脱敏是图层构建的核心设计。processFile 对每个 Compose 文件做两件事:
- 服务引用的
env_file路径被重写为sha256(路径).env的无含义占位符(如5efca9cdbac9...51af3.env); extends.file引用的父文件同样重写为sha256(路径).yaml,并作为独立图层递归发布,该层额外标注com.docker.compose.extends: "true"(processExtends)。
重写通过 transform.ReplaceEnvFile / ReplaceExtendsFile 在 YAML 流上按行/列定位替换,保留原文件的格式与注释。这样做的目的是:发布产物永远不会泄露发布者机器的本地路径,且哈希由路径字符串本身推导,即使可选 env 文件缺失(required: false),占位符也保持一致(测试 Test_processFile_optional_env_file_missing)。
单元测试 Test_createLayers 基于 pkg/compose/testdata/publish/ 下的样例(compose.yaml + common.yaml + test.env)断言了完整结果:发布后的 YAML 中 extends.file 变成 f8f9ede3...90c.yaml,三种 env_file 写法(字符串、列表、映射)统一替换为 5efca9cd...451af3.env,并生成 3 个图层(主文件、extends 父文件、env 文件)。
每个文件层的描述符还携带两个注解:com.docker.compose.file(或 com.docker.compose.envfile)标注文件基名,com.docker.compose.version 标注生成它的 Compose CLI 版本(DescriptorForComposeFile)。
--resolve-image-digests 的 digest 锁定层:开启后,generateImageDigestsOverride 通过 WithImagesResolved 对每个服务镜像(含 pre_start hook 镜像、type: image 卷源镜像)执行 DistributionInspect 解析出 digest,生成一份形如:
services:
app:
image: docker.io/library/nginx:latest@sha256:aaaa...
的 image-digests.yaml 覆盖层追加到制品中,消费方据此即可把浮动 tag 固定到不可变 digest。测试 Test_generateImageDigestsOverride_resolvesDependentImages 验证了依赖镜像(hook、image volume)都会被逐一解析。
六、OCI 清单生成与注册表兼容
PushManifest 实现了文档中 --oci-version “automatically determined by default” 的自动判定逻辑:
- 版本为
1.1或未指定时,先推送一个空 JSON config blob; - 依次推送所有图层;
- 未显式指定版本时,优先按 OCI 1.1 生成 manifest(
ArtifactType: application/vnd.docker.compose.project);若收到非认证类的 4xx 错误(推断注册表不支持 OCI 1.1),自动回退到 OCI 1.0; - 认证类错误(401/403/407,见 clientAuthStatusCodes)不会触发回退,直接报错。
两个版本的关键差异在 generateManifest:
- OCI 1.1:config 为空描述符,
artifactType字段声明application/vnd.docker.compose.project; - OCI 1.0:spec 无 artifactType 字段,按 OCI 1.1 规范的回退建议改用 config.mediaType 标识——这里使用专属的
application/vnd.docker.compose.config.empty.v1+json(内容为{}),让工具能把它识别为 Compose 制品而非容器镜像。
所有 manifest 均带 org.opencontainers.image.created 时间戳注解。仓库凭证方面,oci.NewResolver 从 Docker CLI 的登录配置(config.GetAuthConfig,支持 IdentityToken)取凭据,并复用 Docker Desktop 的代理 transport 访问注册表,因此 docker login 过的用户无需额外配置。
七、--app:发布可整体拉取的完整应用
在 --app 模式下,pushComposeArtifact 额外调用 pushApplicationIndex:
- 遍历项目的每个服务,将其镜像通过 oci.Copy 以 registry mount 的方式“拷贝”到目标仓库(利用
LabelDistributionSource注解触发跨仓库 mount,避免重复上传); - 生成一个
MediaTypeImageIndex索引,Manifests引用所有服务镜像,Subject指向此前发布的 Compose 制品 manifest,ArtifactType同为application/vnd.docker.compose.project; - 如前所述,该模式同时隐含
ResolveImageDigests,因此制品中始终带有 digest 锁定层。
这样消费方可以用单一引用(索引 digest)拉到“Compose 定义 + 全部镜像”的完整应用。
八、典型使用方式与注意事项
一次完整的发布命令通常长这样:
# 交互式发布(会逐项确认 bind mount / 敏感数据 / env 声明)
docker compose alpha publish myorg/myapp:1.0.0
# CI/脚本化发布:自动应答 + 包含 env 文件 + 固定镜像 digest
docker compose alpha publish --with-env --resolve-image-digests -y myorg/myapp:1.0.0
# 发布含全部镜像的完整应用
docker compose alpha publish --app -y myorg/myapp:1.0.0
# 先演练不推送
docker compose alpha publish --dry-run myorg/myapp:1.0.0
结合源码可确认的使用约束:
- 服务必须带
image字段,纯build服务会被整体拒绝,需先构建并推送镜像; - 不允许本地
include,多文件请通过extends组织(extends 父文件会随制品一起发布); -y会跳过所有确认(包括敏感数据确认),在 CI 中应确保 Compose 文件中没有字面量密钥;- 插值占位符(
${VAR})会被原样发布,消费方需自行提供对应环境变量——这正是--with-env与符号插值之间的取舍点。
九、相关代码与测试索引
| 关注点 | 位置 |
|---|---|
| 命令与 flag 定义 | cmd/compose/publish.go |
| 发布主流程、图层构建、预检 | pkg/compose/publish.go |
| OCI media type 与 manifest 生成 | internal/oci/push.go |
| 注册表解析器与镜像 Copy | internal/oci/resolver.go |
| YAML 路径重写(保格式) | pkg/compose/transform/replace.go |
| 单元测试(图层/预检/digest 解析) | pkg/compose/publish_test.go |
| 端到端场景测试 | pkg/e2e/publish_test.go |
| 样例数据 | pkg/compose/testdata/publish/compose.yaml |
| 命令参考文档 | docs/reference/compose_alpha_publish.md |
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