{/* This page is auto-generated from the skill's SKILL.md by website/scripts/generate-skill-docs.py. Edit the source SKILL.md, not this page. */}

Hermes S6 Container Supervision

修改或调试 Hermes Docker 镜像中的 s6 服务。

Skill 元数据

来源可选——使用 hermes skills install official/devops/hermes-s6-container-supervision 安装
路径optional-skills/devops/hermes-s6-container-supervision
版本1.0.0
作者Hermes Agent
许可证MIT
平台linux
标签docker, s6, supervision, gateway, profiles
相关 skillhermes-agent

参考:完整 SKILL.md

INFO

以下是 Hermes 在触发此 skill 时加载的完整 skill 定义。这是 skill 激活时 agent 所看到的指令内容。

Hermes s6-overlay 容器监管

何时使用此 skill

当你正在处理以下工作时加载此 skill:

  • 在 Hermes Docker 镜像中添加或移除一个静态服务(每次容器启动都应被监管的东西,例如 dashboard)
  • 诊断某个按 profile 的网关为何不启动、不重启,或无法在 docker restart 后存活
  • 理解为什么容器的 CMD 是 /opt/hermes/docker/main-wrapper.sh,以及带前导短横线的参数如何到达用户的程序
  • 修改 cont-init.d 启动脚本(UID 重映射、卷种子填充、profile 对账)
  • 修改为按 profile 网关渲染的 run-script(Phase 4)

如果你只是运行 Hermes Agent 并想使用 Docker,请改看 website/docs/user-guide/docker.md。

架构一览

/init                                  ← PID 1 (s6-overlay v3.2.3.0)
├── cont-init.d                        ← oneshot setup, runs as root
│   ├── 01-hermes-setup                ← docker/stage2-hook.sh
│   │   ├── UID/GID remap
│   │   ├── chown /opt/data
│   │   ├── chown /opt/data/profiles (every boot)
│   │   ├── seed .env / config.yaml / SOUL.md
│   │   └── skills_sync.py
│   └── 02-reconcile-profiles          ← hermes_cli.container_boot
│       ├── chown /run/service (hermes-writable for runtime register)
│       └── walk $HERMES_HOME/profiles/<name>/gateway_state.json
│           → recreate /run/service/gateway-<name>/
│           → auto-start only those with prior_state == "running"
│
├── s6-rc.d (static services, in /etc/s6-overlay/s6-rc.d/)
│   ├── main-hermes/run                ← exec sleep infinity (no-op slot)
│   └── dashboard/run                  ← if HERMES_DASHBOARD=1, runs `hermes dashboard`
│
├── /run/service (s6-svscan watches; tmpfs)
│   ├── gateway-coder/                 ← runtime-registered per-profile
│   │   ├── type        ("longrun")
│   │   ├── run         ("#!/command/with-contenv sh ... exec s6-setuidgid hermes hermes -p coder gateway run")
│   │   ├── down        (marker — present means "registered but don't auto-start")
│   │   └── log/run     (s6-log → $HERMES_HOME/logs/gateways/coder/current)
│   └── ...
│
└── CMD ("main program")               ← /opt/hermes/docker/main-wrapper.sh
    └── routes user args: bare exec | hermes subcommand | hermes (no args)
        — exec'd by /init with stdin/stdout/stderr inherited (TTY for --tui)

关键文件

