Docker 构建失败时,很多人第一反应是 Dockerfile 写错了。
但 NousResearch/hermes-agent issue #13925 的坑更隐蔽:第一次 docker compose build 成功,容器跑过一次后,下一次 build 反而失败。
错误类似:
ERROR: failed to build: failed to solve: invalid file request data/.config/pulse/<host>-runtime
看起来像 Docker 本身抽风。
实际是:运行时数据被 bind mount 到项目目录里的 ./data,但 .dockerignore 没排除 data/。容器在 /opt/data 里写出的 dangling symlink 又被下一次 build 当成 build context 打包,最后 Docker tar packer 走到坏链接时直接报错。
一句话:
运行时垃圾进了构建上下文。
复现场景:第一次 build 没事,跑过容器后再 build 就炸
issue 给出的复现很清楚。
先用 compose bind mount:
services:
hermes:
build: .
image: hermes-agent:local
stdin_open: true
tty: true
volumes:
- ./data:/opt/data
第一次构建:
docker compose build
通常成功,因为此时 data/ 还是空的。
然后运行一次容器:
docker compose run --rm hermes doctor
容器可能在 /opt/data 里写入 XDG / PulseAudio 相关状态。
宿主机上就会看到类似:
ls -la data/.config/pulse/
出现:
<host>-runtime -> /tmp/pulse-<random>
这个 symlink 的目标在容器里存在,但在宿主机上不存在。
于是它变成了 dangling symlink。
下一次:
docker compose build
Docker 打包 build context 时走到这个坏链接,就报:
invalid file request
根因:data/ 是运行时目录,却没进 .dockerignore
Hermes Dockerfile 声明了:
VOLUME /opt/data
compose 又把宿主机的:
./data
挂到容器的:
/opt/data
这本身没问题。
问题是 .dockerignore 没有排除:
data/
所以 Docker build 时会把项目目录下的 data/ 也纳入构建上下文。
但 data/ 不是源代码。
它是运行时状态目录,里面可能包含:
- dangling symlinks;
- Unix sockets;
- character devices;
- log files;
- session history;
- memory cache;
- browser / audio / XDG runtime 文件。
这些东西不应该被送进 Docker daemon 作为 build context。
为什么 .gitignore 有还不够?
issue 里还有一个很重要的点:
仓库的 .gitignore 已经包含:
data/
说明项目知道 data/ 是运行时数据,不应该进 Git。
但 Docker build 不看 .gitignore。
Docker 看的是:
.dockerignore
这两个文件不是一回事。
所以会出现这种割裂:
Git 不提交 data/,但 Docker build 仍然打包 data/。
这也是很多 Docker 项目会踩的坑。
最小修复:把 data/ 加进 .dockerignore
issue 提出的修复非常小:
# Environment files
.env
*.md
+
+# Runtime data (bind-mounted at /opt/data; must not leak into build context)
+data/
这行的意义不只是避免 broken symlink。
它还会让 build context 更干净:
- 不再上传日志;
- 不再上传 session;
- 不再上传 memory state;
- 不再上传 browser/audio 临时文件;
- 不再随着运行时间越来越膨胀。
从构建速度、隐私和稳定性上都更合理。
为什么 PulseAudio 会触发?
issue 里提到常见触发点是 PulseAudio。
它会在运行时生成类似:
data/.config/pulse/<host>-runtime -> /tmp/pulse-<random>
这个 target 在容器命名空间里有意义,在宿主机上却可能不存在。
对宿主机 Docker build context packer 来说,它就是一个坏链接。
但这个问题不只限于 PulseAudio。
任何容器写到 /opt/data 的非普通文件都可能触发:
- socket;
- device;
- dangling symlink;
- 特殊 runtime 文件。
所以根因不是 PulseAudio,而是 runtime data 泄漏进 build context。
排查时怎么确认是这个问题?
如果你遇到:
invalid file request data/...
可以先检查:
find data -xtype l -ls
这会找出 dangling symlink。
也可以看 PulseAudio 目录:
ls -la data/.config/pulse/
如果看到类似:
*-runtime -> /tmp/pulse-...
基本就对上了。
再看 .dockerignore:
grep -n '^data/' .dockerignore
如果没有,建议加上。
临时解决方案
方案一:清理 data/ 里的坏链接
find data -xtype l -delete
然后再:
docker compose build
但这只是临时修复。
方案二:把 data/ 加进 .dockerignore
printf '\n# Runtime data\ndata/\n' >> .dockerignore
这是根治方向。
方案三:把运行时数据放到项目目录外
例如:
volumes:
- /var/lib/hermes/data:/opt/data
这样 build context 根本不会扫描到运行时数据。
方案四:用 named volume
volumes:
hermes-data:/opt/data
volumes:
hermes-data:
如果不需要直接在宿主项目目录里看数据,named volume 更干净。
对 DeepAI API 中转站用户的边界说明
这篇问题发生在 Docker build context 和运行时目录隔离层,不在模型 API 层。
DeepAI API 中转站可以作为 Hermes 或其他 Agent 的 OpenAI-compatible 模型入口,解决:
- Base URL;
- API Key;
- 模型调用;
- 上游模型切换。
但它不能修复:
.dockerignore漏掉data/;- bind mount 把 runtime state 写回项目目录;
- broken symlink 进入 Docker build context;
- Docker tar packer 报
invalid file request。
所以看到 Docker build 报错时,要先看构建上下文,不要把问题归到模型 API。
FAQ
为什么第一次 build 成功,第二次失败?
第一次 data/ 可能是空的。容器运行后写入 runtime files,下一次 build 时这些文件进入 build context,触发错误。
为什么 .gitignore 里有 data/ 还会影响 Docker?
Docker build 不看 .gitignore,它看 .dockerignore。
invalid file request data/.config/pulse/... 是什么?
通常是 Docker 打包 build context 时遇到了宿主机上的 dangling symlink 或非普通文件。
删除 data/ 可以吗?
可以临时解决,但可能会删掉运行时状态。更推荐把 data/ 加入 .dockerignore,或把数据目录放到项目目录外。
DeepAI 能解决这个问题吗?
不能。这是 Docker 构建上下文问题,不是模型 API 问题。
总结
#13925 的核心教训是:
运行时目录不要进构建上下文。
./data:/opt/data 适合保存 Hermes 运行状态,但 data/ 必须被 .dockerignore 排除。
否则容器写出来的 symlink、socket、日志和缓存,下一次 build 都可能变成 Docker 的负担,甚至直接让构建失败。