Hermes S6 容器監管
修改、調試或擴展 Hermes Agent Docker 鏡像中的 s6-overlay 監管樹——添加新服務、調試配置文件網關、理解架構 B 主程序模式。
技能元數據
| 來源 | 可選 — 使用 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 |
| 相關技能 | hermes-agent, hermes-agent-dev |
參考:完整 SKILL.md
以下是 Hermes 在觸發此技能時加載的完整技能定義。這是技能激活時代理看到的指令。
Hermes s6-overlay 容器監管
何時使用此技能
在處理以下情況時加載此技能:
- 在 Hermes Docker 鏡像中添加或刪除靜態服務(應在每次容器啟動時受監管的內容,如儀表板)
- 診斷為何按配置文件的網關未啟動、重啟或在
docker restart後無法存活 - 理解為何容器的 CMD 是
/opt/hermes/docker/main-wrapper.sh以及帶前導破折號的參數如何傳遞給用戶程序 - 修改
cont-init.d啟動腳本(UID 重映射、卷種子填充、配置文件協調) - 更改按配置文件網關的渲染運行腳本(第 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)
關鍵文件
| 路徑 | 角色 |
|---|---|
Dockerfile | s6-overlay 安裝 + cont-init.d 接線 + ENTRYPOINT ["/init", "/opt/hermes/docker/main-wrapper.sh"] |
docker/stage2-hook.sh | “舊入口點邏輯”——UID 重映射、chown、種子填充、技能同步。作為 cont-init.d/01-hermes-setup 運行。 |
docker/cont-init.d/02-reconcile-profiles | 在每次啟動時調用 hermes_cli.container_boot,以從持久卷恢復配置文件網關槽位。 |
docker/main-wrapper.sh | 容器的 CMD。路由用戶參數,通過 s6-setuidgid 降級到 hermes,exec 執行所選程序。 |
docker/s6-rc.d/main-hermes/run | 無操作 sleep infinity——存在該槽位是為了使 s6-rc 用戶 bundle 有效;主 hermes 作為 CMD 運行,而非作為受監管的服務。 |
docker/s6-rc.d/dashboard/run | 條件服務——除非 HERMES_DASHBOARD 為真值,否則執行 exec sleep infinity。 |
docker/entrypoint.sh | 向後兼容墊片,exec 執行 stage2 hook。硬編碼舊入口點路徑的外部腳本仍然有效。 |
hermes_cli/service_manager.py | S6ServiceManager:register_profile_gateway, unregister_profile_gateway, start/stop/restart/is_running, list_profile_gateways。 |
hermes_cli/container_boot.py | reconcile_profile_gateways()——遍歷持久配置文件,重新生成 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 機制阻礙了這一點:
- cont-init.d 腳本不接收 CMD 參數——因此 stage2 hook 無法解析
docker run <image> chat -q "hi"來設置供服務run腳本使用的HERMES_ARGS。 /run/s6/basedir/bin/halt不會傳播寫入/run/s6-linux-init-container-results/exitcode的退出碼。無論何種情況,容器始終以 143 (SIGTERM) 退出。s6 作者 skarnet 在 issue #477 中確認:“如果你想要容器關閉,你需要要麼讓你的 CMD 退出,或者,如果你沒有 CMD,則寫入你想要的容器退出碼然後調用 halt”。
因此,我們使用 s6-overlay 原生的 CMD 模式:ENTRYPOINT ["/init", "/opt/hermes/docker/main-wrapper.sh"]。/init 自動將 wrapper 前置到用戶參數之前——因此 docker run <image> --version 變為 /init main-wrapper.sh --version,且 --version 不會被 /init 的 POSIX shell 攔截。wrapper 通過 s6-setuidgid 降級到 hermes,然後 exec 執行所選程序。程序的退出碼成為容器退出碼,完全符合 pre-s6 tini 契約。
權衡:主 hermes 在 s6 下不受監管。這與其在 tini(pre-s6 鏡像)下的行為完全一致。儀表板監管是唯一的新保證——而 /run/service/ 下的按配置文件網關獲得完全監管。
快速食譜
驗證運行中的容器中 s6 是否為 PID 1
docker exec <c> sh -c 'cat /proc/1/comm; readlink /proc/1/exe'
# Expect: s6-svscan or init / /package/admin/s6/.../s6-svscan
檢查配置文件網關服務
# /command/ isn't on docker-exec PATH — use absolute path
docker exec <c> /command/s6-svstat /run/service/gateway-<name>
# "up (pid …) … seconds" → running
# "down (exitcode N) … seconds, normally up, want up, …" → s6 wants it up but the process keeps exiting (crash loop)
# "down … normally up, ready …" → user stopped it
手動啟動/停止服務
docker exec <c> /command/s6-svc -u /run/service/gateway-<name> # up
docker exec <c> /command/s6-svc -d /run/service/gateway-<name> # down
docker exec <c> /command/s6-svc -t /run/service/gateway-<name> # SIGTERM (restart)
查看 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
添加新的靜態服務
- 創建
docker/s6-rc.d/<name>/type,內容為longrun\n,並創建docker/s6-rc.d/<name>/run(使用#!/command/with-contenv sh+# shellcheck shell=sh)。 - 在 run 腳本頂部通過
s6-setuidgid hermes切換到 hermes 用戶(除非你特別需要 root 權限)。 - 創建空的
docker/s6-rc.d/<name>/dependencies.d/base,使其等待 base bundle。 - 創建空的
docker/s6-rc.d/user/contents.d/<name>,使其加入 user bundle。 - Dockerfile 中的
COPY docker/s6-rc.d/會自動拾取它——無需其他更改。
更改每個 profile 的 gateway 運行命令
編輯 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 測試 harness
docker build -t hermes-agent-harness:latest .
HERMES_TEST_IMAGE=hermes-agent-harness:latest scripts/run_tests.sh tests/docker/ -v
# Expect 19 passed, 0 xfailed against the s6 image
該 harness 位於 tests/docker/ 中,當 Docker 不可用時會被跳過。每個測試的超時時間已增加到 180 秒(參見 tests/docker/conftest.py)。
常見陷阱
通過 docker exec 出現 "command not found"
/command/(s6-overlay 放置其二進制文件的位置)僅對由監督樹生成的進程(服務、cont-init.d、main-wrapper.sh)在 PATH 中可用。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 所有(例如,因為默認情況下以 root 身份運行了 docker exec <c> hermes profile create …),協調器將無法讀取 SOUL.md 並因 PermissionError 而失敗。緩解措施:stage2-hook.sh 在每次啟動時都將 $HERMES_HOME/profiles 的所有權更改為 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'。
Gateway 啟動後立即退出(svstat 中顯示 down (exitcode 1))
最可能的原因是 profile 沒有配置模型或認證。服務槽是正確的——gateway 本身未配置。首先運行 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) 而不是期望的退出代碼。這是從 A 到 B 的第二階段架構轉變。對於具有真實退出代碼的容器關閉,你必須讓 CMD (main-wrapper.sh) 正常退出;不要試圖從 finish 腳本控制退出。
相關技能
hermes-agent-dev:通用 hermes-agent 代碼庫導航hermes-tool-quirks:特定的 Hermes-tool 變通方法(sed/grep 等)——在調試 s6 棧與 hermes 內置工具的交互時加載。