首页
/ Docker Compose publish 命令详解:把 Compose 应用发布为 OCI 工件

Docker Compose publish 命令详解:把 Compose 应用发布为 OCI 工件

2026-09-05 15:34:39作者:彭桢灵Jeremy

docker compose publish 用于将一个 Compose 项目整体发布到 OCI 兼容的镜像仓库:命令会先对 Compose 文件做安全性预检(敏感数据、环境变量、bind mount 声明),再把 Compose 文件及其引用的 env 文件、extends 父文件序列化为一组 OCI 层,推送到目标仓库,生成一个以 application/vnd.docker.compose.project 为 artifact type 的 OCI 工件。读完本文,你将掌握 publish 各参数的含义与使用前提、工件在仓库中的实际结构,以及如何用 docker compose -f oci://... 拉回并发布(pull 后 up)这个工件。

命令与参数速览

命令语法为:

docker compose publish [OPTIONS] REPOSITORY[:TAG]

其中唯一的必填位置参数是目标仓库引用(REPOSITORY[:TAG]),必须已具备该仓库的推送权限。参数一览(来自 docs/reference/compose_publish.md,并结合 cmd/compose/publish.go 中的 flag 定义补充):

参数 类型 默认值 说明
--app bool false 发布完整应用工件(额外附带一个引用全部服务镜像的 OCI image index)
--dry-run bool false 以 dry run 模式执行,只走加载、预检与序列化流程,不实际推送
--oci-version string 自动判定 指定生成的 OCI image/artifact 规范版本,默认自动判定
--resolve-image-digests bool false 将镜像 tag 解析并固定为 digest
--with-env bool false 将环境变量文件(env file)内容一并写入发布的 OCI 工件,并静默 env 相关确认提示
-y, --yes bool false 对所有确认提示自动回答 "yes"
--insecure-registry bool false 以明文 HTTP 访问仓库(隐藏参数,源码注释标明仅供测试用途)

几个源码中可以确认的实现细节:

  • --app 打开时,runPublish 会强制 ResolveImageDigests: opts.resolveImageDigests || opts.app(见 cmd/compose/publish.go),即发布完整应用时镜像引用必然固定为 digest,保证 index 指向的镜像不可漂移。
  • -y 会把 AlwaysOkPrompt() 注入服务选项(cmd/compose/publish.go),跳过 bind mount、敏感数据、env 等全部交互确认;源码还保留了对误写的 --y 的向后兼容与弃用警告(cmd/compose/publish.go)。
  • --insecure-registry 在 flag 注册后被 MarkHidden,注释明确写着 "Should only be used for testing purpose, we don't want to promote use of insecure registries"(cmd/compose/publish.go)。
  • 仓库加载阶段如果检测到本地 include 指令,runPublish 会直接报错 cannot publish compose file with local includescmd/compose/publish.go),因为 include 内容无法作为独立工件分发。

发布前预检:四类安全检查

publish 的入口实现是 pkg/compose/publish.go 中的 Publish/publish 方法。执行推送前,preChecks 会依次做四件事(pkg/compose/publish.go):

1. 拒绝纯 build 服务

checkOnlyBuildSection 会检查每个服务:如果 Image 为空且只有 build 段,整个项目无法发布,直接报错并列出这些服务名:

your Compose stack cannot be published as it only contains a build section for service(s):
- "serviceA"

原因很直接:发布工件只固化 image 引用,消费者无法在无构建上下文的环境中重现构建。

2. bind mount 声明确认

checkForBindMount 收集所有服务的 bind 类型卷(pkg/compose/publish.go),若存在则交互式提示:

you are about to publish bind mounts declaration within your OCI artifact.
only the bind mount declarations will be added to the OCI artifact (not content)
please double check that you are not mounting potential user's sensitive directories or data
Are you ok to publish these bind mount declarations?

注意只发布声明而不发布内容——即消费者的 /host/path 会映射到他自己的宿主路径,提示的目的是防止把含敏感路径的声明扩散出去。

3. 敏感数据扫描

checkForSensitiveData 使用 DefangLabs 的 secret-detector 扫描四类内容(pkg/compose/publish.go):

  • 所有 Compose 文件(以未插值的原始 YAML 重新序列化后扫描,检查用户真实写入的字面量);
  • 各服务声明的 env 文件(文件缺失时,仅 required: true 才报错,可选 env 文件缺失则跳过);
  • configs: 中由 file: 定义的资源;
  • secrets: 中由 file: 定义的资源。

