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

Compose 卷名后加子目录报 undefined volume

命名卷短语法不会把斜杠后的内容当作子目录。核对 Engine 与 Compose 版本,使用 volume.subpath,并在挂载前准备目录、验证卷身份和数据位置。

Compose 中把挂载写成 vol-web/reports:/data,启动时却报 refers to undefined volume vol-web/reports: invalid compose project。顶层已经声明了 vol-web,继续补声明仍不符合“只挂载卷里的 reports 目录”的本意。

这里的斜杠不会让 Compose 自动把来源拆成卷名和子目录。应使用卷挂载长语法,把来源卷与 volume.subpath 分开,并确保该目录在挂载前已经存在。

下面针对 Linux 容器、普通 docker compose 与本地命名卷。Engine 从 26.0 引入卷子目录挂载,Compose 从 2.26.0 支持传递这项配置。不要将这些条件直接套到旧版 docker-compose、Swarm 部署或特定网络卷驱动上。

先核对客户端、服务端和卷身份

docker version
docker compose version
docker volume inspect demo-shared-data

demo-shared-data 是示例卷名。使用远程 Docker context 时,要看实际连接的服务端版本,而不是只看本机 CLI。卷也属于该 Docker 服务端,不能根据本机目录是否存在推断远端卷的内容。

短语法中的 vol-web/reports 被当作完整的卷引用,而不是已声明卷的子目录。将顶层键也改成这个字符串并不能建立所需的子目录挂载关系。若配置字段能解析,但容器仍看到整个卷,还应检查 Compose 版本;只升级 Engine 并不足以保证旧客户端会传递 subpath。

将卷和目录写到各自字段

下面的 compose.yaml 使用一个已经存在的卷,容器只读取其中的 reports 目录:

services:
  reader:
    image: alpine:3.22
    command: ["ls", "-la", "/data"]
    volumes:
      - type: volume
        source: shared-data
        target: /data
        read_only: true
        volume:
          subpath: reports
          nocopy: true

volumes:
  shared-data:
    external: true
    name: demo-shared-data

这里 source 引用顶层键 shared-data,实际 Docker 卷名由 name 指定;reports 是卷内部的目录。external: true 要求卷预先存在,避免误把 Compose 新建的项目卷当作原数据卷。

nocopy: true 关闭从容器目标路径向卷复制初始内容的行为;它既不会创建 reports,也不会替你迁移原来放在卷根目录的数据。read_only 是这个检查容器的访问限制,真实应用是否需要写入应按用途另行设置。

示例服务只是列出目录,不是常驻应用。换入自己的应用时,保留其镜像、启动参数和用户配置,只针对已经确认的挂载项修改。

子目录必须在首次挂载前存在

挂载不存在的 subpath 会失败。不要通过反复重建应用容器等待它自动出现,也不要让应用在 /data 中执行 mkdir reports:当挂载目标本身尚不能建立时,应用根本还没有进入这个步骤。

对于已有业务卷,先备份并只读检查根目录。下面只读列出示例卷,不复制镜像内的初始内容:

docker run --rm \
  --mount type=volume,src=demo-shared-data,dst=/volume,readonly,volume-nocopy \
  alpine:3.22 ls -la /volume

先前的 docker volume inspect 必须已经确认卷存在且身份正确。不要跳过这一步,把打错的卷名当作现有数据使用。

如果确认这个卷应当新增 reports,该路径尚不存在,并已安排好数据备份和目录归属,可以用一次性辅助容器创建目录:

docker run --rm \
  --mount type=volume,src=demo-shared-data,dst=/volume,volume-nocopy \
  alpine:3.22 mkdir /volume/reports

这里故意不使用 mkdir -p:同名对象已存在时先停下来检查,避免掩盖已有文件或链接。示例通常会创建 root 所有的目录;要让非 root 应用写入,必须按应用实际 UID/GID 和现有权限策略安排初始化,不要直接对整个共享卷递归改权限。

如果数据原本就在卷根目录,创建一个空 reports 不会把这些数据移动进去。此时先决定是否需要迁移、停止哪些写入者、如何恢复目录布局,再调整挂载;不能把应用看到空目录当成数据已经丢失,也不能立即初始化覆盖。

验证看到的是子目录

先验证配置,再运行上面的只读检查服务:

docker compose config --quiet
docker compose run --rm reader

配置检查成功只能证明模型可接受。第二条命令实际挂载后,应列出卷内 reports 的内容,且不应出现卷根目录中与它并列的其他目录。用已知的非敏感文件核对路径,比单看容器启动成功更可靠。

迁移到业务服务前,再按真实应用用户验证所需的读写权限。子目录挂载不等于修复文件权限,也不替代共享卷数据的一致性设计。

回退时恢复原来已经验证的挂载配置,并按变更记录确认应用重新看到正确位置。仅改回 YAML 不会撤销已经移动或写入的数据;数据布局改变要按备份和停写计划恢复。不要用 docker compose down -v 或删除卷来处理 subpath 配置错误。