DEBUG.WIKI
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
雨云;给项目一台云服务器
RackNerd;$21.99/年 · 1GB KVM

Docker Compose 2.24.5 多层 extends 报 Circular reference

Docker Compose 2.24.5 的多层 extends 可能误报循环引用。区分真实引用环与历史版本故障,用 config 校验原入口,并在更新插件后复核完整模型及依赖资源。

同一套配置在 Docker Compose 2.24.2 中可以解析,升级到 2.24.5 后,多层 extends 却报循环引用,甚至显示入口文件在继承自身:

Circular reference:
  app_service in /path/to/final.yaml
  extends app_service in /path/to/final.yaml

这不一定意味着配置真的绕回了自身。Compose 2.24.5 存在相关解析问题,2.24.6 已修复这次问题;但真正的 extends 循环仍然不受支持。应同时核对实际调用的 Compose 版本,以及“文件加服务名”组成的完整引用链。

本文针对这个历史版本故障,已知环境包括 Ubuntu 22.04、Docker Engine 25.0.3 与 Compose 插件 2.24.5。不要把所有版本中的 Circular reference 都归到同一个缺陷,也不要把升级到任意新版本理解成可以容忍真实循环。

同名服务不等于引用同一个节点

下面是用于分析继承关系的最小配置,三个文件放在同一目录。它只定义一个打印文字后退出的示例服务,不用于部署业务。

base.yaml:

services:
  app_base:
    image: alpine
    command: ["echo", "Hello from Base Service"]

intermediate.yaml:

services:
  app_service:
    extends:
      file: base.yaml
      service: app_base
    environment:
      APP_MODE: intermediate

final.yaml:

services:
  app_service:
    extends:
      file: intermediate.yaml
      service: app_service

引用顺序是:final.yaml 中的 app_service,读取 intermediate.yaml 中的 app_service,后者再读取 base.yaml 中的 app_base。最底层没有 extends,链条到这里结束。前两层服务名相同,但文件不同,不构成自引用。

相反,若最后一个节点又引用回前面的同一文件、同一服务,才是真正的环。还有一种容易忽略的情况:extends 只写 service 而没有 file 时,会在当前配置文件里找目标服务;如果名称又是它自己,就可能形成直接自引用。

逐层列出真实文件与服务名,而不是只比较名字。示例把文件放在同一目录,是为了排除路径干扰;实际项目还要核对相对路径、入口文件及工作目录,不能仅凭错误文本中重复的路径就直接改服务名。

用 config 隔离解析问题

在实际执行部署的终端或 CI 环境中检查:

docker compose version
docker compose -f final.yaml config --quiet

config --quiet 只校验配置,不创建或启动服务。若在这里就得到循环引用错误,故障仍在配置解析阶段,不需要先排查容器网络、镜像中的程序或服务健康检查。

确认使用的是带空格的 docker compose 插件命令。宿主机的版本不能代表 CI 镜像或另一台开发机里的版本;更新完成后也要在原来的执行环境中重新运行版本命令。

应保持入口参数一致。业务命令若使用多个 -f、指定环境文件或其他项目参数,校验时也要带上同样的参数。为了验证而换成另一个入口文件,即使成功,也不能证明原部署路径已经恢复。

如果引用链确实闭合,应修改配置,把公共部分放到不再引用上层的基础服务中;如果链条没有环、使用的恰好是 2.24.5,并且此前 2.24.2 能解析同一输入,则应优先验证版本修复。这个对照不能替代检查环境变量和实际读取的文件是否相同。

更新执行解析的 Compose 插件

2.24.6 是这次历史问题的修复节点,不是现在部署新环境时推荐长期固定的版本。应按现有安装渠道选择适合项目的维护版本,先在本机或 CI 测试环境验证完整配置,再安排生产变更。

对于已经配置 Docker 官方软件仓库、通过包管理器安装插件的 Ubuntu 或 Debian,可按该渠道更新:

sudo apt-get update
sudo apt-get install docker-compose-plugin
docker compose version

这组命令不适用于所有安装方式。Docker Desktop 应通过它自身的更新渠道处理;手工安装的插件需要按原安装位置维护。不要在不了解当前安装来源时,把下载的二进制覆盖到多个目录里。验收以原执行环境实际打印的 Compose 版本为准,不以安装命令退出成功为准。

更新前记录当前版本,保存配置、环境文件和原安装包或二进制;更新后先重新运行:

docker compose -f final.yaml config --quiet
docker compose -f final.yaml config

第一条应通过校验。第二条用于审阅最终模型:示例中的 app_service 应保留底层镜像与命令,以及中间层的 APP_MODE,继承关系应已展开。输出可能包含插值后的敏感配置,不要直接贴到公开工单或日志。

业务项目还要核对挂载、网络、端口、依赖及环境变量,而不只是看循环错误消失。解析通过只证明配置模型能够生成,业务启动和功能验收仍应在原有发布流程中完成。

如果升级到 2.24.6 后,错误变成:

service "app_service" can't be used with `extends` as it declare `depends_on`

这与原来的循环引用提示不同,涉及该历史解析器对被继承服务的限制。不能据此认定循环修复无效,也不要为消除错误直接删除业务所需的启动依赖。

当前 Compose 规则允许被继承服务声明资源依赖,但不会自动把这些依赖资源导入最终模型。维护旧环境时需要继续评估后续版本;在当前环境中,则应明确把所需服务、网络、卷等声明到最终模型,并对照生成结果检查。不要把 2.24.6 的这个限制写成所有 Compose 版本的永久规则。

如果不得不整理配置,可以把公共基础配置与最终项目的服务依赖分开:基础层保存镜像、环境等复用项,最终层声明所需依赖。调整后必须保留原依赖的含义,包括健康条件;不能只求 YAML 能通过解析。

回退只处理插件与配置,不清理业务数据

若候选版本改变了其他解析行为,停止后续部署,使用事先保存的安装包或二进制恢复原插件,再用原版本和原配置重新校验。仅为比较解析结果而回退到旧版,应作为短期验证,不宜把已知存在问题的版本继续当成长期方案。

更换 Compose 插件不等于回退已经创建或更新过的业务容器。若还未运行部署命令,保留当前业务状态即可;若已经更新服务,应按该项目已有的应用回退流程恢复,不能仅降插件版本就宣布完成回退。

这个错误发生在解析继承关系时,不需要通过删除卷、清理镜像或重建 Docker 数据目录来处理。也不要改用 docker stack deploy 作为绕过办法,它不支持 extends,与这里的 Compose 解析场景不同。