路径作用
Dockerfiles6-overlay 安装 + cont-init.d 接线 + ENTRYPOINT ["/opt/hermes/docker/entrypoint-dispatch.sh"]
docker/entrypoint-dispatch.shPID-1 分发器:当镜像占用 PID 1 时,exec /init + main-wrapper;在被包装的运行时(Fly Machines、docker run --init)上,则直接回退到 stage2-hook + main-wrapper,并先恢复 s6 辅助工具 PATH(#38349)。
docker/stage2-hook.sh"旧的 entrypoint 逻辑"——UID 重映射、chown、种子填充、skills 同步。作为 cont-init.d/01-hermes-setup 运行。
docker/cont-init.d/02-reconcile-profiles每次启动调用 hermes_cli.container_boot,从持久化卷恢复 profile 网关槽位。
docker/main-wrapper.sh容器的 CMD。路由用户参数,通过 s6-setuidgid 降权到 hermes,再 exec 选定的程序。
docker/s6-rc.d/main-hermes/run空操作 sleep infinity——槽位存在是为了让 s6-rc user bundle 合法;主 hermes 作为 CMD 运行,而非被监管的服务。
docker/s6-rc.d/dashboard/run条件服务——除非 HERMES_DASHBOARD 为真,否则 exec sleep infinity。
docker/entrypoint.sh向后兼容垫片,exec 到 stage2 hook。那些硬编码了旧 entrypoint 路径的外部脚本仍然可用。
hermes_cli/service_manager.pyS6ServiceManager:register_profile_gateway、unregister_profile_gateway、start/stop/restart/is_running、list_profile_gateways。
hermes_cli/container_boot.pyreconcile_profile_gateways()——遍历持久化 profiles,重新生成 s6 槽位,输出 container-boot.log。
hermes_cli/gateway.py::_dispatch_via_service_manager_if_s6拦截 hermes gateway start/stop/restart,在容器中运行时路由到 s6。

为什么采用架构 B(CMD 作为主程序,而非由 s6 监管)

最初的计划(v1–v3)要求主 hermes 作为受监管的 s6-rc 服务运行。s6-overlay v3 的两个真实机制阻断了这条路:

  1. cont-init.d 脚本收不到 CMD 参数——因此 stage2 hook 无法解析 docker run <image> chat -q "hi" 来设置 HERMES_ARGS 供服务的 run 脚本消费。
  2. /run/s6/basedir/bin/halt 不会传播写入 /run/s6-linux-init-container-results/exitcode 的退出码。无论如何容器总是以 143(SIGTERM)退出。skarnet(s6 作者)在 issue #477 中确认:"if you want a container shutdown, you need to either have your CMD exit, or, if you have no CMD, write the container exit code you want then call halt"。

因此我们通过分发器采用 s6-overlay 原生的 CMD 模式:ENTRYPOINT ["/opt/hermes/docker/entrypoint-dispatch.sh"],它在 PID 1 下 exec /init /opt/hermes/docker/main-wrapper.sh "$@"。wrapper 会被自动前置到用户参数之前——所以 docker run <image> --version 变成 /init main-wrapper.sh --version,而 --version 不会被 /init 的 POSIX shell 拦截。wrapper 通过 s6-setuidgid 降权到 hermes,然后 exec 选定的程序。程序的退出码即容器退出码,与 s6 之前的 tini 契约完全一致。当 entrypoint 不是 PID 1 时(Fly Machines、docker run --init),分发器完全跳过 /init(否则会以 can only run as pid 1 中止),恢复 s6 辅助工具 PATH,运行 stage2-hook.sh,并直接 exec main-wrapper.sh——这条路径上没有受监管的服务(#38349)。

权衡:主 hermes 在 s6 下不受监管。这与它在 tini(s6 之前的镜像)下的行为完全一致。Dashboard 监管是唯一的新增保证——而 /run/service/ 下的按 profile 网关获得完整监管。

快速配方

验证运行中的容器里 s6 是否为 PID 1

docker exec <c> sh -c 'cat /proc/1/comm; readlink /proc/1/exe'
# 期望:s6-svscan 或 init / /package/admin/s6/.../s6-svscan

查看某个 profile 网关服务

# /command/ 不在 docker-exec 的 PATH 上——使用绝对路径
docker exec <c> /command/s6-svstat /run/service/gateway-<name>
# "up (pid …) … seconds"            → 运行中
# "down (exitcode N) … seconds, normally up, want up, …" → s6 希望它起来但进程持续退出(崩溃循环)
# "down … normally up, ready …"     → 用户手动停止了它

手动拉起 / 停止服务

docker exec <c> /command/s6-svc -u /run/service/gateway-<name>   # 拉起
docker exec <c> /command/s6-svc -d /run/service/gateway-<name>   # 停止
docker exec <c> /command/s6-svc -t /run/service/gateway-<name>    # SIGTERM(重启)

查看 cont-init 对账日志

docker exec <c> tail -n 50 /opt/data/logs/container-boot.log
# 2026-05-21T06:18:05+0000 profile=coder prior_state=running action=started
# 2026-05-21T06:18:05+0000 profile=writer prior_state=stopped action=registered

新增一个静态服务

  1. 创建 docker/s6-rc.d/<name>/type,内容为 longrun\n;创建 docker/s6-rc.d/<name>/run(使用 #!/command/with-contenv sh + # shellcheck shell=sh)。
  2. 在 run 顶部通过 s6-setuidgid hermes 降权到 hermes(除非你明确需要 root)。
  3. 创建空的 docker/s6-rc.d/<name>/dependencies.d/base,使其等待 base bundle。
  4. 创建空的 docker/s6-rc.d/user/contents.d/<name>,使其加入 user bundle。
  5. Dockerfile 中的 COPY docker/s6-rc.d/ 会自动收录它——无需其他改动。

修改按 profile 网关的运行命令

编辑 hermes_cli/service_manager.py 中的 S6ServiceManager._render_run_script。该函数在启动对账时也被 hermes_cli/container_boot.py::_register_service 调用,因此它是唯一权威来源。相应更新 tests/hermes_cli/test_service_manager.py::test_s6_register_creates_service_dir_and_triggers_scan 中的断言。

运行 docker 测试套件

docker build -t hermes-agent-harness:latest .
HERMES_TEST_IMAGE=hermes-agent-harness:latest scripts/run_tests.sh tests/docker/ -v
# 期望在 s6 镜像上:19 passed, 0 xfailed

测试套件位于 tests/docker/,在 Docker 不可用时会跳过。每个测试的超时已提高到 180s(见 tests/docker/conftest.py)。

常见陷阱

通过 docker exec 报 "command not found"

/command/(s6-overlay 放置其二进制的位置)仅对监管树派生的进程在 PATH 上——服务、cont-init.d、main-wrapper.sh。docker exec <c> s6-svstat … 会报 "command not found";始终使用绝对路径 /command/s6-svstat。hermes 二进制可用是因为 Dockerfile 把 /opt/hermes/.venv/bin 加进了运行时 ENV PATH。

Profile 目录归属

cont-init 对账器以 hermes 身份运行(02-reconcile-profiles 中的 s6-setuidgid hermes)。如果某个 profile 目录最终归 root 所有(例如因为 docker exec <c> hermes profile create … 默认以 root 运行),对账器无法读取 SOUL.md 并报 PermissionError。缓解措施:stage2-hook.sh 在每次启动时幂等地把 $HERMES_HOME/profiles chown 给 hermes。不要删除该段。

通过 docker exec 写入的文件归 root 所有

docker exec 默认以 root 运行。要么传 --user hermes,要么等下次重启时 stage2 的 chown 清扫。不要手动以 root 在 $HERMES_HOME/profiles/<name>/ 下写文件——下次对账会清扫它们,但进行中的操作可能遇到权限错误。

服务槽位存在,但 s6-svstat 说 "s6-supervise not running"

服务目录在 tmpfs 上,容器重启时被清空。可能是 cont-init 对账器尚未运行(docker restart 后稍等片刻),或它失败了。查看 docker logs <c> | grep '02-reconcile'。

网关启动后立即退出(svstat 显示 down (exitcode 1))

很可能是该 profile 没有配置模型或认证。服务槽位是对的——网关本身未配置。先运行 hermes -p <profile> setup。s6 监管器会不断重启它;这正是期望行为(修好配置后,下一次尝试就会成功并保持运行)。

对账器跳过了某个 profile

对账器以是否存在 SOUL.md 作为"真实 profile"的标记。hermes profile create 总会生成它。如果某个 profile 目录缺少 SOUL.md(残留目录、部分恢复、备份进行中),对账器会有意跳过它。添加一个 SOUL.md(哪怕是空的)即可重新纳入。

"救命,容器退出码是 143!"

检查是否有东西在调用 s6-svscanctl -t 或 /run/s6/basedir/bin/halt——两者都会让 /init 进入第 3 阶段关闭,但返回 143(SIGTERM)而非期望的退出码。这就是 Phase 2 从架构 A 转向 B 的原因。要让容器以真实退出码关闭,必须让 CMD(main-wrapper.sh)正常退出;不要试图从 finish 脚本控制退出码。

相关 skill

  • hermes-agent-dev:通用的 hermes-agent 代码库导航
  • hermes-tool-quirks:特定的 Hermes 工具变通方法(sed/grep 等)——在调试 s6 栈与 hermes 内置工具的交互时加载。