命中后逐项列出类型与键值并要求确认,例如 e2e 测试 pkg/e2e/publish_test.go 验证的输出覆盖了 AWS Client ID、AWS Secret Key、Github authentication、JSON Web Token、Private Key 等类别。拒绝(输入 n)会使整个发布以 operation cancelled(退出码 130)终止,--yes 则全部自动确认。

4. 环境变量与 config content 泄漏检查

checkEnvironmentVariables 针对"即将被序列化进工件的每一个 Compose 文件"(含 extends 父文件)做更细粒度的 env 检查(pkg/compose/publish.go):

  • env_file 声明:任何服务声明了 env_file 都会列出(service "X": env_file declared),因为文件路径本身即本地环境信息;
  • 可疑键名字面量:用关键字检测器(password、secret、token、api_key 等键名)扫描 environment 的字面值,如 MYSQL_ROOT_PASSWORD: toto 会被标记为 service "db": literal value for "MYSQL_ROOT_PASSWORD"
  • 插值安全"${DB_PASSWORD}""$API_KEY" 这类插值在工件中保持符号形态、不泄漏解析后的值,因此不会触发提示(pkg/compose/publish.go 的注释与 pkg/compose/publish_test.go 中 "interpolated values are silent" 用例均确认了这一点);$$ 转义(字面 $)会被还原判断,"pa$$word" 这类仍按字面量告警;
  • 字面 config.content:内联 config 内容若为字面量(非插值模板),单独弹出一个确认框,该检查与 --with-env 解耦——--with-env 只控制环境变量相关提示。

提示文案中还写明了出口:"Use --with-env to silence this prompt and always publish env declarations.",这些行为由 pkg/e2e/publish_test.go 的场景测试逐一固化。

工件内容:层是如何生成的

预检通过后,publish 会调用非导出的 s.push(而非公开 Push)先把项目引用的镜像推送到仓库(IgnoreFailures: true, ImageMandatory: truepkg/compose/publish.go)——即每个服务声明的 image 必须可推送成功,这是 ImageMandatory 的语义。

随后 createLayers 构建工件的层(pkg/compose/publish.go):

  1. Compose 文件层:每个顶层 Compose 文件经过 processFile 处理——以原始环境重新加载后做两项改写:
    • env_file 路径被替换为路径字符串 SHA-256 哈希命名的 <hash>.env 占位符,真实文件仅在 --with-env 时作为独立层上传(media type application/vnd.docker.compose.envfile)。文件缺失但 required: false 时,占位符仍写入(保证工件不泄漏本地路径且行为一致),只是不注册上传,这一点由 pkg/compose/publish_test.goTest_processFile_optional_env_file_missing 验证;
    • extends.file 同样被改写为 <hash>.yaml 占位符,父文件作为带 com.docker.compose.extends: "true" 注解的层递归上传(pkg/compose/publish.go)。因此工件中不存在发布者的绝对/相对路径。
  2. env 文件层(仅 --with-env):由 envFileLayersenvFiles map 中登记的文件逐一打包(pkg/compose/publish.go)。
  3. digest override 层(仅 --resolve-image-digests--app):generateImageDigestsOverride 通过 ImageDigestResolver 对服务镜像、pre_start hook 镜像、type: image 卷源镜像逐一 DistributionInspect,生成一份只含 image: repo@sha256:... 的 override YAML(pkg/compose/publish.go,覆盖范围由 pkg/compose/publish_test.go 的 mock 测试确认)。

单元测试 Test_createLayerspkg/compose/publish_test.go)展示了改写后的实际形态:extends.file 变为 f8f9ede3...d390c.yamlenv_file 变为 5efca9cd...451af3.env,且各层带有 com.docker.compose.file / com.docker.compose.envfile 注解。测试数据可参考 pkg/compose/testdata/publish/compose.yaml

OCI 工件结构与版本兼容

层与清单的推送在 internal/oci/push.go 中完成。工件的关键常量:

常量 用途
ComposeProjectArtifactType application/vnd.docker.compose.project OCI 1.1 manifest 的 artifactType 字段
ComposeYAMLMediaType application/vnd.docker.compose.file+yaml Compose 文件层的 media type
ComposeEmptyConfigMediaType application/vnd.docker.compose.config.empty.v1+json OCI 1.0 模式下的空 config media type
ComposeEnvFileMediaType application/vnd.docker.compose.envfile env 文件层的 media type

PushManifest 的版本策略(internal/oci/push.go):

  • 未指定 --oci-version 时,先尝试按 OCI 1.1 生成清单(带 artifactType、空 JSON config);
  • 若仓库返回非认证类的 4xx(说明仓库不支持 1.1 特性),自动回退为 OCI 1.0 形态:artifactType 省略,config 使用 ComposeEmptyConfigMediaType,让依赖 config.mediaType 识别工件的旧工具链仍能认出这是 Compose 工件(internal/oci/push.go);
  • --oci-version 显式取值 1.0 / 1.1pkg/api/api.go)时跳过探测。API 注释还说明:Compose 目前会基于域名对 ECR 仓库自动使用 OCI 1.0 模式(pkg/api/api.go)。

--app:可整体拉取的应用 index

--app 会额外推送一个 image index(pkg/compose/publish.go):

  • 对每个服务的 image 执行 registry-to-registry 复制引用(oci.Copy),把所有服务镜像聚合成一个 MediaTypeImageIndex
  • 该 index 的 Subject 指向 Compose 项目 manifest,artifactType 同样为 application/vnd.docker.compose.project,并带 com.docker.compose.version 注解;
  • 由于强制 resolve digests,消费者拉取 index 时拿到的镜像内容与发布时刻完全一致。

这样"应用 = Compose 定义 + 全部镜像"成为一个仓库内自包含的整体。

端到端验证:发布后如何消费

e2e 测试 TestPublishpkg/e2e/publish_test.go)给出了完整的发布—消费闭环,可以直接照搬到本地实操:

# 1. 起一个本地 registry:3
docker run --name reg -P -d registry:3

# 2. 发布 Compose 项目(fixture 为 compose.yaml + compose-override.yaml)
docker compose -f compose.yaml -f compose-override.yaml \
  -p myapp publish --with-env --yes <registry-host:port>/test:test

# 3. 用 oci:// URI 直接以该工件为 compose 文件查看解析结果
docker compose -f oci://<registry-host:port>/test:test config

# 4. 甚至可以直接 up
docker compose -f oci://<registry-host:port>/test:test up

测试断言了 config 输出与原始 Compose 语义一致(服务环境、镜像、网络),并特别覆盖了一个回归场景:up 会二次加载远程工件来枚举待插值变量,此时必须继承 --insecure-registry(对应 pkg/e2e/publish_test.go 中对 docker/compose#13824 的注释)。发布用的 fixture 可参考 pkg/e2e/fixtures/publish/oci/compose.yaml(其中 extends.yaml 作为 extends 父文件同目录存放)。

各预检路径的 e2e 断言同样可查:bind mount 提示接受/拒绝(pkg/e2e/publish_test.go)、纯 build 项目被拒(pkg/e2e/publish_test.go)、本地 include 被拒(pkg/e2e/publish_test.go)、敏感数据全类别列出(pkg/e2e/publish_test.go)。

使用前提与注意事项小结

  • 适用版本前提:以当前仓库 github.com/docker/compose/v5 的源码为准;--app--with-env、digest 固定等行为均属该版本实现,旧版 CLI 行为可能不同。
  • 硬性限制:含本地 include 的项目、纯 buildimage 的服务无法发布,命令会直接报错而非进入交互。
  • 安全默认值:默认不发布 env 文件内容;env_file 声明、可疑键名环境字面量、字面 config.content、bind mount 声明、扫出的密钥都会触发交互确认,-y 会跳过所有确认,CI 场景下请先确认声明内容确实可公开。
  • 仓库兼容性:绝大多数场景无需关心 --oci-version,自动"1.1 优先、4xx 回退 1.0"已覆盖主流仓库;仅当目标仓库行为异常或需要复现工件形态时才显式指定。
  • 路径安全设计:发布的工件中不含发布者本地路径——env 文件与 extends 父文件一律以路径哈希占位符引用(pkg/compose/publish.go),这是消费端可以安全复用该工件的前提。
登录后查看全文
热门项目推荐
相关项目推荐