hermes-s6-container-supervision について
Hermes の Docker 起動とサービス監視を保守する開発者向けスキルです。s6-overlay の構成、初期化スクリプト、プロファイル別ゲートウェイの復元や、主プロセスと監視対象サービスの役割を解説します。
主な機能
- 01s6-overlay の起動・監視構成の解説
- 02初期化とプロファイル復元の説明
- 03プロファイル別ゲートウェイ枠の管理
- 04状態確認とトラブルシューティング
活用シーン
- 01コンテナ再起動後のゲートウェイ停止の調査
- 02Docker 静的サービスの追加・変更
- 03コンテナ初期化や起動引数の変更
- 04s6 サービス状態の確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ nousresearch/ hermes-agent --skill hermes-s6-container-supervisionインストール後、Claude Code で/hermes-s6-container-supervisionを入力して読み込まれたか確認します。
詳細
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 ["/opt/hermes/docker/entrypoint-dispatch.sh"] |
docker/entrypoint-dispatch.sh | PID 1 のディスパッチャーです。イメージが PID 1 を所有する場合は /init と main-wrapper を exec し、ラップされたランタイム(Fly Machines、docker run --init)では stage2-hook と main-wrapper に直接フォールバックし、その前に s6 ヘルパーの PATH を復元します(#38349)。 |
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 のユーザーバンドルが有効になるようスロットが存在します。main hermes は監視対象のサービスではなく、CMD として実行されます。 |
docker/s6-rc.d/dashboard/run | 条件付きサービスです。HERMES_DASHBOARD が真値でない場合は exec sleep infinity を実行します。 |
docker/entrypoint.sh | stage2 hook を exec する後方互換性用シムです。以前のエントリーポイントパスをハードコードしていた外部スクリプトも引き続き動作します。 |
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 を採用する理由(s6-supervised ではなく CMD をメインプログラムにする)
当初の計画(v1~v3)では、メインの hermes を監視対象の s6-rc サービスとして実行する予定でした。しかし、s6-overlay v3 の実際の動作上の仕組みが 2 つの障壁となりました。
- cont-init.d スクリプトには CMD 引数が渡されません。そのため、stage2 フックでは
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 ["/opt/hermes/docker/entrypoint-dispatch.sh"] は、PID 1 で /init /opt/hermes/docker/main-wrapper.sh "$@" を exec します。ラッパーはユーザー引数の前に自動的に追加されるため、docker run <image> --version は /init main-wrapper.sh --version となり、--version が /init の POSIX シェルに横取りされることはありません。ラッパーは s6-setuidgid を使って hermes に権限を落とし、選択されたプログラムを exec します。プログラムの終了コードはそのままコンテナの終了コードとなり、s6 導入前の tini の契約と完全に一致します。エントリーポイントが PID 1 ではない場合(Fly Machines、docker run --init)、ディスパッチャーは /init を完全にスキップします(can only run as pid 1 で異常終了するためです)。s6 ヘルパーの PATH を復元し、stage2-hook.sh を実行してから main-wrapper.sh を直接 exec します。この経路では監視対象のサービスは実行されません(#38349)。
トレードオフ:メインの hermes は s6 による監視対象外です。これは s6 導入前のイメージで tini のもとで動作していたときとまったく同じです。新たに保証されるのは Dashboard の監視のみです。一方、/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を使用します)。- 特に root 権限が必要な場合を除き、run の先頭で
s6-setuidgid hermesを使って hermes に権限を落とします。 - ベースバンドルの起動を待つように、空の
docker/s6-rc.d/<name>/dependencies.d/baseを作成します。 - user バンドルに含めるため、空の
docker/s6-rc.d/user/contents.d/<name>を作成します。 - Dockerfile の
COPY docker/s6-rc.d/が自動的に取り込みます。ほかに変更は必要ありません。
プロファイルごとのゲートウェイ実行コマンドを変更する
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
# Expect 19 passed, 0 xfailed against the s6 image
ハーネスは tests/docker/ にあり、Docker が利用できない場合はスキップされます。テストごとのタイムアウトは 180s に延長されています(tests/docker/conftest.py を参照)。
よくある落とし穴
docker exec で「command not found」が発生する
s6-overlay がバイナリを配置する /command/ は、監視ツリーによって起動されたプロセス(サービス、cont-init.d、main-wrapper.sh)にのみ PATH として設定されています。docker exec <c> s6-svstat … は「command not found」で失敗するため、必ず絶対パス /command/s6-svstat を使用してください。hermes バイナリが使えるのは、Dockerfile が実行時の ENV PATH に /opt/hermes/.venv/bin を追加しているためです。
プロファイルディレクトリの所有者
cont-init リコンサイラーは hermes として実行されます(02-reconcile-profiles 内の s6-setuidgid hermes)。プロファイルディレクトリの所有者が root になると(例:docker exec <c> hermes profile create … がデフォルトで root として実行された場合)、リコンサイラーは 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' を確認してください。
ゲートウェイが起動後すぐに終了する(svstat に down (exitcode 1) と表示される)
プロファイルにモデルまたは認証情報が設定されていない可能性が最も高いです。サービススロットは正常で、ゲートウェイ自体が未設定です。まず hermes -p <profile> setup を実行してください。s6 スーパーバイザーはゲートウェイの再起動を続けます。これは意図した動作です(設定を修正すると、次の起動が成功し、そのまま稼働します)。
リコンサイラーがプロファイルをスキップした
リコンサイラーは、SOUL.md の存在を「実際のプロファイル」を示すマーカーとして使用します。hermes profile create は必ずこのファイルを作成します。プロファイルディレクトリに SOUL.md がない場合(余分なディレクトリ、部分的な復元、バックアップ中など)、リコンサイラーは意図的にスキップします。再び対象にするには、SOUL.md を追加してください(空のファイルでも構いません)。
「助けて、コンテナが 143 で終了します!」
s6-svscanctl -t または /run/s6/basedir/bin/halt を呼び出しているものがないか確認してください。どちらも /init にステージ 3 のシャットダウンを開始させますが、意図した終了コードではなく 143(SIGTERM)を返します。これは、アーキテクチャを A から B に切り替えた Phase 2 の変更によるものです。実際の終了コードでコンテナをシャットダウンするには、CMD(main-wrapper.sh)が通常どおり終了するようにしてください。finish スクリプトから終了を制御しようとしないでください。
関連スキル
hermes-agent-dev:hermes-agent コードベースの一般的なナビゲーションhermes-tool-quirks:Hermes ツール固有の回避策(sed/grep など)。s6 スタックと hermes 組み込みツールとの連携をデバッグする際に読み込んでください。