Hermes の Docker 設定
目次
- イメージのチャンネルと、実行時に何を誰が持つか
- クイックスタート
- ゲートウェイとして動かす
- ダッシュボードを動かす
- 対話的に動かす(CLI のチャット)
- 残り続けるボリューム
- コンテナで state.db を置くファイルシステムの条件
- 書き換えないインストール先
- 複数プロファイルへの対応
- コンテナの外から複数のプロファイルへ届かせる
- なぜコンテナを増やさず、1 つで多くのプロファイルを抱えるのか
- それでもコンテナを分けたくなるとき
- ログの行き先
- 環境変数を渡す
- Docker Compose の例
- 任意: Linux デスクトップの音声の橋渡し
- 資源の上限
- Dockerfile が何をしているか
- docker exec は自動で hermes ユーザーへ落ちます
- プロファイルごとのゲートウェイ管理
- 更新する
- スキルと資格情報のファイル
- コンテナに道具を足す
- npm や Python の道具 — npx か uvx を使う
- そのほかの道具(apt のパッケージ、実行ファイル) — 入れて覚えさせる
- 長持ちさせる — 派生イメージを作る
- 込み入った道具や、複数のサービスの組み合わせ — サイドカーのコンテナを動かす
- 広く役に立つ道具 — issue か pull request を出す
- ローカルの推論サーバーにつなぐ(vLLM、Ollama など)
- Docker Compose(勧められる方法)
- 単独の docker run(Compose なし)
- つながっているか確かめる
- Ollama
- 困ったときは
- コンテナがすぐ終わってしまう
- 「Permission denied」のエラーが出る
- 共有したデータディレクトリが何度も 0700 に戻る
- どの docker exec でも「Permission denied」になる(インストール先が 0700 で閉じている)
- PID 1 の下にゾンビ(
)のプロセスがたまる - ブラウザの道具が動かない
- ネットワークの不調のあと、ゲートウェイがつなぎ直せない
- コンテナの様子を確かめる
Docker と Hermes Agent の関わり方には、はっきり違う 2 つがあります。
- Hermes を Docker の中で動かす — エージェント自身がコンテナの中で動きます(このページの主題です)
- Docker をターミナルのバックエンドとして使う — エージェントはホストで動き、コマンドはすべて 1 つの長生きする Docker のサンドボックスコンテナの中で実行されます。このコンテナは、Hermes のプロセスが生きているあいだ、ツールの呼び出しをまたいでも
/newをしてもサブエージェントを使っても残り続けます(設定 → Docker バックエンドを参照してください)
このページで扱うのは 1 のほうです。コンテナは、利用者のデータ(設定、API キー、セッション、スキル、記憶)をすべて、ホストから /opt/data にマウントした 1 つのディレクトリに置きます。イメージ自体は状態を持たないので、新しい版を取ってくれば、設定を失わずに入れ替えられます。
イメージのチャンネルと、実行時に何を誰が持つか
| イメージのタグ | 意味 |
|---|---|
latest / stable |
安定版のリリースの関門を通ったイメージです。 |
main |
main ブランチのビルドから公開される開発用のイメージです。 |
X.Y.Z |
版番号の付いた安定版のイメージです。配備する版をきっちり固定するなら digest を使ってください。 |
ワークフローは amd64 と arm64 のイメージをビルドしてテストします。安定版の公開では、ビルドし直さず、 テスト済みのイメージのアーカイブをそのまま使います。stable と latest を進めるのは、リリースの昇格を 最後まで通したときだけです。main への push では、これらのタグは進みません。
イメージの Python 環境は pyproject.toml と uv.lock に従います(いまは Python 3.14)。厳選した追加分は、ネイティブのデスクトップ版の一式が使う --all-extras とは別物です。Electron のデスクトップアプリは含みません。
クイックスタート
Hermes Agent を初めて動かすなら、ホストにデータ用のディレクトリを作り、対話的にコンテナを起動して設定ウィザードを走らせます。
mkdir -p ~/.hermes
docker run -it --rm \
-v ~/.hermes:/opt/data \
nousresearch/hermes-agent setupこれで設定ウィザードに入ります。API キーを尋ねられ、答えると ~/.hermes/.env へ書き込まれます。この作業は一度きりです。この時点で、ゲートウェイが相手にするチャットの仕組みも設定しておくことを強く勧めます。
ゲートウェイとして動かす
設定が済んだら、常駐のゲートウェイ(Telegram、Discord、Slack、WhatsApp など)としてコンテナを裏で動かします。
docker run -d \
--name hermes \
--restart unless-stopped \
-v ~/.hermes:/opt/data \
-p 8642:8642 \
nousresearch/hermes-agent gateway run8642 番のポートは、ゲートウェイの OpenAI 互換の API サーバーと稼働確認のエンドポイントを外に出します。チャットのプラットフォーム(Telegram、Discord など)しか使わないなら要りませんが、ダッシュボードや外部のツールからゲートウェイへ届かせたいなら必要です。
TUI も、ローカルのバックエンドとしてこの見守られたゲートウェイを使います。メッセージングのプラットフォームを何も設定していないときも同じです。対話的に --tui で立ち上げると、登録済みの s6 のゲートウェイが自動で起動し、準備が整うまで待ちます。通常の名前付きプロファイルは、自分を受け持つルートのゲートウェイにつながります。gateway.standalone: true を明示したプロファイルは、自分用に登録された枠を使います。クライアントの起動時に、サービスを入れたり、s6 を通さずに管理外のプロセスを立てたりすることはありません。
補足: API サーバーは API_SERVER_ENABLED=true で開きます。コンテナの中の 127.0.0.1 の外へ出すには、API_SERVER_HOST=0.0.0.0 と API_SERVER_KEY(8 文字以上。openssl rand -hex 32 で作れます)も設定してください。例を挙げます。
docker run -d \
--name hermes \
--restart unless-stopped \
-v ~/.hermes:/opt/data \
-p 8642:8642 \
-e API_SERVER_ENABLED=true \
-e API_SERVER_HOST=0.0.0.0 \
-e API_SERVER_KEY="$(openssl rand -hex 32)" \
-e API_SERVER_CORS_ORIGINS='*' \
nousresearch/hermes-agent gateway runインターネットに面した端末でポートを開けるのは、それ自体が危険です。危険を分かっていないなら開けないでください。
ダッシュボードを動かす
組み込みの Web ダッシュボードは、同じコンテナの中でゲートウェイと並んで、s6-rc に見守られるサービスとして動きます。立ち上げるには HERMES_DASHBOARD=1 を設定します。
docker run -d \
--name hermes \
--restart unless-stopped \
-v ~/.hermes:/opt/data \
-p 8642:8642 \
-p 9119:9119 \
-e HERMES_DASHBOARD=1 \
nousresearch/hermes-agent gateway runダッシュボードは s6 が見守ります。落ちたら s6-supervise が少し間を置いて自動で立ち上げ直します。ダッシュボードの標準出力と標準エラーは docker logs <container> へ流れます(接頭辞は付きません。ゲートウェイ自身の出力はプロファイルごとの s6 のログファイルへ移ったので、下のログの行き先を参照してください。2 つの流れがぶつかることはありません)。
| 環境変数 | 説明 | 既定 |
|---|---|---|
HERMES_DASHBOARD |
1(または true / yes)にすると、見守られるダッシュボードのサービスが有効になります |
*(未設定 — サービスは登録されるが止まったまま)* |
HERMES_DASHBOARD_HOST |
ダッシュボードの HTTP サーバーが待ち受けるアドレス | 0.0.0.0 |
HERMES_DASHBOARD_PORT |
ダッシュボードの HTTP サーバーのポート | 9119 |
HERMES_DASHBOARD_INSECURE |
廃止 / 何もしません。 以前は認証の関門を素通りさせていましたが、2026 年 6 月の強化以降、認証を無効にしません。ループバック以外で待ち受ける場合は、常に認証の提供元が必要です | *(無視されます — 代わりに提供元を設定してください)* |
コンテナの中のダッシュボードは、既定で 0.0.0.0 を待ち受けます。そうでなければ、公開した -p 9119:9119 のポートにホストから届きません。コンテナ内のループバックだけに絞りたいときは(サイドカーやリバースプロキシを使う構成など)、HERMES_DASHBOARD_HOST=127.0.0.1 を設定します。
ダッシュボードの認証の関門は、次の 2 つがどちらも成り立つときに自動で働きます。
- 待ち受けるアドレスがループバックでない(コンテナの中の既定の
0.0.0.0など)、かつ DashboardAuthProviderのプラグインが登録されている。
2 つ目を満たす方法は、最初から 3 つ用意されています。
- ユーザー名とパスワード — 信頼できるネットワークの中や VPN の後ろで、自分で立てた社内・自宅のコンテナに向く、いちばん簡単な方法です。
HERMES_DASHBOARD_BASIC_AUTH_USERNAMEとHERMES_DASHBOARD_BASIC_AUTH_PASSWORD(再起動をまたいでセッションを保つにはHERMES_DASHBOARD_BASIC_AUTH_SECRETも)を設定します。インターネットへ直接さらす用途には向きません。 - OAuth(Nous Portal) — ホスティングや一般公開の環境向けです。
HERMES_DASHBOARD_OAUTH_CLIENT_IDを設定するとdashboard_auth/nousの提供元が有効になります。 - 自前の OIDC — 標準の OpenID Connect で自分の認証基盤に問い合わせます。
HERMES_DASHBOARD_OIDC_ISSUERとHERMES_DASHBOARD_OIDC_CLIENT_IDを設定するとdashboard_auth/self_hostedの提供元が有効になります。
どれを選んでも、関門は保護された経路へ入る前にログインの画面へ案内します。3 つの提供元については Web ダッシュボード → 認証を参照してください。
Traefik や nginx のようなリバースプロキシが別のコンテナで動いている場合、その ブリッジネットワーク上のアドレスは既定では信頼されません。マウントした config.yaml にダッシュボードの公開 URL を書き、そのプロキシの正確な IP か、専用のプロキシ用ネットワークなら範囲を限った CIDR だけを信頼させてください。
dashboard:
public_url: "https://dashboard.example.com"
trusted_proxies:
- "172.20.0.5"
# Or, if the proxy address is dynamic on a dedicated network:
# - "172.20.0.0/24"こうすると、そのプロキシの X-Forwarded-Proto: https が安全な OAuth の クッキーを左右できるようになり、他の相手からの転送ヘッダは信頼されないままになります。*、0.0.0.0/0、::/0 は使わないでください。範囲を限っていないこれらの指定を Hermes は拒みます。
提供元が 1 つも登録されておらず、ループバック以外で待ち受けている場合、ダッシュボードは起動の時点で安全側に倒れて止まり、足りない環境変数を指し示すエラーを出します。公開のアドレスで認証なしにダッシュボードを出す抜け道は、もうありません。HERMES_DASHBOARD_INSECURE=1 は廃止され、何もしません(警告を出して無視されます)。提供元を設定するか、HERMES_DASHBOARD_HOST=127.0.0.1 で待ち受けて SSH のトンネルや Tailscale 越しにダッシュボードへ届くようにしてください。
ダッシュボードを別のコンテナで動かすことはできますが、そのコンテナがホストの PID と ネットワークの名前空間を共有している必要があります(たとえば network_mode: host。リポジトリ自身の docker-compose.yml の dashboard サービスがそうしています)。ゲートウェイが生きているかどうかの検出には、ゲートウェイのプロセスと PID の名前空間を共有していることが要るので、この制限が当てはまるのは、PID の名前空間を共有しないブリッジネットワークの独立したコンテナで動かすダッシュボードだけです。
対話的に動かす(CLI のチャット)
すでにあるデータのディレクトリに対して対話的なチャットを開くには、こうします。
docker run -it --rm \
-v ~/.hermes:/opt/data \
nousresearch/hermes-agent動いているコンテナの中でターミナルをすでに開いているなら(Docker Desktop などから)、これだけです。
/opt/hermes/.venv/bin/hermes残り続けるボリューム
/opt/data のボリュームは、Hermes のすべての状態の正本です。ホストの ~/.hermes/ ディレクトリに対応し、次を含みます。
| パス | 中身 |
|---|---|
.env |
API キーと秘密 |
config.yaml |
Hermes のすべての設定 |
SOUL.md |
エージェントの人格・自己像 |
sessions/ |
会話の履歴 |
memories/ |
残り続ける記憶の置き場 |
skills/ |
入れてあるスキル |
home/ |
Hermes のツールのサブプロセス(git、ssh、gh、npm、スキルの CLI)が使う、プロファイルごとの HOME |
cron/ |
定時実行の定義 |
hooks/ |
イベントのフック |
logs/ |
実行時のログ |
skins/ |
CLI の外観 |
コンテナで state.db を置くファイルシステムの条件
Hermes はセッションを SQLite データベース(/opt/data/state.db)に保存しており、既定では WAL ジャーナルモードで開きます。WAL は、ファイルを開いているすべてのプロセスの間で共有メモリ(state.db-shm)の内容が食い違わないことを前提にしています。VM の境界をまたぐバインドマウントはこれを満たしません。virtiofs(macOS の Docker Desktop と Podman、OrbStack)と 9p / drvfs(Windows の Docker Desktop)では、複数のプロセスが同時に書き込むと WAL データベースが気づかないうちに壊れます。しかも本体のファイルは PRAGMA integrity_check を通ってしまいます。
Hermes の対処(v2026.9.14 以降):
- virtiofs / 9p のマウント上に新しく作るデータベースは、ロールバック(
DELETE)ジャーナルモードで作成し、警告を一度だけログに出します。こちらで何かする必要はありません。 - そうしたマウント上に既にある WAL データベースは、動いている最中に切り替えることはしません。ほかの Hermes プロセスが開いているかもしれず、稼働中に切り替えるとチェックポイント前のコミットが失われるからです。代わりに、各プロセスが起動時にエラーを一度だけログに出し、
hermes doctorがデータベースを指摘します。直し方は次の2つです。
1. そのデータベースを使っている Hermes のプロセスをすべて止めてから、イメージに同梱の Python で一度だけオフライン変換します(イメージに sqlite3 シェルはありません): docker exec hermes python3 -c "import sqlite3; print(sqlite3.connect('/opt/data/state.db').execute('PRAGMA journal_mode=DELETE').fetchone()[0])"。あとで開いたときに WAL へ戻らないよう、config.yaml に database.journal_mode: delete を設定してください。 2. データディレクトリをネイティブのボリュームへ移します。名前付きの Docker ボリューム(-v hermes-data:/opt/data)は VM 自身の ext4 ファイルシステム上にあり、WAL が普通に使えます。
検出はコンテナ内の /proc/self/mountinfo を読んで行うので、ホストの OS に関係なく働きます。NFS、SMB、一般的な FUSE のマウントは判別しません。これらの上では database.journal_mode: delete を明示的に設定してください。gateway、cron、ワーカーの各プロセスがデータベースを同時に開くため、Hermes は SQLite の locking_mode=EXCLUSIVE を代わりの手段として用意していません。
書き換えないインストール先
ホスティング用および公開されている Docker のイメージでは、/opt/hermes がアプリケーションの入っている場所です。root の持ち物で、動作中の hermes ユーザーからは読むだけなので、エージェントのやり取り、ゲートウェイのセッション、ダッシュボードの操作、ふつうの docker exec hermes hermes ... のコマンドからは、中核のソース、同梱の .venv、node_modules、TUI の一式をその場で書き換えられません。
書き換わる Hermes の状態はすべて /opt/data の下に属します。設定、.env、プロファイル、スキル、記憶、セッション、ログ、ダッシュボードへの投入物、プラグイン、その他あなたが管理するファイルです。イメージは実行時の .pyc の書き出しと、/opt/hermes への Hermes の遅延依存インストールも止めています。公開イメージに必要な追加のプラットフォーム依存は、イメージに焼き込むか、イメージを作り直して入れてください。
ホスティング用・公開のイメージでは、エージェントの自己改善は /opt/data の下のスキル・記憶・プラグイン・設定に限られます。/opt/hermes にある中核のソースは書き換えません。中核の変更はリポジトリへの PR で行い、イメージを更新して届けるのであって、動いているインストール先を生で書き換えるのではありません。
運用者が /opt/data の外のファイルを直したり調べたりする必要があるときは、意図して root のシェルを使ってください。hermes の橋渡しは、ふだん docker exec hermes hermes ... を動作中のユーザーへ落とします。root の権限が本当に要る一回きりの実行では、HERMES_DOCKER_EXEC_AS_ROOT=1 を設定してください。
~ の下に資格情報を置くスキルの CLI は、データのボリュームの根っこではなく、サブプロセスの HOME に対して用意する必要があります。たとえば xurl のスキルは OAuth の状態を ~/.xurl に置きますが、公式の Docker の配置では Hermes のツールの呼び出しはそれを /opt/data/home/.xurl として読みます。ですから xurl の認証を手で行うときは HOME=/opt/data/home を付け、HOME=/opt/data/home xurl auth status で確かめてください。
複数プロファイルへの対応
Hermes は複数のプロファイルに対応しています。~/.hermes/ の下の別々のディレクトリで、1 つのインストールから独立したエージェント(SOUL、スキル、記憶、セッション、資格情報がそれぞれ別)を動かせます。公式の Docker イメージの中では、s6 の見守りの木はプロファイルを一人前の見守り対象として扱うので、勧められる形はすべてのプロファイルを 1 つのコンテナで抱えることです。
hermes profile create <name> で作ったプロファイルには、次が付きます。
/run/service/gateway-<name>/に専用の s6 のサービス枠。実行時に動的に登録されるので、コンテナを作り直す必要はありません。- 落ちたときの自動再起動。間隔は
s6-superviseが調整します。 ${HERMES_HOME}/logs/gateways/<name>/currentにプロファイルごとの回転するログ(1 MB × 10 世代)。- コンテナの再起動をまたいだ状態の保存。起動時の調整役が各プロファイルのディレクトリから
gateway_state.jsonを読み、最後に記録された状態がrunningだったプロファイルの枠だけを立ち上げ直します。再起動しても止まったままになるのは、あなたが明示的に止めた(hermes gateway stop)ゲートウェイだけです。コンテナの再起動、イメージの更新、思わぬ終了では記録がrunningのまま残るので、次の起動でゲートウェイは自動で立ち上がります。
ホスト側から、バインドマウントした ~/.hermes に対して作ったプロファイルは、ディレクトリはできても枠はできません(ホストのプロセスからはコンテナの /run/service に手が届かないためです)。コンテナの中で hermes -p <name> gateway start を実行すると、足りない枠がその場で登録されて立ち上がります。docker restart は要りません。これをするのは start だけで、しかも本物のプロファイルのディレクトリ(SOUL.md があるもの)に限られます。登録されていないプロファイルへの stop/restart や、打ち間違えた -p の名前は、これまでどおり ✗ no such gateway で失敗します。
ホストで実行する操作のコマンドは、コンテナの中からも同じように使えます。
# Create a profile — registers the gateway-<name> s6 slot.
docker exec hermes hermes profile create coder
# Start / stop / restart — dispatches s6-svc; the gateway lifecycle survives docker restart.
docker exec hermes hermes -p coder gateway start
docker exec hermes hermes -p coder gateway stop
docker exec hermes hermes -p coder gateway restart
# Status — reports `Manager: s6 (container supervisor)` inside the container.
docker exec hermes hermes -p coder gateway status
# Remove a profile — tears down the s6 slot too.
docker exec hermes hermes profile delete coder裏では、コンテナの中の hermes gateway start/stop/restart は横取りされ、正しいサービスのディレクトリに対する s6-svc へ渡されます。s6 のコマンドを直接覚える必要はありません。見守り役の生の状態を見たいときは /command/s6-svstat /run/service/gateway-<name> を使ってください(/command/ が PATH に入るのは見守りの木から生まれたプロセスだけなので、docker exec から呼ぶときは絶対パスで渡します)。
コンテナの外から複数のプロファイルへ届かせる
外からプロファイルのゲートウェイへ届く道は 2 つあり、それぞれ動きが違います。混同しないでください。
Hermes デスクトップ(と Web ダッシュボード)。 デスクトップアプリの Remote Gateway の接続がしゃべる相手は hermes dashboard のバックエンド(既定は 9119 番のポート、HERMES_DASHBOARD=1 で有効になります)で、OpenAI の API サーバー*ではありません*。1 つのダッシュボードのバックエンドが、同じ場所にあるすべてのプロファイルを引き受けます。アプリのプロファイル切り替えが要求ごとに宛先のプロファイルを送り、バックエンドがディスク上のそのプロファイルの HERMES_HOME を開くからです。ですからデスクトップのために、プロファイルごとに 2 つ目のポートや 2 つ目の接続を用意する必要はありません。:9119 の 1 本の接続が、切り替えを通してすべてをまかないます。
OpenAI 互換のクライアント(Open WebUI、LobeChat、/v1/...)。 これらがしゃべる相手はプロファイルごとの API サーバーで、こちらはどのプロファイルでも 8642 番のポートを掴みます(API_SERVER_PORT か platforms.api_server.extra.port から決まります。自動の割り当ても、config.yaml の gateway.port のような項目もありません)。クライアントから*特定の* 2 つ目のプロファイルへ届かせたいなら、そのプロファイル自身の .env に別の API_SERVER_PORT を与えてください。そうしないと、そのゲートウェイも 8642 を掴もうとして既定のプロファイルとぶつかります。
# Create the profile (registers its gateway-<name> s6 slot)
docker exec hermes hermes profile create work
# Point its API server at a free port (write to the profile's own .env)
cat >> /opt/data/profiles/work/.env <<'EOF'
API_SERVER_ENABLED=true
API_SERVER_PORT=8643
EOF
docker exec hermes hermes -p work gateway restartAPI_SERVER_PORT は各プロファイル自身の .env に書き、コンテナ全体の environment: の塊には決して書かないでください。全体の値を置くと、すべてのプロファイルが同じポートへ押し込まれ、ぶつかります。ブリッジのネットワークを使っているなら、追加のポートを docker-compose.yml で公開します(- "8643:8643")。network_mode: host ならすでにホストから届きます。既定のプロファイルの 8642 の接続はそのままです。
なぜコンテナを増やさず、1 つで多くのプロファイルを抱えるのか
s6 へ移る前は「プロファイルごとに 1 コンテナ」が勧められる形でした。コンテナの中に複数のゲートウェイを見守る役がいなかったからです。s6 が PID 1 になったいま、それはもう必要なく、1 つのコンテナにまとめる形がほとんどの面で簡単です。
| 1 コンテナに多くのプロファイル | プロファイルごとに 1 コンテナ | |
|---|---|---|
| ディスクの負担 | イメージ 1 つ、同梱の venv 1 つ、Playwright のキャッシュ 1 つ | N 個のイメージと N 個のキャッシュ |
| メモリの負担 | Python のインタプリタのキャッシュと node_modules を共有 | コンテナごとに重複 |
| プロファイルを作る | docker exec ... hermes profile create <name>(数秒) |
新しい docker run の実行 + ポートの割り当て + 設定のマウント |
| プロファイルごとの復旧 | s6-supervise の自動再起動 |
Docker の --restart unless-stopped(遅く、隣の仕事も巻き添えにする) |
| ログ | s6-log によるプロファイルごとの回転ファイルと、コンテナ起動の記録 |
コンテナごとの docker logs <name>。回転の仕組みはなし |
| バックアップ | ~/.hermes の 1 ディレクトリ |
揃えるべき N 個のディレクトリ |
既定のプロファイル(default)は最初の起動で必ず登録されるので、新しいコンテナは見守られたゲートウェイを 1 つ備えた状態で届きます。プロファイルを足すのは、純粋に実行時の作業です。
それでもコンテナを分けたくなるとき
コンテナの中でプロファイルを分けるのが既定です。プロファイルごとにコンテナを分けるのは、はっきりした理由があるときだけにしてください。
- 仕事ごとに資源を隔てたい — たとえば、プロファイル A で暴走したブラウザのツールのセッションが、プロファイル B のメモリを食い尽くさないようにしたい。コンテナならプロファイルごとに
--memoryや--cpusを指定できます。 - イメージの版を別々に固定したい — 仕事ごとに上流のイメージのタグを変えたい。
- ネットワークを分けたい — プロファイルごとに別の Docker のネットワーク(たとえば顧客向けと社内用)。
- 法令順守や被害の範囲 — 別々の資格情報が、OS のプロセスの木を共有しないようにしたい。
その場合は、container_name、volumes、ports をそれぞれ分けて、プロファイルごとに 1 つのサービスを書きます。
services:
hermes-work:
image: nousresearch/hermes-agent:latest
container_name: hermes-work
restart: unless-stopped
command: gateway run
ports:
- "8642:8642"
volumes:
- ~/.hermes-work:/opt/data
hermes-personal:
image: nousresearch/hermes-agent:latest
container_name: hermes-personal
restart: unless-stopped
command: gateway run
ports:
- "8643:8642"
volumes:
- ~/.hermes-personal:/opt/data残り続けるボリュームの警告はここでも生きています。2 つのコンテナを同じ ~/.hermes のディレクトリへ同時に向けてはいけません。コンテナの中の s6 の見守り役は、自分のプロファイルの一式を管理します。データのボリュームをコンテナ同士で共有すると、セッションのファイルと記憶の置き場が壊れます。
ログの行き先
s6 のコンテナにはログの出口が 4 つあり、「docker logs にゲートウェイの何も出ないのはなぜか」はよくある驚きです。早見表を挙げます。
| 出どころ | 行き先 | 読み方 |
|---|---|---|
プロファイルごとのゲートウェイ(hermes gateway run と、s6 の下のプロファイルごとのゲートウェイ) |
2 か所に分けて出ます。docker logs <container>(そのまま、余分な接頭辞なし)と ${HERMES_HOME}/logs/gateways/<profile>/current(回転あり、ISO-8601 の時刻付き、1 MB × 10 世代) |
ホストで docker logs -f hermes または tail -F ~/.hermes/logs/gateways/default/current |
ダッシュボード(HERMES_DASHBOARD=1 のとき) |
docker logs <container>(接頭辞なし) |
docker logs -f hermes。ゲートウェイの行と混ざります |
| 起動時の調整役(コンテナが立ち上がるたびに、どのプロファイルのゲートウェイを戻したかを記録します) | ${HERMES_HOME}/logs/container-boot.log(追記だけの記録) |
tail -F ~/.hermes/logs/container-boot.log |
Hermes 全体のログ(agent.log、errors.log) |
${HERMES_HOME}/logs/(プロファイルを見分けます) |
docker exec hermes hermes logs --follow [--level WARNING] [--session <id>] |
端末の入出力がある対話的なコンテナ(docker run -it ... --tui や bash など)では、前面で動くコマンドが端末を使います。エントリーポイントは既定で S6_LOGGING=2 と S6_CMD_USE_TERMINAL=1 を設定するので、見守られているゲートウェイとダッシュボードの出力がその画面を上書きすることはありません。両者のコンソール出力は、コンテナ内の /run/uncaught-logs/current にある、s6 の回転する受け皿のログへ送られます。プロファイルごとのゲートウェイのログファイルと、通常の Hermes のログは、上の表の場所でこれまでどおり見られます。受け皿のディレクトリは実行時だけの置き場なので、コンテナを再起動したり削除したりすると残りません。
人の見ていない(ヘッドレスの)コンテナでは、これまでどおりサービスの出力を docker logs へ流します。S6_LOGGING と S6_CMD_USE_TERMINAL を明示して設定した場合は、こうした対話時の既定値よりそちらが優先されます。
知っておくと役立つことが 2 つあります。
- コンテナの再起動をまたいで残るのは、
logs/gateways/<profile>/currentのファイルのほうです。docker logsはいまのコンテナが生きているあいだの出力しか保たず(docker rmで消えます)、回転するファイルはマウントしたボリュームに残ります。 - 起動時の調整役が残す行の形は
<iso-timestamp> profile=<name> prior_state=<state> action=<registered|started>なので、grep profile=coder ~/.hermes/logs/container-boot.logと打つだけで、そのプロファイルが最後にいつ戻されたのか、s6 が自動で立ち上げたのかが分かります。
環境変数を渡す
API キーは、コンテナの中の /opt/data/.env から読まれます。環境変数として直接渡すこともできます。
docker run -it --rm \
-v ~/.hermes:/opt/data \
-e ANTHROPIC_API_KEY="sk-ant-..." \
-e OPENAI_API_KEY="sk-..." \
nousresearch/hermes-agent-e で直接渡した値は .env の値より優先されます。キーをディスクに置きたくない CI/CD や秘密の管理サービスとの連携で役立ちます。
Docker Compose の例
ゲートウェイとダッシュボードの両方を常駐させるなら、docker-compose.yaml を使うと楽です。
services:
hermes:
image: nousresearch/hermes-agent:latest
container_name: hermes
restart: unless-stopped
command: gateway run
ports:
- "8642:8642" # gateway API
- "9119:9119" # dashboard (only reached when HERMES_DASHBOARD=1)
volumes:
- ~/.hermes:/opt/data
environment:
- HERMES_DASHBOARD=1
# Uncomment to forward specific env vars instead of using .env file:
# - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
# - OPENAI_API_KEY=${OPENAI_API_KEY}
# - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
deploy:
resources:
limits:
memory: 4G
cpus: "2.0"docker compose up -d で始め、docker compose logs -f でログを見ます。見守られているゲートウェイの標準出力は、ボリューム上の ${HERMES_HOME}/logs/gateways/<profile>/current にも分けて出ます。振り分けの全体像はログの行き先を参照してください。
任意: Linux デスクトップの音声の橋渡し
Docker の中で音声モードを使うには、2 つのことが要ります。コンテナの中で Hermes が音声のデバイスを調べられること、そしてコンテナからホストの音声サーバーへ届くことです。ここでは、PulseAudio 互換のソケットを出している Linux デスクトップ(多くの PipeWire の構成を含みます)向けに、ホスト側の音声の配管を説明します。
まず、Compose のファイルの隣に ALSA の設定を作ります。
pcm.!default {
type pulse
hint {
show on
description "Default ALSA Output (PulseAudio)"
}
}
pcm.pulse {
type pulse
}
ctl.!default {
type pulse
}次に、ALSA の PulseAudio プラグインを入れた小さな派生イメージを作ります。
FROM nousresearch/hermes-agent:latest
USER root
RUN apt-get update \
&& apt-get install -y --no-install-recommends libasound2-plugins \
&& rm -rf /var/lib/apt/lists/*そのイメージを Compose で使い、ホストのユーザーの PulseAudio のソケットとクッキーを渡します。
services:
hermes:
build:
context: .
dockerfile: Dockerfile.audio
image: hermes-agent-audio
container_name: hermes
restart: unless-stopped
command: gateway run
volumes:
- ~/.hermes:/opt/data
- /run/user/${HERMES_UID}/pulse:/run/user/${HERMES_UID}/pulse
# no-tmp: ok — path inside the container
- ~/.config/pulse/cookie:/tmp/pulse-cookie:ro
- ./asound.conf:/etc/asound.conf:ro
environment:
- HERMES_UID=${HERMES_UID}
- HERMES_GID=${HERMES_GID}
- XDG_RUNTIME_DIR=/run/user/${HERMES_UID}
- PULSE_SERVER=unix:/run/user/${HERMES_UID}/pulse/native
# no-tmp: ok — path inside the container
- PULSE_COOKIE=/tmp/pulse-cookieコンテナのプロセスがユーザーごとの音声のソケットへ届くよう、ホストの UID と GID を渡して起動します。
export HERMES_UID="$(id -u)"
export HERMES_GID="$(id -g)"
docker compose up -d --buildコンテナの中から PortAudio に何が見えているかを確かめるには、こうします。
docker exec hermes /opt/hermes/.venv/bin/python -c "import sounddevice as sd; print(sd.query_devices())"資源の上限
Hermes のコンテナが必要とする資源はほどほどです。勧められる下限を挙げます。
| 資源 | 最低 | 推奨 |
|---|---|---|
| メモリ | 1 GB | 2〜4 GB |
| CPU | 1 コア | 2 コア |
| ディスク(データのボリューム) | 500 MB | 2 GB 以上(セッションとスキルで増えます) |
いちばんメモリを食うのはブラウザの自動操作(Playwright / Chromium)です。ブラウザのツールが要らないなら 1 GB で足ります。使うなら 2 GB 以上を割り当ててください。
Docker で上限を決めるにはこうします。
docker run -d \
--name hermes \
--restart unless-stopped \
--memory=4g --cpus=2 \
-v ~/.hermes:/opt/data \
nousresearch/hermes-agent gateway runDockerfile が何をしているか
イメージは Debian 13.4 を使い、次を含みます。
- コミット済みの
uv.lockから揃えた Python 3.14 の環境。そのあと
Hermes 自身を依存なしの編集可能な形で入れます。
- 厳選した追加分
all、messaging、otlp、anthropic、bedrock、
azure-identity、matrix。これは --all-extras ではありません。
- digest で固定した Node のソースイメージから取った Node.js 26 と npm。
/opt/hermes/toolsに置いた、PM で版を固定した uv、完全版の Chromium、FFmpeg、ripgrep。- システムの Git、OpenSSH、Docker CLI、Chromium の共有ライブラリ。
- 作り置きの TUI / ダッシュボードの資材と、焼き込んだ Photon のサイドカーの依存。
- 見守りとゾンビプロセスの片付けのための s6-overlay。
Chromium は npx playwright install ではなく PM を通して用意します。ビルドは、 解決した実行ファイルの場所を /etc/hermes/agent-browser-executable-path に記録します。 PLAYWRIGHT_BROWSERS_PATH は、データのマウントの外にある /opt/hermes/tools を指します。
接尾辞の付かない(-desktop でない)タグも含め、どのイメージにも Playwright の軽い headless shell ではなく 完全版の Chromium が入っています。版を固定しチェックサムを確かめた 1 つのブラウザで、画面を出さない閲覧と、 画面を出す Bot Screen のセッションの両方をまかないます。その代わりにイメージは大きくなります。完全版は、 以前のイメージが積んでいた headless shell より大きいからです。
必要なときだけ使うバックエンドの SDK(Edge TTS、Firecrawl、Exa、プラットフォームのアダプター、プラグインの 依存)は、最初に使うときに /opt/data/installs の下の PM の依存の世代へ入ります。そのため、コンテナを作り直しても イメージを更新しても残ります。 イメージ自身の /opt/hermes/.venv は書き換えられません。起動のたびに、コンテナは記録された選択を 新しいイメージのロックに照らして解決し直してから、サービスを立ち上げます。これが失敗したとき(たとえばオフライン)は、 イメージ自身の環境で起動し、記録された追加分は次の起動やインストールのために取っておきます。 必要に応じたインストールを断るには security.allow_lazy_installs: false を設定してください。以前の lazy-packages の重ね合わせは使いません。
イメージの出どころの記録は、ソースのマウントとデータのマウントのどちらの外でもある /etc/hermes/image-provenance.json にあります。 ビルドの刻印は /opt/hermes/install-stamp.json にあります。 刻印を渡さずにローカルでビルドした場合は、コミットをこしらえず、リビジョンは不明と報告します。hermes update は イメージが持つコードの変更を断ります。アプリケーションを更新するには、イメージを入れ替えてください。
コンテナの ENTRYPOINT は小さな振り分け役(docker/entrypoint-dispatch.sh)です。コンテナが PID 1 を持っているとき(ふつうの Docker / Podman)は s6-overlay の /init を exec し、下で説明する見守りの木がまるごと手に入ります。プラットフォームがイメージの入口を自前の PID 1 の init で包んでいるとき(Fly.io Machines、docker run --init、一部の Nomad / Kubernetes の構成)、/init は s6-overlay-suexec: fatal: can only run as pid 1 で止まってしまうので、振り分け役は代わりに stage2 の下ごしらえを直接実行し、s6 なしで主のラッパーを exec します。この代わりの道でも、頼んだコマンドは動きますが、見守られるサービス(ダッシュボード、プロファイルごとのゲートウェイ)は使えません。
PID 1 の道では、/init は次を行います。
- root として
/etc/cont-init.d/01-hermes-setup(=docker/stage2-hook.sh)を実行します。UID / GID の付け替え(任意)、ボリュームの持ち主の修正、初回起動での.env/config.yaml/SOUL.mdの用意、HERMES_SKIP_CONFIG_MIGRATION=1でなければ対話なしの設定の移行、同梱スキルの反映を行います。 /etc/cont-init.d/02-reconcile-profiles(=hermes_cli.container_boot)を実行します。$HERMES_HOME/profiles/<name>/をたどり、プロファイルごとのゲートウェイの s6 のサービス枠を/run/service/gateway-<profile>/に作り直し、最後に記録された状態がrunningだったものだけを自動で立ち上げます(プロファイルごとのゲートウェイ管理を参照してください)。- 固定の
main-hermesとdashboardの s6-rc のサービスを始めます。 - コンテナの CMD を主のプログラムとして exec します(
/opt/hermes/docker/main-wrapper.sh)。これはdocker runに渡された引数を次のように振り分けます。
- 引数なし →
hermes(既定) - 最初の引数が PATH 上の実行ファイル(
sleep、bashなど)→ そのまま実行します - それ以外 →
hermes <args>(サブコマンドとして通します)
この主のプログラムが終わると、その終了コードでコンテナも終わります。
docker exec は自動で hermes ユーザーへ落ちます
docker exec hermes <cmd> は既定でコンテナの中の root として動きますが、イメージには /opt/hermes/bin/hermes という薄い橋渡しが入っていて(PATH のいちばん前にあります)、root からの呼び出しを見分けて s6-setuidgid hermes を通して透過的に実行し直します。ですから docker exec hermes hermes login、docker exec hermes hermes profile create …、docker exec hermes hermes setup などは、--user のフラグを足さなくても UID 10000 の持ち物としてファイルを書きます。つまり、見守られたゲートウェイから読める形です。root 以外からの呼び出し(見守られているプロセス自身、docker exec --user hermes、コンテナの中のカンバンのサブエージェント)は近道を通って venv の実行ファイルを直接呼ぶので、よく通る道に余計な負担はありません。
診断のためのセッション、root だけが見られる状態の確認、/opt/data の外にある root の持ち物のファイルなど、root の権限を保った docker exec がどうしても要るときは、その実行だけ外せます。
docker exec -e HERMES_DOCKER_EXEC_AS_ROOT=1 hermes <cmd>橋渡しが受け付けるのは 1 / true / yes(大文字小文字は問いません)です。それ以外は、=0 のような打ち間違いも含めてすべて落とす動きになるので、気づかないうちに外れることはありません。s6-setuidgid が使えない場合(s6-overlay を取り除いた独自のビルドなど)、橋渡しは root として動くことを拒み、代わりに 126 で終了します。壊れた権限の仕組みを黙って通すのではなく、はっきり見せるためです。かつては docker exec hermes login が auth.json を root:root として書き、見守られたゲートウェイの認証がどのチャットのプラットフォームでも壊れる、という落とし穴がありました。
プロファイルごとのゲートウェイ管理
hermes profile create <name> で作ったプロファイルには、/run/service/gateway-<name>/ に s6 が見守るゲートウェイのサービスが自動で登録され、コンテナの再起動をまたいで状態を保ったまま自動で立ち上がります。利用者から見た流れと操作のコマンドは、上の複数プロファイルへの対応を参照してください。
s6 より前のイメージに比べた、見守りの利点:
- ゲートウェイが落ちても、
s6-superviseがおよそ 1 秒待って自動で立ち上げ直します。 HERMES_DASHBOARD=1で有効にしたダッシュボードも同じ見守りの木に乗り、同じように自動で立ち上げ直されます。docker restart、イメージの更新(docker compose up -d --force-recreate)、思わぬ終了があっても、動いていたゲートウェイは保たれます。cont-init の調整役が$HERMES_HOME/profiles/<name>/gateway_state.jsonを読み、最後に記録された状態がrunningなら枠を立ち上げ直します。stoppedが記録され、再起動しても止まったままになるのは、明示的なhermes gateway stopだけです。再起動や更新のときにコンテナや s6 が送る SIGTERM は「まだ動いている」として扱われ、自動で立ち上がります。- プロファイルごとのゲートウェイのログは
$HERMES_HOME/logs/gateways/<profile>/currentに残り(s6-logが回転させます)、調整役の動きは起動のたびに$HERMES_HOME/logs/container-boot.logへ書き足されます。振り分けの全体像はログの行き先を参照してください。
コンテナの中の hermes status --full は Manager: s6 (container supervisor) と表示します。見守り役の生の様子を見たいときは /command/s6-svstat /run/service/gateway-<name> を使ってください(/command/ が PATH に入るのは見守りの木のプロセスだけなので、docker exec から呼ぶときは絶対パスで渡します)。
更新する
最新のイメージを取ってきて、コンテナを作り直します。データのディレクトリは そのまま残り、コンテナはゲートウェイを始める前に、マウントされた $HERMES_HOME/config.yaml に対して対話なしの設定の移行を行います。 移行が要るときは、Hermes がまず config.yaml と .env の隣に時刻の入ったバックアップを書きます。
docker pull nousresearch/hermes-agent:latest
docker rm -f hermes
docker run -d \
--name hermes \
--restart unless-stopped \
-v ~/.hermes:/opt/data \
nousresearch/hermes-agent gateway runDocker Compose ならこうです。
docker compose pull
docker compose up -dHERMES_SKIP_CONFIG_MIGRATION=1 は、新しいイメージに書き換えさせる前に、保存された設定を自分で確かめたり移行したりする必要があるときだけ設定してください。
スキルと資格情報のファイル
Docker を実行の場として使うとき(上で説明した使い方ではなく、エージェントが Docker のサンドボックスの中でコマンドを実行する場合です。設定 → Docker バックエンドを参照してください)、Hermes はすべてのツールの呼び出しで 1 つの長生きするコンテナを使い回し、スキルのディレクトリ(~/.hermes/skills/)と、スキルが宣言した資格情報のファイルを、そのコンテナへ読み取り専用のボリュームとして自動でマウントします。スキルのスクリプト・ひな形・資料は、何も設定しなくてもサンドボックスの中で使えます。コンテナは Hermes のプロセスが生きているあいだ残るので、入れた依存も書いたファイルも、次のツールの呼び出しまでそのままです。
同じ同期は SSH と Modal のバックエンドでも行われます。スキルと資格情報のファイルは、コマンドごとに rsync か Modal のマウント API で送られます。
コンテナに道具を足す
公式のイメージには選りすぐりの道具が入っていますが(Dockerfile が何をしているかを参照してください)、エージェントが使いたがるすべての道具が最初から入っているわけではありません。手間と長持ちの度合いが増える順に、勧められる方法が 5 つあります。
npm や Python の道具 — npx か uvx を使う
npm や PyPI で公開されている道具なら、npx(npm)か uvx(Python)で実行するよう Hermes に伝え、そのコマンドを記憶に残させてください。設定ファイルや資格情報が要る道具なら、それらを /opt/data の下(たとえば /opt/data/<tool>/config.yaml)に置くよう指示します。
依存は必要になったときに取ってきて、コンテナが生きているあいだキャッシュされます。/opt/data の下に書いた設定は、マウントされたホストのディレクトリにあるので、コンテナを再起動しても残ります。パッケージのキャッシュそのものは docker rm のあとに作り直されますが、npx と uvx は次に道具を動かすときに黙って取り直します。
そのほかの道具(apt のパッケージ、実行ファイル) — 入れて覚えさせる
実行時のユーザーは、システムの APT パッケージを入れられません。たまに使う道具なら、 運用する人が意図して root のシェルを使うことはできます。ただし、その変更はコンテナを入れ替えるまでしか残りません。 コンテナを再起動しても書き込み可能な層は残りますが、作り直すと消えます。導入のコマンドを覚えさせても、自動で用意されるわけではありません。 毎回同じように要るシステムの依存には、派生のイメージを使ってください。
これは、すぐ入れられて、たまに使う道具に向いています。いつも使う道具には、次の方法のほうが向きます。
長持ちさせる — 派生イメージを作る
コンテナが立ち上がるたびに、入れ直しの待ち時間なしですぐ使えなければならない道具は、nousresearch/hermes-agent を継いだ新しいイメージを作り、その道具を層として入れてください。
FROM nousresearch/hermes-agent:latest
USER root
RUN apt-get update \
&& apt-get install -y --no-install-recommends <your-package> \
&& rm -rf /var/lib/apt/lists/*
# Keep the root entrypoint; s6 drops privileges for the runtime.作ったら、公式のイメージの代わりに使います。
docker build -t my-hermes:latest .
docker run -d \
--name hermes \
--restart unless-stopped \
-v ~/.hermes:/opt/data \
-p 8642:8642 \
my-hermes:latest gateway run入口のスクリプトと /opt/data の扱いはそのまま受け継がれるので、このページの残りもそのまま当てはまります。上流の nousresearch/hermes-agent の新しい版を取ってきたら、イメージを作り直すのを忘れないでください。
込み入った道具や、複数のサービスの組み合わせ — サイドカーのコンテナを動かす
自分のサービスを連れてくる道具(データベース、Web サーバー、キュー、画面のないブラウザの群れ)や、Hermes のコンテナに置くには重すぎる道具は、共有の Docker のネットワークの上で別のコンテナとして動かしてください。Hermes は、ローカルの推論サーバーへ届くのと同じやり方で、コンテナ名を使ってサイドカーへ届きます(ローカルの推論サーバーにつなぐを参照してください)。
services:
hermes:
image: nousresearch/hermes-agent:latest
container_name: hermes
restart: unless-stopped
command: gateway run
ports:
- "8642:8642"
volumes:
- ~/.hermes:/opt/data
networks:
- hermes-net
my-tool:
image: example/my-tool:latest
container_name: my-tool
restart: unless-stopped
networks:
- hermes-net
networks:
hermes-net:
driver: bridgeHermes のコンテナの中からは、サイドカーへ http://my-tool:<port>(あるいはそれが話すプロトコル)で届きます。この形なら、サービスごとに一生も資源の上限も更新の周期も別々に保てますし、1 つの道具にしか要らない依存で Hermes のイメージをふくらませずに済みます。
広く役に立つ道具 — issue か pull request を出す
多くの Hermes Agent の利用者に役立ちそうな道具なら、私的な派生イメージで抱え込まず、上流へ寄せることを考えてください。hermes-agent のリポジトリに issue か pull request を出し、その道具と使いどころを説明します。公式のイメージに取り込まれた道具はすべての利用者の役に立ち、分家を抱え続ける手間も避けられます。
ローカルの推論サーバーにつなぐ(vLLM、Ollama など)
Hermes を Docker で動かしていて、推論サーバー(vLLM、Ollama、text-generation-inference など)もホストか別のコンテナで動いている場合、ネットワークの扱いに気を配る必要があります。
Docker Compose(勧められる方法)
両方のサービスを同じ Docker のネットワークに置きます。いちばん確実なやり方です。
services:
vllm:
image: vllm/vllm-openai:latest
container_name: vllm
command: >
--model Qwen/Qwen2.5-7B-Instruct
--served-model-name my-model
--host 0.0.0.0
--port 8000
ports:
- "8000:8000"
networks:
- hermes-net
deploy:
resources:
reservations:
devices:
- capabilities: [gpu]
hermes:
image: nousresearch/hermes-agent:latest
container_name: hermes
restart: unless-stopped
command: gateway run
ports:
- "8642:8642"
volumes:
- ~/.hermes:/opt/data
networks:
- hermes-net
networks:
hermes-net:
driver: bridgeそのうえで ~/.hermes/config.yaml では、ホスト名にコンテナ名を使います。
model:
provider: custom
model: my-model
base_url: http://vllm:8000/v1
api_key: "none"単独の docker run(Compose なし)
推論サーバーが(Docker ではなく)ホストで直接動いているなら、macOS と Windows では host.docker.internal を、Linux では --network host を使います。
macOS / Windows:
docker run -d \
--name hermes \
-v ~/.hermes:/opt/data \
-p 8642:8642 \
nousresearch/hermes-agent gateway run# config.yaml
model:
provider: custom
model: my-model
base_url: http://host.docker.internal:8000/v1
api_key: "none"Linux(ホストのネットワークを使う場合):
docker run -d \
--name hermes \
--network host \
-v ~/.hermes:/opt/data \
nousresearch/hermes-agent gateway run# config.yaml
model:
provider: custom
model: my-model
base_url: http://127.0.0.1:8000/v1
api_key: "none"つながっているか確かめる
Hermes のコンテナの中から、推論サーバーへ届くことを確かめます。
docker exec hermes curl -s http://vllm:8000/v1/models用意しているモデルを並べた JSON の応答が返るはずです。返らないときは、次を確かめてください。
- 両方のコンテナが同じ Docker のネットワークにいるか(
docker network inspect hermes-net) - 推論サーバーが
127.0.0.1ではなく0.0.0.0で待ち受けているか - ポート番号が合っているか
Ollama
Ollama も同じです。ホストで動いているなら host.docker.internal:11434(macOS / Windows)か 127.0.0.1:11434(--network host の Linux)を使います。同じ Docker のネットワークの別のコンテナで動いているなら、こうします。
model:
provider: custom
model: llama3
base_url: http://ollama:11434/v1
api_key: "none"困ったときは
コンテナがすぐ終わってしまう
docker logs hermes でログを見てください。よくある原因はこれです。
.envのファイルがない、または壊れている — まず対話的に動かして設定を済ませてください- ポートを公開しているとき、ポートがぶつかっている
「Permission denied」のエラーが出る
コンテナの stage2 のフックは、見守られる各サービスの中で s6-setuidgid を使い、root でない hermes ユーザー(UID 10000)へ権限を落とします。ホストの ~/.hermes/ が別の UID の持ち物なら、HERMES_UID と HERMES_GID(LinuxServer.io や NAS のイメージに合わせた PUID と PGID という別名でも構いません)をホストのユーザーに合わせるか、データのディレクトリを書き込めるようにしてください。
データの一式をまるごと誰でも読める状態にしないでください。資格情報が入っています。 代わりに、コンテナの UID と GID を、マウントしたディレクトリの持ち主に合わせてください。
NAS(UGOS、Synology、unRAID)では、データのディレクトリはたいてい、コンテナからは chown できないホストの UID が持つマウントです。UID 10000 ではなくマウントの持ち主として動くよう、PUID と PGID(または HERMES_UID と HERMES_GID)をそのホストのユーザーに合わせてください。
docker run -d \
--name hermes \
-e PUID=1000 -e PGID=10 \
-v /volume1/docker/hermes:/opt/data \
nousresearch/hermes-agent gateway rundocker exec hermes <cmd> も自動で UID 10000 へ落ちます。詳しいことと、実行ごとに外す方法は docker exec は自動で hermes ユーザーへ落ちますを参照してください。
共有したデータディレクトリが何度も 0700 に戻る
コンテナの外では、Hermes は起動するたびに HERMES_HOME(とその中の cron/、sessions/、logs/、memories/ のサブディレクトリ)を持ち主だけが使える 0700 に固めます。コンテナの中ではディレクトリの権限に手を付けないので、別の UID で動く隣のコンテナ(Web UI や権限を直す係など)と共有しているバインドマウントは、ホスト側で設定した権限と ACL がそのまま保たれます。それでも特定のディレクトリ権限を強制したいときは、HERMES_HOME_MODE を設定してください(8 進数で、たとえば HERMES_HOME_MODE=0755)。この設定はコンテナの中でも適用されます。
どの docker exec でも「Permission denied」になる(インストール先が 0700 で閉じている)
2026 年 8 月下旬より前に作られたイメージには、資格情報のファイルを /opt/hermes の直下に書くとそのディレクトリが 0700 に絞られ、hermes ユーザー(UID 10000)がインストール先から締め出されるという不具合がありました。以降、新しい docker exec はすべて Permission denied で失敗します。
新しいイメージを取ってきてコンテナを作り直せば、恒久的に直ります(インストール先は 0755 で届き、いまの版はもう絞りません)。作り直さずに、動いているコンテナをその場で立て直したいときはこうします。
docker exec -u root hermes chmod 0755 /opt/hermesPID 1 の下にゾンビ(<defunct>)のプロセスがたまる
コンテナの中で ps -eo stat,ppid,comm | awk '$1 ~ /^Z/' を実行すると、片付けられないまま終わった子プロセスが一覧で出ます。これは hermes 自身が PID 1 になっているときに起きます。ほとんどの場合、Compose のサービスが entrypoint: を差し替えたせいで docker/entrypoint-dispatch.sh → /init を通らなくなっているのが原因です。Hermes も起動時にこれを警告します(this process is PID 1 with no init above it)。既定の入口に戻すか、init: true(Compose)/ docker run --init を足して docker-init が親を失ったプロセスを片付けるようにしてください。詳しくは Dockerfile がしていること を見てください。コンテナを作り直せば、すでにたまったゾンビは消えます。
ブラウザの道具が動かない
Playwright には共有メモリが要ります。docker run のコマンドに --shm-size=1g を足してください。
docker run -d \
--name hermes \
--shm-size=1g \
-v ~/.hermes:/opt/data \
nousresearch/hermes-agent gateway runネットワークの不調のあと、ゲートウェイがつなぎ直せない
--restart unless-stopped のフラグが、一時的な失敗のほとんどを引き受けます。それでもゲートウェイが動かなくなっているなら、コンテナを再起動してください。
docker restart hermesコンテナの様子を確かめる
docker logs --tail 50 hermes # Recent logs
docker run -it --rm nousresearch/hermes-agent:latest --version # Verify version
docker stats hermes # Resource usage