Hermes Agent VPS セットアップガイド
対象 VPS: Xserver VPS(Ubuntu 24.04 LTS / 26.04 LTS 両対応)
目的: Google Workspace 連携 + VPS 状態監視を 24/7 自律運用
バージョン: v4.3(Phase 3 動作確認のユーザー切り替えを整理)
最終更新: 2026-05-24
§ 00このドキュメントについて
「Hermes 本体は Docker コンテナで完全に隔離し、ホスト OS の状態(fail2ban / journalctl 等)はホスト側の root cron が JSON 化したものを read-only マウントで読ませる」という構成のセットアップ手順をまとめたものです。
設計思想は bAI(Bridge AI)と同じで、LLM レイヤーは生データに直接到達できない、事前整形された情報のみを扱うという構造を VPS 運用に持ち込んでいます。
表記規約
各セクションの冒頭に作業ユーザーを明示しています:
- 作業ユーザー: 管理者 — 普段の sudo 持ちユーザー(例:
<管理者ユーザー>)で実行 - 作業ユーザー: hermes —
sudo -iu hermesで hermes に切り替えてから実行 - 作業ユーザー: root —
sudoを都度つけて実行(管理者ユーザーから)
ユーザーを跨ぐセクションでは、各コードブロックの先頭に切り替えコマンドを書いています。プロンプトが想定通りか不安な場合は whoami で確認してください。
SSH クライアントによる差異
ssh 接続クライアントによって OAuth フローの挙動が変わります:
- Cursor / VSCode Remote-SSH: ループバックポートの自動転送機能があるため、OAuth コールバックが手元 PC のブラウザから VPS のコンテナまで自動で届く。追加設定不要。
- 素の OpenSSH / PuTTY / Termius 等: ループバック自動転送は無いため、setup wizard 実行時は
ssh -Lで手動トンネル、または--network hostモードで起動する必要がある。
このガイドでは両方をカバーします。
§ 01アーキテクチャ概要
構成図
┌──────────────── Xserver VPS (Ubuntu 24.04 / 26.04 LTS) ────┐
│ │
│ ┌─ root 権限レイヤー ─────────────────────────────────┐ │
│ │ │ │
│ │ cron (root) │ │
│ │ └─ /usr/local/bin/hermes-health-collect.sh │ │
│ │ ├─ fail2ban-client status │ │
│ │ ├─ journalctl _COMM=sshd │ │
│ │ ├─ systemctl is-active │ │
│ │ ├─ df / free / uptime │ │
│ │ └─ JSON 化 → /var/lib/hermes-health/ │ │
│ │ │ │
│ └───────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ :ro mount │
│ ┌─ hermes system user レイヤー (sudo なし) ────────────┐ │
│ │ │ │
│ │ Docker container: hermes │ │
│ │ ├─ entrypoint が HERMES_UID/HERMES_GID で │ │
│ │ │ 内部ユーザーを gosu リマップ │ │
│ │ ├─ /opt/hermes/.venv/bin/hermes ← CLI バイナリ │ │
│ │ ├─ /opt/data ← bind: ~/.hermes (rw) │ │
│ │ │ ├─ config.yaml, .env │ │
│ │ │ ├─ memory/, skills/, sessions/ │ │
│ │ │ ├─ workspace/ (gateway 作業ディレクトリ) │ │
│ │ │ ├─ credentials/ (OAuth tokens) │ │
│ │ │ └─ google/ │ │
│ │ ├─ /opt/data/health ← bind: /var/lib/hermes-health │ │
│ │ │ (ro) │ │
│ │ └─ HERMES_DASHBOARD=1 で dashboard を同居起動 │ │
│ │ (gateway:8642 と dashboard:9119) │ │
│ │ │ │
│ └───────────────────────────────────────────────────────┘ │
│ │ │
└──────────────────────────────┼────────────────────────────────┘
▼
外部: OpenRouter / xAI / Anthropic /
Google API / Discord
設計原則
- 権限の最小化: hermes ユーザーは sudo を持たない。docker グループのみ所属。SSH 直接ログインも禁止。
- データの局所化: Hermes 関連の全データは
~/.hermes/に集約。コンテナは窓に過ぎない(bind mount)。 - 生データへの到達禁止: Hermes は collector が出した JSON のみを読む。
fail2ban-client等を直接叩けない。 - 再現性: VPS 再構築は
docker-compose.yml+collector スクリプト+~/.hermesのバックアップだけで完了する。 - バージョン固定:
:latestを使わず明示的なバージョンタグで運用。 - 公式 entrypoint への信任: UID/GID リマップ・dashboard 起動・gateway 初期化は全て公式 entrypoint に任せる。compose 側で
user:を上書きしない。
§ 02全体の流れ(Big Picture)
セットアップは8フェーズに分かれる。
| Phase | 内容 | 主な作業ユーザー | 所要 |
|---|---|---|---|
| 1 | VPS 初期化(ユーザー、SSH、ufw、fail2ban) | 管理者 | 30分 |
| 2 | Docker Engine インストール | 管理者 | 15分 |
| 3 | hermes system user の作成 | 管理者 | 10分 |
| 4 | Hermes コンテナ起動、初期セットアップ、永続化検証 | hermes(一部管理者) | 60分 |
| 5 | Google Workspace 接続 | 管理者 → hermes | 30分 |
| 6 | ホスト側 collector 実装 | 管理者 | 30分 |
| 7 | ヘルスチェックスキル投入 | hermes | 30分 |
| 8 | バックアップとロールバック手順の確立 | 管理者 | 30分 |
合計 4 時間程度。Phase 5 と Phase 6 は順序入れ替え可能。
§ 03Phase 1: VPS 初期化
既に Xserver VPS で fail2ban や ufw を運用中の状態を前提とするが、念のためチェックリストを置く。
OS バージョン確認
lsb_release -a
# 期待: Ubuntu 24.04 LTS (Noble Numbat) または 26.04 LTS (Resolute Raccoon)
cat /etc/os-release | grep -E '^(VERSION_ID|UBUNTU_CODENAME|VERSION_CODENAME)'
# UBUNTU_CODENAME が Phase 2 の Docker リポジトリ登録で使われる
# 例: noble (24.04) または resolute (26.04)
resolute を提供しているが、リリース直後で運用情報が少ないため、何か詰まったら Docker フォーラムや Issue を確認すること。サービス確認
# fail2ban 動作確認
sudo systemctl status fail2ban
# ufw 動作確認
sudo ufw status verbose
# SSH ポート(公開鍵認証のみ、パスワード認証無効)
sudo sshd -T | grep -iE "passwordauthentication|permitrootlogin|pubkey"
期待値:
passwordauthentication nopermitrootlogin noまたはpermitrootlogin prohibit-passwordpubkeyauthentication yes
管理者ユーザーの存在確認
Hermes 用ではない通常の管理者ユーザー(sudo 持ち)で SSH ログインできること。以降の手順は基本的にこの管理者ユーザーから実行する。
sudo -v
時刻同期確認
timedatectl status
# 「System clock synchronized: yes」になっていること
# 「Time zone: Asia/Tokyo (JST, +0900)」が望ましい
SSH 接続クライアントの種類を確認
OAuth ベース Provider(Grok、Anthropic OAuth)を使う場合、Phase 4 の setup 手順が SSH クライアントによって変わる。今接続に使っているクライアントを確認しておく:
| クライアント | ループバック自動転送 | Phase 4 で必要な追加作業 |
|---|---|---|
| Cursor (Remote-SSH) | あり | なし |
| VSCode (Remote-SSH) | あり | なし |
| 素の OpenSSH (ターミナル) | なし | ssh -L でポートフォワード |
| PuTTY / RLogin | なし(or 手動設定) | ssh -L 相当の設定 |
| Termius | 設定により異なる | 確認が必要 |
API キーベース Provider(OpenRouter 等)のみを使う予定なら、SSH クライアントの種類は関係ない。
§ 04Phase 2: Docker Engine インストール
Ubuntu 標準の apt install docker.io ではなく、Docker 公式リポジトリから入れる(バージョンが新しく、Compose V2 が同梱)。
競合する旧パッケージの削除
sudo apt remove -y \
docker.io \
docker-doc \
docker-compose \
docker-compose-v2 \
podman-docker \
containerd \
runc \
2>/dev/null || true
各パッケージの意図:
docker.io: Ubuntu リポジトリ版(バージョンが古い)docker-doc: ドキュメント(不要)docker-compose: Python 製 V1(非推奨)docker-compose-v2: Ubuntu 同梱の V2 シムpodman-docker: Docker 互換シムcontainerd/runc: Ubuntu リポジトリ版(公式版containerd.ioと競合)
依存パッケージのインストール
sudo apt update
sudo apt install -y ca-certificates curl
Docker 公式 GPG キーの追加(.asc 形式)
apt update で NO_PUBKEY エラーになる。必ず実行すること。sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
-o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# 確認: ファイルが存在し、サイズが約 1.7KB あること
ls -la /etc/apt/keyrings/docker.asc
sudo head -1 /etc/apt/keyrings/docker.asc
# "-----BEGIN PGP PUBLIC KEY BLOCK-----" が表示されること
リポジトリの登録(DEB822 形式)
echo \
"Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc" | \
sudo tee /etc/apt/sources.list.d/docker.sources > /dev/null
# 確認: Suites が noble (24.04) または resolute (26.04) になっていること
cat /etc/apt/sources.list.d/docker.sources
Docker と Compose プラグインのインストール
sudo apt update
sudo apt install -y \
docker-ce \
docker-ce-cli \
containerd.io \
docker-buildx-plugin \
docker-compose-plugin
| パッケージ | 役割 |
|---|---|
docker-ce | Docker Engine 本体 |
docker-ce-cli | docker コマンド |
containerd.io | コンテナランタイム |
docker-buildx-plugin | docker buildx サブコマンド |
docker-compose-plugin | docker compose サブコマンド(V2) |
起動と自動起動の有効化
sudo systemctl enable --now docker
sudo systemctl enable containerd
動作確認
sudo systemctl is-active docker
# → active
docker --version
docker compose version
sudo docker run --rm hello-world
# "Hello from Docker!" メッセージが出ればOK
docker グループの確認
getent group docker
# 例: docker:x:988:
# ↑GID(環境により異なる)
詰まったときの切り分け
NO_PUBKEY エラーで apt update が失敗する: GPG キー取得ステップを飛ばしているか、docker.asc が空になっている。
ls -la /etc/apt/keyrings/docker.asc
# サイズが 0 や数バイトしか無ければ取得失敗
# 再取得
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
-o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
sudo apt update
Suites: の行が空になる: /etc/os-release に UBUNTU_CODENAME も VERSION_CODENAME も無い特殊な環境。手動でコードネームを書き込む:
sudo sed -i 's/^Suites:.*/Suites: resolute/' /etc/apt/sources.list.d/docker.sources
# noble の場合は resolute → noble
§ 05Phase 3: hermes system user の作成
useradd による作成
sudo useradd \
--system \
--create-home \
--home-dir /home/hermes \
--shell /bin/bash \
--user-group \
--comment "Hermes Agent service account" \
hermes
各フラグの意味:
--system: UID を system 範囲(100〜999)から割り当て、aging を無効化--create-home: ホームディレクトリを作成(Hermes のデータ集約先なので必須)--home-dir /home/hermes: 場所を明示--shell /bin/bash: 管理時にsudo -iu hermesで入るためシェルを与える--user-group: 同名のグループを作って一次グループに設定
パスワードロック状態の確認
system user は作成直後からパスワードがロックされている。意図的にこのまま放置する。
sudo passwd -S hermes
# 例: hermes L 2026-05-24 0 99999 7 -1
# ↑
# L = Locked
L になっていれば:
- パスワード認証でログインできない
- 他ユーザーから
su - hermes不可(root と sudo 持ちユーザーは例外) - 管理者から
sudo -iu hermesでのみ入れる
UID/GID の確認(重要)
id hermes
# 例(Xserver VPS): uid=999(hermes) gid=986(hermes) groups=986(hermes)
# 例(別 VPS): uid=998(hermes) gid=998(hermes) groups=998(hermes)
UID は環境によって 998 / 999 / その他になる。Xserver VPS で観測されたのは UID=999, GID=986 のようなパターン。重要なのは:
- ホストの hermes ユーザーの UID を Phase 4 の
~/hermes-stack/.envに正しく書く($(id -u)で動的取得するので失敗しない) - bind mount でホストとコンテナ内のファイル所有権を一致させる
docker グループに追加
sudo usermod -aG docker hermes
SSH 直接ログイン禁止
/etc/ssh/sshd_config に DenyUsers を追加し、SSH 経由での hermes ログインを明示的に禁止する。
echo "DenyUsers hermes" | sudo tee -a /etc/ssh/sshd_config
sudo systemctl reload sshd
これで仮に /home/hermes/.ssh/authorized_keys に鍵が置かれても外部から hermes としてログイン不可になる。多層防御。
AllowUsers 運用の場合: 既に AllowUsers で許可ユーザーを明示している環境では、hermes を含めないだけで自動的に拒否される。両方設定する必要はない。動作確認
sudo -iu hermes 以降だけが hermes ユーザーでの操作。hermes は sudo を持たないので、sudo を含む確認(sudo ls・sudo grep など)は必ず管理者プロンプトで実行すること。hermes プロンプト(hermes@...$)で sudo を叩くと sudo: ... I'm afraid I can't do that と拒否される(=エラーではなく仕様)。# ===== ここから管理者ユーザーで実行 =====
# UID が system 範囲(< 1000)か
id hermes
# パスワードロック確認
sudo passwd -S hermes | awk '{print $2}'
# → L
# ホームディレクトリ(HOME_MODE 0700 なので sudo 必須)
sudo ls -la /home/hermes
# 所有者が hermes:hermes になっていること
# ※ 所有者・モードの確認だけなら sudo 不要の `ls -ld /home/hermes` でも可
# docker グループ所属
groups hermes | grep -q docker && echo OK
# → OK
# SSH 直接ログイン不可(sudo を使うので必ず管理者ユーザーで)
sudo grep -E "^DenyUsers.*hermes" /etc/ssh/sshd_config && echo OK
# → OK
# ===== 最後に hermes に切り替えて確認(このブロックの末尾に配置)=====
# 注意: 以降は hermes ユーザー。sudo は一切使えない。
sudo -iu hermes
whoami
# → hermes
docker ps
# → 空のテーブル(CONTAINER ID, IMAGE, ...)
exit
# → exit で管理者プロンプトに戻る
hermes ユーザーへの入り方(今後の標準フロー)
system user は SSH 直接入れないので、管理は管理者ユーザー経由で行う。現時点では ~/hermes-stack/ などのディレクトリはまだ存在しない(Phase 4 で作成する)。今できることは hermes に入って docker ps を確認するくらいまで。
# 管理者ユーザーから hermes に切り替え
sudo -iu hermes
# プロンプトが hermes になることを確認
whoami
# → hermes
# Phase 3 時点で動くコマンド
docker ps # 空のテーブル
docker version
# 管理者に戻る
exit
Phase 4 以降は ~/hermes-stack/ ディレクトリを作って、そこで docker compose を使う運用に入っていく。
§ 06Phase 4: Hermes コンテナの起動と初期セットアップ
このフェーズは hermes ユーザーと管理者ユーザーを行き来する。各セクション冒頭の「作業ユーザー」を確認しながら進めること。プロンプトが想定通りか不安な場合は whoami で確認する。
Phase 4 の作業フロー(全体像)
詰まりやすいので、最初に全体の順序を示しておく:
- ディレクトリ準備(hermes)
.envとdocker-compose.ymlを先に作る(hermes)← v4.1 までは setup wizard の後だったが、v4.2 では先に/var/lib/hermes-health/を作る(管理者)- setup wizard を実行(hermes、OAuth Provider は
--network host必須) docker compose up -dで常駐起動(hermes)- 動作確認と永続化検証(hermes)
「先に compose ファイルを作る理由」: setup wizard で OAuth が時間切れになっても、docker compose up -d まで戻れる状態にしておくため。Phase 4 で時間がかかるのは OAuth フローなので、その前に全部の準備を済ませる。
ディレクトリ構造(hermes ユーザー側、完了後イメージ)
/home/hermes/
├── .hermes/ # ← bind mount to /opt/data
│ ├── config.yaml
│ ├── .env # setup wizard が書き込む(API キー or OAuth ref)
│ ├── credentials/ # OAuth tokens (Grok / Anthropic OAuth)
│ ├── memory/
│ ├── skills/
│ ├── sessions/
│ ├── workspace/ # gateway の作業ディレクトリ(自律作業の場)
│ └── google/ # Google OAuth tokens (Phase 5 で作られる)
└── hermes-stack/ # docker-compose 配置
├── docker-compose.yml
└── .env # compose 用変数(バージョンと UID/GID のみ)
compose 用ディレクトリの準備
# 管理者ユーザーから hermes に切り替え
sudo -iu hermes
whoami # → hermes であること
# ディレクトリ作成
mkdir -p ~/hermes-stack ~/.hermes
cd ~/hermes-stack
# 確認
ls -la ~
# hermes-stack/ と .hermes/ が作られていること
compose 用 .env の作成
cd ~/hermes-stack
cat > .env <<'EOF'
# Hermes バージョン(明示ピン留め)
HERMES_VERSION=v2026.5.16
EOF
# コンテナ内 hermes ユーザーをリマップするための UID/GID
# (環境ごとに 998 / 999 等で異なるので動的取得)
echo "HERMES_UID=$(id -u)" >> .env
echo "HERMES_GID=$(id -g)" >> .env
chmod 600 .env
# 確認
cat .env
# HERMES_UID, HERMES_GID がホストの id -u / id -g と一致すること
:latest は更新で挙動が変わるリスクがあるため運用では使わない。明示バージョンで固定し、自分が判断したタイミングで上げる。usermod/groupmod + gosu で内部 hermes ユーザーをホスト UID にリマップする設計。compose の user: で起動 UID を強制するとこの初期化がスキップされ、ディレクトリ作成や dashboard side-process の起動が失敗する。docker-compose.yml の作成
cd ~/hermes-stack
cat > docker-compose.yml <<'EOF'
services:
hermes:
image: nousresearch/hermes-agent:${HERMES_VERSION}
container_name: hermes
restart: unless-stopped
# user: は指定しないこと
# entrypoint が HERMES_UID/HERMES_GID を読んで内部ユーザーを gosu リマップする
volumes:
# Hermes の全データ(rw)
- /home/hermes/.hermes:/opt/data
# ホスト側 collector の出力(ro、Phase 6 で配置)
- /var/lib/hermes-health:/opt/data/health:ro
ports:
# gateway(ローカルバインドのみ)
- "127.0.0.1:8642:8642"
# dashboard(同一コンテナ内で side-process として起動)
- "127.0.0.1:9119:9119"
environment:
- HERMES_UID=${HERMES_UID}
- HERMES_GID=${HERMES_GID}
- HERMES_DASHBOARD=1
- TZ=Asia/Tokyo
command: gateway run
healthcheck:
# gateway は /health エンドポイントを提供しないため TCP 接続成立で判定
# (curl ベースの健康診断は使えない)
test: ["CMD-SHELL", "bash -c '</dev/tcp/localhost/8642'"]
interval: 1m
timeout: 10s
retries: 3
start_period: 30s
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "5"
EOF
# 確認
cat docker-compose.yml
ls -la
# .env と docker-compose.yml が両方あること
HERMES_DASHBOARD=1 を渡すと、Hermes の entrypoint が dashboard を gateway と同一コンテナ内の side-process として起動する。これは公式の標準ルート。dashboard を別コンテナで動かす流儀もあるが、その場合は GATEWAY_HEALTH_URL の明示が必要になり手順が増える。単一 VPS で完結する用途では同居の方が単純で推奨。/health エンドポイントを提供しないため、curl -f http://localhost:8642/health は接続リセットを返し healthcheck が常に失敗する。bash の /dev/tcp 機能で TCP 接続成立のみ確認する方式に切り替えることで、外部ツール不要で gateway の生存確認ができる。~/.hermes/.env と二重定義になる。environment: 経由の値は .env を上書きする優先度を持つため、運用では ~/.hermes/.env に一本化する。/var/lib/hermes-health/ の事前作成
docker compose up 時に :ro マウント先が存在しないとエラーになるので、ダミーで先に作る:
# hermes から管理者に戻る
exit
whoami # 管理者ユーザー名(例: <管理者ユーザー>)であること
sudo mkdir -p /var/lib/hermes-health
sudo chown root:hermes /var/lib/hermes-health
sudo chmod 750 /var/lib/hermes-health
# 確認
ls -ld /var/lib/hermes-health
# drwxr-x--- root hermes ... であること
Phase 6 で本物の collector が出力を書く。
Provider 選択方針の決定
setup wizard 実行前に、どの Provider を使うかを決めておく。OAuth 系を選ぶ場合は実行コマンドが変わる。
| Provider タイプ | 例 | setup wizard の起動方法 |
|---|---|---|
| API キーベース | OpenRouter, OpenAI 直接, Anthropic API key | 通常モード |
| OAuth ベース | xAI Grok OAuth (SuperGrok), Anthropic OAuth (Claude Max) | --network host 必須 |
OAuth ベースを選ぶと、wizard が一時的にローカルポート(例: 127.0.0.1:56121)で OAuth コールバックを listen する。デフォルトの docker run だとこのポートがコンテナの中に閉じてしまい、ブラウザが認証完了後にリダイレクトしてもコールバックを受け取れない。--network host でコンテナにホストのネットワーク名前空間を共有させると、コンテナ内 127.0.0.1 がホストの 127.0.0.1 と一致してコールバックが届く。
setup wizard の実行(API キーベース Provider の場合)
sudo -iu hermes
whoami # → hermes
docker run -it --rm \
-v /home/hermes/.hermes:/opt/data \
-e HERMES_UID=$(id -u) \
-e HERMES_GID=$(id -g) \
nousresearch/hermes-agent:v2026.5.16 \
setup
ウィザードで設定する項目(API キーベースの場合):
- Provider: OpenRouter / OpenAI / Anthropic API key 等
- API Key: 該当キーを貼り付け
- Model: 用途に応じて
- SOUL.md: ペルソナ定義(後で編集可)
- Terminal backend:
localのまま(既にコンテナ内なので二重隔離は不要) - Gateway working directory:
/opt/data/workspace - Enable sudo support: N(必須。hermes は sudo を持たない)
- Messaging apps: スキップ可
- Approval mode:
smart
setup wizard の実行(OAuth ベース Provider の場合)
OAuth フローを含むので --network host で起動する:
sudo -iu hermes
whoami # → hermes
docker run -it --rm \
-v /home/hermes/.hermes:/opt/data \
-e HERMES_UID=$(id -u) \
-e HERMES_GID=$(id -g) \
--network host \
nousresearch/hermes-agent:v2026.5.16 \
setup
--network host は setup の時だけ。通常運用の docker compose up -d は 127.0.0.1:8642:8642 のローカルバインド構成のままで使う。Provider 選択で OAuth 系(例: xAI Grok OAuth (SuperGrok Subscription))を選ぶと、以下のような表示が出る:
Open this URL to authorize Hermes with xAI:
https://auth.x.ai/oauth2/authorize?response_type=code&client_id=...&redirect_uri=http%3A%2F%2F127.0.0.1%3A56121%2Fcallback&...
Waiting for callback on http://127.0.0.1:56121/callback
Could not open the browser automatically; use the URL above.
56121)は毎回ランダム。表示された値をメモする。OAuth フロー実行(SSH クライアントの種類による分岐)
ケース A: Cursor / VSCode Remote-SSH を使っている場合
ループバック自動転送が効くので、追加作業なしで以下を実行するだけ:
- setup wizard が表示した OAuth URL を手元 PC のブラウザで開く(ターミナルの URL をクリック / コピー&貼り付け)
- Grok のログイン画面で認証
- 認証完了後、ブラウザが自動で
http://127.0.0.1:56121/callback?code=...にリダイレクト - Cursor / VSCode の自動転送で VPS のコンテナにコールバックが届く
- setup wizard が次のステップに進む(「successful」のような表示)
「接続を確立できませんでした、コードをコピー&ペーストしてください」という画面が出たら、それはコールバックが届かなかった signal。SSH クライアントの自動転送が効いていない可能性があるのでケース B へ。
ケース B: 素の OpenSSH / PuTTY / その他 を使っている場合
手元 PC のブラウザから VPS のループバックに到達する経路が無いので、手動で SSH トンネルを張る。
手順:
- setup wizard が OAuth URL を表示してポート番号(例: 56121)が確定したら、手元 PC で新しいターミナルを開く
- 新ターミナルから SSH トンネルを張る:
# XXXXX は wizard が表示した実際のポート番号
ssh -L XXXXX:127.0.0.1:XXXXX -N <管理者ユーザー>@<VPSアドレス>
# 例:
# ssh -L 56121:127.0.0.1:56121 -N <管理者ユーザー>@<VPSアドレス>
-N でシェルを開かず、トンネルだけ確立。何も表示されなくて正常(接続維持中の状態)。
- 手元 PC のブラウザで OAuth URL を開く
- Grok で認証
- ブラウザがリダイレクト → SSH トンネル経由で VPS のコンテナに到達 → setup 完了
- SSH トンネルのターミナルは
Ctrl+Cで終了
OAuth で時間切れになった場合
OAuth コードの有効期限は数分なので、ブラウザを開く前に以下の準備をしておくと安全:
- SSH トンネルが必要な場合は事前に張っておく
- 手元 PC のブラウザを起動しておく
- クリップボードを空にしておく
時間切れで wizard が中断したらキャンセル(Ctrl+C)して、もう一度同じコマンドで実行。既存設定があれば wizard がデフォルト値として表示するので、変更が必要な項目だけ再入力する。
setup wizard 完了後の確認
# config が作られたか
ls -la ~/.hermes/
# config.yaml の中身(Provider 等の確認)
cat ~/.hermes/config.yaml | head -30
# 認証情報の確認
# OAuth の場合は ~/.hermes/credentials/ に保存される(.env ではない)
ls -la ~/.hermes/credentials/ 2>/dev/null
# API キーの場合は ~/.hermes/.env に書かれる
grep -iE 'api_key|token' ~/.hermes/.env 2>/dev/null | sed 's/=.*/=***/'
.env を grep して何も出ない場合: OAuth ベース Provider を選んだ場合、認証情報は .env ではなく credentials/ ディレクトリ配下に保存される。.env が空でも問題ない。コンテナ起動
cd ~/hermes-stack
docker compose pull
docker compose up -d
# ステータス確認(healthcheck の interval が 1m + start_period が 30s なので少し待つ)
sleep 90
docker compose ps
# STATUS が "Up X minutes (healthy)" になればOK
# (health: starting) のままなら、もう1分待ってから再確認
動作確認
dashboard / gateway の生存
# dashboard が応答するか(HTML が返る)
curl -fsS http://127.0.0.1:9119 | head
# gateway は /health エンドポイントが無いので curl は失敗するが、
# 8642 で listen していることは docker compose ps の (healthy) で判定済み
コンテナ内ユーザーの UID/GID リマップ確認
# entrypoint が gosu でリマップした結果を確認
# (docker exec のデフォルトユーザーは root なので、-u hermes 指定が重要)
docker exec -u hermes hermes id
# uid=999(hermes) gid=986(hermes) など、ホスト側 (id) と一致すること
docker exec hermes env | grep -E '^HERMES_(UID|GID|DASHBOARD)'
# HERMES_UID, HERMES_GID, HERMES_DASHBOARD=1 が出ること
Hermes との対話(重要: コマンドの絶対パス)
hermes バイナリのコンテナ内パスは /opt/hermes/.venv/bin/hermes。docker exec のデフォルトユーザーは root で PATH が違うため、絶対パスで呼ぶ必要がある。さらにファイル所有権を破壊しないために -u hermes を必須にする:
# 対話を1回試す
docker exec -u hermes -it hermes /opt/hermes/.venv/bin/hermes chat -q "hello"
# Hermes が応答を返せば成功
alias 設定(毎回フルパスを打つのが煩雑なため)
# hermes ユーザーの ~/.bashrc に alias を追加
echo "alias hermes='docker exec -u hermes -it hermes /opt/hermes/.venv/bin/hermes'" >> ~/.bashrc
source ~/.bashrc
# 以降は短く書ける
hermes chat -q "hello"
hermes --help
hermes sessions list
-u hermes 必須か: docker exec のデフォルトユーザーは Dockerfile の USER 指定または root。Hermes の場合は root として exec されるため、hermes login などを -u hermes なしで実行すると認証ファイルが root 所有で書かれ、リマップ後の hermes uid で動く gateway から読めなくなる(auth.json 所有権バグ)。alias で -u hermes を固定しておくことで予防できる。永続化の検証(重要)
docker pull でアップデートしてもデータが残ることを、運用前に必ず実機検証する。
# 1. コンテナ内に印を書く
docker exec -u hermes hermes touch /opt/data/persistence-test.txt
docker exec -u hermes hermes ls -la /opt/data/persistence-test.txt
# 2. ホスト側でも見えるか
ls -la /home/hermes/.hermes/persistence-test.txt
# → 同じファイルが見えるはず(bind mount が効いている証拠)
# → 所有者が hermes:hermes であること(gosu リマップが効いている証拠)
# 3. mount 状態の inspect
docker inspect hermes | jq '.[0].Mounts'
# 期待値:
# [
# { "Type": "bind", "Source": "/home/hermes/.hermes",
# "Destination": "/opt/data", "RW": true, ... },
# { "Type": "bind", "Source": "/var/lib/hermes-health",
# "Destination": "/opt/data/health", "RW": false, ... }
# ]
# 重要: "Type": "bind" であること("volume" だと named volume で別管理)
# 4. コンテナを完全削除
cd ~/hermes-stack
docker compose down
# 5. 新しくコンテナを起動
docker compose up -d
sleep 90
docker compose ps
# (healthy) であること
# 6. 印が残っているか
docker exec -u hermes hermes ls -la /opt/data/persistence-test.txt
# → ファイルが残っていればOK
# → 所有者も hermes:hermes のままであること
# 7. dashboard も再度応答するか
curl -fsS http://127.0.0.1:9119 | head
# 8. テストファイルを削除
docker exec -u hermes hermes rm /opt/data/persistence-test.txt
これで「docker compose down → up -d してもデータと dashboard 起動状態が残る」ことが実証できる。この検証を通さずに運用に入らない。
トラブル時の対処
healthcheck が (unhealthy) のまま: gateway は /health エンドポイントを提供しないので、healthcheck で curl ベースのテストを使っていると常に失敗する。docker-compose.yml の healthcheck が TCP 接続確認方式になっているか確認:
grep -A2 'healthcheck:' ~/hermes-stack/docker-compose.yml
# test: ["CMD-SHELL", "bash -c '</dev/tcp/localhost/8642'"] ← この行があること
curl ベースになっていたら、上の docker-compose.yml ブロックで書き直し、docker compose down && docker compose up -d。
permission denied で書けない: ~/.hermes/ の所有権を確認
ls -la ~/.hermes/
# 全部 hermes:hermes になっているか
ズレていた場合は管理者に戻って chown:
exit # hermes から管理者へ
sudo chown -R hermes:hermes /home/hermes/.hermes
sudo -iu hermes
間違って user: を docker-compose.yml に書いて起動してしまった場合: entrypoint の gosu リマップが効かず、初期化が中途半端になる(dashboard が立たない、ファイル所有権が変、など)。対処:
cd ~/hermes-stack
docker compose down
# docker-compose.yml から user: 行を削除して、environment に HERMES_UID/HERMES_GID を入れる
# (上記の docker-compose.yml と一致させる)
vi docker-compose.yml
# 管理者で所有権リセット
exit
sudo chown -R hermes:hermes /home/hermes/.hermes
sudo -iu hermes
cd ~/hermes-stack && docker compose up -d
dashboard が応答しない:
# 環境変数が伝わっているか
docker exec hermes env | grep HERMES_DASHBOARD
# HERMES_DASHBOARD=1 が出ること
# プロセス確認
docker exec hermes ps -ef | grep dashboard
# /opt/hermes/.venv/bin/hermes dashboard ... が走っていること
# entrypoint のログで dashboard 起動を確認
docker logs hermes 2>&1 | grep -iE 'dashboard|9119'
# Hermes の dashboard side-process は supervised ではないため、
# 何らかの理由で落ちた場合はコンテナごと再起動
docker compose restart hermes
setup wizard が時間切れになる: OAuth コードの有効期限は数分。次の準備をしてから再実行:
- SSH トンネル(必要なら)を事前に張る
- 手元 PC のブラウザを起動済みにする
- クリップボード空に
docker run --rm ... setupを再実行(既存設定はそのまま残るので、変更したい項目だけ再入力すれば良い)
OAuth ステップで Enter を押しても進まない: コールバックが届いていない。「コードをコピー&ペーストしてください」画面が出ているなら、それは Grok 側のフォールバック表示で、Hermes 側には貼り付け先が無い。--network host で起動しているか、SSH トンネル張っているか、SSH クライアントが自動転送するかを再確認。
§ 07Phase 5: Google Workspace 接続
GCP プロジェクト作成と API 有効化
https://console.cloud.google.com にアクセスし:
- 新規プロジェクト作成(例:
hermes-personal) - API ライブラリで以下を有効化:
- Gmail API
- Google Calendar API
- Google Drive API
- Google Sheets API
- Google Docs API
- People API
- 「APIとサービス」→「OAuth 同意画面」
- ユーザータイプ: 外部
- テストユーザーに自分の Google アカウントを追加
- 「認証情報」→「認証情報を作成」→「OAuth クライアント ID」
- アプリケーションタイプ: デスクトップ アプリ
- JSON をダウンロード
JSON をコンテナに渡す
# ダウンロードした JSON を手元から VPS の管理者ユーザーへ scp 済みの前提
# (例: scp client_secret.json admin@vps:~/)
# 管理者から hermes 配下へコピーして所有権を整える
sudo cp ~/client_secret_xxx.json /home/hermes/.hermes/google_client_secret.json
sudo chown hermes:hermes /home/hermes/.hermes/google_client_secret.json
sudo chmod 600 /home/hermes/.hermes/google_client_secret.json
認証フロー実行
sudo -iu hermes
whoami # → hermes
# alias を設定済みなら短く
hermes
# 設定していなければフルパスで
# docker exec -u hermes -it hermes /opt/hermes/.venv/bin/hermes
Hermes 内のインタラクティブセッションで:
> Google Workspace のセットアップを始めて
Hermes が auth URL を出してくる。Phase 4 の OAuth フローと同様に SSH クライアント次第で扱いが変わる:
- Cursor / VSCode Remote-SSH: そのままブラウザで開けば自動転送される
- 素の SSH:
ssh -Lで当該ポートを転送してからブラウザで開く
承認後 http://localhost:1/?code=... にリダイレクト(接続エラー画面でも問題なし)、その URL 全体をコピーして Hermes に貼り戻す。
動作確認
> 今日のカレンダーの予定を見せて
> 未読のメールを5件サマリーして
auth.json 所有権の確認
ls -la /home/hermes/.hermes/google/
全ファイルが hermes:hermes 所有になっていればOK。root:root が混ざっていたら管理者に戻って修正:
exit # hermes から管理者へ
sudo chown -R hermes:hermes /home/hermes/.hermes/google/
sudo -iu hermes
cd ~/hermes-stack && docker compose restart hermes
§ 08Phase 6: ホスト側コレクタの実装
事前確認1: fail2ban の jail 名
ジェイル名は環境によって sshd だったり ssh だったりするので先に確認:
sudo fail2ban-client status
# Jail list: の行に出てくる名前を控える
# 例: Jail list: sshd または ssh
以降のスクリプトでは sshd を仮定する。ssh だった場合は collector スクリプト内の fail2ban-client status sshd の引数を ssh に書き換えること。
事前確認2: SSH journal の単位名
Ubuntu 22.10 以降は SSH が socket activation 化されているため、journalctl -u ssh で認証イベントが拾えるかは環境依存:
# どこに認証イベントが流れているか確認
sudo journalctl -u ssh --since "1 hour ago" --no-pager | tail -20
sudo journalctl -u ssh.socket --since "1 hour ago" --no-pager | tail -20
sudo journalctl _COMM=sshd --since "1 hour ago" --no-pager | tail -20
_COMM=sshd での絞り込みが最も確実(socket activation で短命ユニットに散ったログも拾える)。以下のスクリプトはこれを採用する。
collector スクリプトの配置
sudo tee /usr/local/bin/hermes-health-collect.sh > /dev/null <<'SCRIPT'
#!/bin/bash
# Hermes 用ヘルスチェック情報コレクタ
# root cron から実行され、JSON を /var/lib/hermes-health/ に書き出す
set -euo pipefail
OUT=/var/lib/hermes-health
TS=$(date -Iseconds)
mkdir -p "$OUT"
# 一時ディレクトリを mktemp で確保し、終了時に必ず削除(並行実行でも衝突しない)
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
# --- fail2ban ---
{
echo "{"
echo " \"timestamp\": \"$TS\","
if fail2ban-client status sshd > "$TMP/f2b.txt" 2>/dev/null; then
BANNED=$(grep -oP 'Banned IP list:\s*\K.*' "$TMP/f2b.txt" || echo "")
TOTAL_BANNED=$(grep -oP 'Total banned:\s*\K\d+' "$TMP/f2b.txt" || echo "0")
CURRENT_FAILED=$(grep -oP 'Currently failed:\s*\K\d+' "$TMP/f2b.txt" || echo "0")
echo " \"sshd\": {"
echo " \"banned_ips\": \"$BANNED\","
echo " \"total_banned\": $TOTAL_BANNED,"
echo " \"currently_failed\": $CURRENT_FAILED"
echo " }"
else
echo " \"sshd\": null,"
echo " \"error\": \"fail2ban-client not available or sshd jail not running\""
fi
echo "}"
} > "$OUT/fail2ban.json.tmp"
mv "$OUT/fail2ban.json.tmp" "$OUT/fail2ban.json"
# --- 直近1時間の SSH 認証イベント(_COMM=sshd で絞り込み) ---
journalctl _COMM=sshd --since "1 hour ago" --no-pager -o json \
> "$OUT/auth.json.tmp" 2>/dev/null || : > "$OUT/auth.json.tmp"
mv "$OUT/auth.json.tmp" "$OUT/auth.json"
# 注: -o json は NDJSON (各行が独立した JSON)。jq で読む際は jq -s で配列化するか、
# slurp なしで1行ずつ処理する。Hermes スキルでもこれを前提に解釈する。
# --- システムリソース ---
{
echo "{"
echo " \"timestamp\": \"$TS\","
echo " \"disk\": {"
df -B1 / | awk 'NR==2 {printf " \"total\": %d, \"used\": %d, \"available\": %d, \"percent\": \"%s\"\n", $2, $3, $4, $5}'
echo " },"
echo " \"memory\": {"
free -b | awk '/^Mem:/ {printf " \"total\": %d, \"used\": %d, \"available\": %d\n", $2, $3, $7}'
echo " },"
LOAD=$(uptime | grep -oP 'load average:\s*\K.*')
echo " \"load_average\": \"$LOAD\","
UPTIME=$(uptime -p)
echo " \"uptime\": \"$UPTIME\""
echo "}"
} > "$OUT/system.json.tmp"
mv "$OUT/system.json.tmp" "$OUT/system.json"
# --- 重要サービスの稼働状況 ---
SERVICES=(ssh fail2ban ufw docker)
{
echo "{"
echo " \"timestamp\": \"$TS\","
echo " \"services\": {"
LAST=$((${#SERVICES[@]} - 1))
for i in "${!SERVICES[@]}"; do
svc="${SERVICES[$i]}"
STATUS=$(systemctl is-active "$svc" 2>/dev/null || echo "unknown")
if [ "$i" -lt "$LAST" ]; then
printf ' "%s": "%s",\n' "$svc" "$STATUS"
else
printf ' "%s": "%s"\n' "$svc" "$STATUS"
fi
done
echo " }"
echo "}"
} > "$OUT/services.json.tmp"
mv "$OUT/services.json.tmp" "$OUT/services.json"
# --- 出力ファイルの mode 調整のみ(所有権は初回 chown -R で確定済み) ---
chmod 640 "$OUT"/*.json 2>/dev/null || true
SCRIPT
sudo chmod 755 /usr/local/bin/hermes-health-collect.sh
初回実行と権限の初期設定
# 初回のみ、ディレクトリ全体の所有権を確定
# (Phase 4 でディレクトリ自体は作っているが、念のため再適用)
sudo chown -R root:hermes /var/lib/hermes-health
sudo chmod 750 /var/lib/hermes-health
# collector を1回走らせて生成確認
sudo /usr/local/bin/hermes-health-collect.sh
ls -la /var/lib/hermes-health/
# 各 JSON が valid であることを確認
for f in /var/lib/hermes-health/*.json; do
if [ "$(basename "$f")" = "auth.json" ]; then
if [ -s "$f" ]; then
jq -e -s . "$f" >/dev/null && echo "OK: $f (NDJSON)" || echo "NG: $f"
else
echo "OK: $f (empty)"
fi
else
jq -e . "$f" >/dev/null && echo "OK: $f" || echo "NG: $f"
fi
done
# 中身を眺める
cat /var/lib/hermes-health/system.json | jq .
cat /var/lib/hermes-health/services.json | jq .
cron に登録
sudo tee /etc/cron.d/hermes-health > /dev/null <<'EOF'
# Hermes 用ヘルスチェック collector
*/5 * * * * root /usr/local/bin/hermes-health-collect.sh
EOF
コンテナから見えるか確認
sudo -iu hermes
docker exec -u hermes -it hermes ls -la /opt/data/health/
docker exec -u hermes -it hermes cat /opt/data/health/system.json
exit # 管理者に戻る
§ 09Phase 7: ヘルスチェックスキルの投入
sudo -iu hermes
whoami # → hermes
スキル定義
~/.hermes/skills/vps-health-check/SKILL.md を作成:
mkdir -p ~/.hermes/skills/vps-health-check
cat > ~/.hermes/skills/vps-health-check/SKILL.md <<'EOF'
---
name: vps-health-check
description: Hermes 自身が動いている VPS のヘルスチェックを実施し、異常があれば Discord で報告する。
trigger: ヘルスチェック要求、または定期実行(1日1回 + 異常検出時)
---
# VPS ヘルスチェック手順
## 入力データ
すべて `/opt/data/health/` 配下の JSON ファイルから読み取る。**シェルコマンドを直接実行してはならない**。生データは root 側の collector が事前整形している。
- `system.json`: disk, memory, load, uptime
- `fail2ban.json`: sshd jail の状態
- `auth.json`: 直近1時間の認証イベント (NDJSON 形式)
- `services.json`: 主要サービスの稼働状況
## チェック項目と閾値
### システムリソース
- ディスク使用率 > 80% → 警告
- ディスク使用率 > 90% → 緊急
- メモリ available < 10% → 警告
- load average 1min > CPU 数 × 2 → 警告
### セキュリティ
- fail2ban の total_banned が前回チェック時より +5 以上 → 警告
- currently_failed > 10 → 攻撃継続中の可能性、警告
- ssh service が active でない → 緊急
### サービス
- docker / ssh / fail2ban のいずれかが active でない → 緊急
## 出力
異常検出時、Discord に以下フォーマットで送信:
```
🚨 [VPS Health Alert] severity={warning|critical}
- 検出時刻: ISO8601
- 該当項目: <chronological list>
- 推奨対応: <Hermes が状況から判断>
- 詳細: <該当 JSON の関連部分>
```
正常時は何もしない(毎日朝9時の定期サマリーを除く)。
## 状態の永続化
前回チェック時の `total_banned` 値を `~/.hermes/memory/vps-health-state.json` に保存し、次回チェック時に差分を計算する。
## 禁止事項
- ホスト側コマンドを実行しようとしない(コンテナの中なので物理的に不可能だが、念のため明示)
- 認証ファイルの内容(auth.json)の生エントリを Discord に流さない(IP 等の情報漏洩防止のため、件数のみ報告)
EOF
定期実行設定
Hermes 内で:
hermes # alias 経由
> このスキルを毎日朝9時に実行する cron スケジュールを設定して
> また毎時5分に異常検知だけ実行するスケジュールも追加して
Hermes が cron スキルを使って自身のスケジュールに登録する。完了後 exit で Hermes インタラクティブセッションから抜ける。
exit # hermes から管理者へ
§ 10Phase 8: バックアップとロールバック
自動バックアップスクリプト
sudo tee /usr/local/bin/hermes-backup.sh > /dev/null <<'SCRIPT'
#!/bin/bash
set -euo pipefail
BACKUP_DIR=/var/backups/hermes
DATE=$(date +%Y%m%d-%H%M)
RETENTION_DAYS=14
mkdir -p "$BACKUP_DIR"
# DB の一貫性のため、Hermes を一時停止
docker stop hermes >/dev/null
# tarball 作成
tar czf "$BACKUP_DIR/hermes-$DATE.tar.gz" \
-C /home/hermes \
--exclude='.hermes/sessions/cache' \
--exclude='.hermes/logs' \
.hermes hermes-stack
docker start hermes >/dev/null
# 保持期間超過分を削除
find "$BACKUP_DIR" -name "hermes-*.tar.gz" -mtime +$RETENTION_DAYS -delete
# サイズと整合性チェック
SIZE=$(du -h "$BACKUP_DIR/hermes-$DATE.tar.gz" | cut -f1)
tar tzf "$BACKUP_DIR/hermes-$DATE.tar.gz" >/dev/null
echo "[$(date -Iseconds)] Backup OK: hermes-$DATE.tar.gz ($SIZE)"
SCRIPT
sudo chmod 755 /usr/local/bin/hermes-backup.sh
# cron 登録(毎日 4:00)
sudo tee /etc/cron.d/hermes-backup > /dev/null <<'EOF'
0 4 * * * root /usr/local/bin/hermes-backup.sh >> /var/log/hermes-backup.log 2>&1
EOF
オフサイトバックアップ(推奨)
sudo apt install rclone
sudo rclone config # gdrive リモート等を作成
# backup スクリプトの末尾に追加(例)
# rclone copy "$BACKUP_DIR/hermes-$DATE.tar.gz" gdrive:hermes-vps-backup/
ログローテーション
/var/log/hermes-backup.log は logrotate で管理する。Docker コンテナログは docker-compose.yml の logging オプションで既にローテーション設定済み:
sudo tee /etc/logrotate.d/hermes > /dev/null <<'EOF'
/var/log/hermes-backup.log {
weekly
rotate 8
compress
delaycompress
missingok
notifempty
create 644 root root
}
EOF
# 構文チェック(dry run)
sudo logrotate -d /etc/logrotate.d/hermes
将来、collector や Hermes 内部のスキルが追加ログを /var/log/hermes-*.log 等に吐くようになったら、同じ logrotate 定義の対象に追加する。
ロールバック手順
ロールバックは管理者と hermes を行き来する:
# === 作業ユーザー: hermes ===
sudo -iu hermes
cd ~/hermes-stack
docker compose down
# 現状を退避(万一に備えて)
mv ~/.hermes ~/.hermes.broken-$(date +%Y%m%d)
# === 作業ユーザー: 管理者 ===
exit
whoami # 管理者であること
# バックアップから復元
sudo tar xzf /var/backups/hermes/hermes-20260520-0400.tar.gz \
-C /home/hermes
sudo chown -R hermes:hermes /home/hermes/.hermes /home/hermes/hermes-stack
# === 作業ユーザー: hermes ===
sudo -iu hermes
cd ~/hermes-stack
docker compose up -d
sleep 90
docker compose ps
# 動作確認
hermes chat -q "hello" # alias 設定済みなら
§ 11アップデート手順
複数のユーザーを行き来する:
# === 作業ユーザー: hermes ===
sudo -iu hermes
cd ~/hermes-stack
# 1. 最新版のリリースノートを確認
# https://github.com/NousResearch/hermes-agent/releases
# === 作業ユーザー: 管理者(バックアップ取得のため) ===
exit
sudo /usr/local/bin/hermes-backup.sh
# === 作業ユーザー: hermes ===
sudo -iu hermes
cd ~/hermes-stack
# 2. .env の HERMES_VERSION を新バージョンに書き換え
sed -i 's/^HERMES_VERSION=.*/HERMES_VERSION=v2026.6.0/' .env
cat .env # 確認
# 3. イメージ取得とコンテナ再作成
docker compose pull
docker compose up -d
sleep 90
# 4. ステータス
docker compose ps # (healthy) になること
docker compose logs --tail 50
# 5. 動作確認
hermes --help
hermes chat -q "hello"
バージョンを戻したい場合
.env の HERMES_VERSION を元の値に戻して docker compose up -d で OK。データは触らない(bind mount で永続化済み)。
§ 12注意点と既知の落とし穴
auth.json 所有権バグの対策
hermes login を docker exec -it -u root で実行する(または -u を省略してデフォルト root で動かす)と auth.json が root 所有で書かれ、その後 gosu リマップ後の hermes uid で動いている gateway から読めなくなる。対策:
- compose で
user:を指定しない。environmentにHERMES_UID/HERMES_GIDを渡し、entrypoint の gosu リマップに任せる docker execで操作するときは必ず-u hermesを付ける(デフォルトは root)- alias を
'docker exec -u hermes -it hermes /opt/hermes/.venv/bin/hermes'で固定しておくと事故防止になる - 認証ファイルが root 所有になっていたら
sudo chown -R hermes:hermes /home/hermes/.hermes
UID マッピングの正しいやり方
公式 Hermes イメージの内部 hermes ユーザーは固定 UID(10000 がデフォルト)で焼かれているが、起動時に entrypoint が usermod/groupmod で HERMES_UID/HERMES_GID の値に書き換え、gosu で権限降下する設計。
つまり:
- compose の
user:を指定してはならない(entrypoint の root フェーズが走らずリマップが失敗する) - 代わりに
environmentでHERMES_UID=$(id -u)HERMES_GID=$(id -g)を渡す - これで bind mount した
~/.hermes/の所有権とコンテナ内ユーザーの UID が一致し、permission denied が起きない - system user の UID は環境によって 998 / 999 等で異なるので、決め打ちではなく動的取得
もし以前に user: を指定して起動してしまっていた場合、~/.hermes/ 配下や ~/.hermes/google/ の所有権が壊れている可能性があるので、sudo chown -R hermes:hermes /home/hermes/.hermes でリセットしてから compose を直す。
gateway は /health エンドポイントを提供しない
healthcheck で curl http://localhost:8642/health を使うと「接続リセット」となり常に unhealthy になる。docker-compose.yml の healthcheck は bash の /dev/tcp を使った TCP 接続成立確認に切り替える:
healthcheck:
test: ["CMD-SHELL", "bash -c '</dev/tcp/localhost/8642'"]
OAuth フローはコールバックが届く経路が必須
Hermes の OAuth フロー(Grok / Anthropic OAuth)は純粋なローカルコールバック方式で、コードコピペの OOB 入力プロンプトを提供しない。setup 時にコールバックポート(毎回ランダム)に手元 PC のブラウザから到達できる経路を確保する必要がある:
- Cursor / VSCode Remote-SSH: ループバック自動転送が効くので追加作業なし
- 素の SSH:
ssh -L XXXXX:127.0.0.1:XXXXX -Nで手動転送 --network host: setup wizard のコンテナ起動時に必須
通常運用の docker compose up -d は 127.0.0.1 ローカルバインドのままで OK。--network host は setup の時だけ。
docker exec は root で実行される
docker exec hermes ... のデフォルトユーザーはコンテナの USER 指定または root。Hermes イメージは root で exec される。そのまま hermes login 系のコマンドを叩くと auth.json が root 所有で書かれてバグの引き金になる。
必ず -u hermes を付ける:
docker exec -u hermes -it hermes /opt/hermes/.venv/bin/hermes ...
alias 化を強く推奨。
hermes コマンドのパス
コンテナ内で hermes バイナリは /opt/hermes/.venv/bin/hermes にある。コンテナ内 root ユーザーの PATH には /opt/data/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin しかなく、/opt/hermes/.venv/bin は含まれない。docker exec hermes hermes ... だと「executable file not found」になる。
正しい呼び方:
# フルパス指定
docker exec -u hermes -it hermes /opt/hermes/.venv/bin/hermes chat -q "..."
# または alias 経由
hermes chat -q "..."
Provider 認証情報の保存場所
API キーベース Provider(OpenRouter 等)の場合は ~/.hermes/.env にキーが書かれる。OAuth ベース Provider(Grok、Anthropic OAuth)の場合は ~/.hermes/credentials/ 配下に保存される。.env を grep して見つからなくても認証ができているなら問題ない。
確認:
ls -la ~/.hermes/credentials/ 2>/dev/null
cat ~/.hermes/config.yaml | grep -A2 provider
docker compose down -v は禁止
-v フラグは named volume を削除する。bind mount には影響しないが、誤って named volume 化していた場合に大事故になる。常に docker compose down(フラグなし)を使う。
マルチプロファイル機能は使わない
Hermes には profile 機能(複数の独立した Hermes インスタンスを1インストールから動かす)があるが、公式が「Docker 下では非推奨」と明示している。複数プロファイルが必要になったら、別の docker-compose service として独立した container 化する。
スキルが OS パッケージを要求する場合
ごく稀に、新しいスキルがコンテナ内に存在しない OS パッケージを要求する。対処:
- 公式イメージで足りない → Dockerfile を作って自前ビルド
- ```dockerfile
- FROM nousresearch/hermes-agent:v2026.5.16
- USER root
- RUN apt update && apt install -y <package>
- USER hermes
- ```
- 一時的に試したい →
docker exec -u root -it hermes apt install <package>(次のdocker pullで消える)
sudo support は絶対に有効にしない
setup wizard の Enable sudo support? (stores password for apt install, etc.) には 必ず N を回答する。
- hermes ユーザーは sudo を持たないので機能しない
- パスワードを Hermes 内部に保存することになり情報漏洩リスク
- bAI 思想(LLM は事前整形された情報のみ扱う、OS レベル操作させない)に反する
秘匿情報の扱い
~/hermes-stack/.env と ~/.hermes/.env には API キー、bot token、OAuth secret 等が入る。
- mode 600 を必ず維持
- git に commit しない
- バックアップ tarball には含まれるので、tarball も 600 で保管
- オフサイトバックアップ先の暗号化を確認
gateway ポートはローカルバインドのみ
docker-compose.yml で "127.0.0.1:8642:8642" のように明示的にローカルバインドする。"8642:8642" だとすべてのインタフェースで listen し、攻撃面が広がる。
外部から dashboard を見たい場合は SSH ポートフォワード経由で(または Cursor / VSCode Remote-SSH の自動転送で勝手にやってくれる):
ssh -L 9119:127.0.0.1:9119 admin@your-vps
# ブラウザで http://localhost:9119
Discord bot を gateway として使う場合
bot token は強力(DM 経由でコマンド実行できる)。
HERMES_DISCORD_ALLOWED_USERSで自分の Discord user ID のみに制限- bot 設定で「Allow direct messages」以外のスコープは付けない
- token がリークしたら即 regenerate
auth.json の上書き再認証バグ(Issue #4718)
Google OAuth の再認証時に、新しいトークンが古いトークンの権限を狭めて上書きしてしまうケースがある。再認証時は同じスコープセットを指定すること。
docker グループ = 実質 root
docker グループ所属は docker run --privileged -v /:/host などで実質 root 相当の権限を持つ。VPS が Hermes 専用である前提で許容している。本気で隔離したいなら rootless Docker への移行を検討。
Phase をまたぐユーザー切り替えで詰まったら
各セクションの冒頭に「作業ユーザー」を明示しているが、長時間かけて作業すると今どちらにいるか分からなくなる。詰まったら以下を確認:
whoami
# 期待値:
# - 管理者作業中: 自分の通常ユーザー名(例: <管理者ユーザー>)
# - hermes 作業中: hermes
exit で hermes から管理者に戻れる。sudo -iu hermes で管理者から hermes に切り替えられる。hermes から hermes には入れない(sudo を持っていないため)。
§ 13検証チェックリスト
セットアップ完了時にこれらをすべて通過していること。
Phase 1: 基盤
- ☐
lsb_release -aで Ubuntu 24.04 LTS または 26.04 LTS と確認 - ☐
systemctl is-active fail2ban→ active - ☐
ufw statusで必要なルールがある - ☐
timedatectl statusで時刻同期 OK - ☐SSH クライアントの種類(Cursor / VSCode Remote-SSH か否か)を把握済み
Phase 2: Docker
- ☐
sudo systemctl is-active docker→ active - ☐
docker --versionで 27.x.x 以上 - ☐
docker compose versionで v2.x.x - ☐
sudo docker run --rm hello-world成功 - ☐
getent group dockerで docker グループ存在 - ☐
/etc/apt/keyrings/docker.ascがサイズ約 1.7KB で存在 - ☐
/etc/apt/sources.list.d/docker.sourcesの Suites が現在の OS コードネーム
Phase 3: hermes ユーザー
- ☐
id hermesで UID が < 1000(system user) - ☐
sudo passwd -S hermesの2列目がL(ロック) - ☐
groups hermesに docker が含まれる - ☐
/etc/ssh/sshd_configにDenyUsers hermesが記載 - ☐
sudo -iu hermes→docker ps成功 - ☐ホームディレクトリ
/home/hermes/所有者がhermes:hermes
Phase 4: コンテナ
- ☐
~/hermes-stack/docker-compose.ymlと~/hermes-stack/.envが hermes 所有・mode 600 - ☐
docker-compose.ymlにuser:行がないこと - ☐
docker-compose.ymlのenvironmentにHERMES_UID/HERMES_GID/HERMES_DASHBOARD=1がある - ☐healthcheck が TCP 接続確認方式(
/dev/tcp/localhost/8642) - ☐
docker compose psで hermes が(healthy) - ☐
docker exec -u hermes hermes idでホスト hermes と同じ UID/GID - ☐
docker exec hermes env | grep HERMES_DASHBOARDで1 - ☐
docker exec -u hermes -it hermes /opt/hermes/.venv/bin/hermes chat -q "hello"が応答 - ☐alias
hermesが.bashrcで設定済み - ☐
curl -fsS http://127.0.0.1:9119で dashboard HTML が返る - ☐Provider 認証完了確認(
hermes chat -q "..."で応答が返る) - ☐永続化検証: コンテナ削除→再作成後にテストファイルが残り、所有者も保持される
- ☐
docker inspect hermes | jq '.[0].Mounts'で Type: bind を確認
Phase 5: Google Workspace
- ☐カレンダーの予定が取れる
- ☐Gmail 検索ができる
- ☐OAuth トークンファイルが hermes 所有
- ☐スコープに必要な権限が全て含まれている
Phase 6: 監視
- ☐
fail2ban-client statusで実際の jail 名を確認済み - ☐
journalctl _COMM=sshdで認証イベントが拾えることを確認済み - ☐
*/5 * * * *cron が動いている (cat /var/log/syslog | grep CRON) - ☐全 4 種類の JSON が 5 分以内に更新されている
- ☐各 JSON が
jqで valid(auth.json は NDJSON としてjq -s) - ☐コンテナから
/opt/data/health/system.jsonが読める - ☐
/var/lib/hermes-health/がroot:hermes750
Phase 7: スキル
- ☐ヘルスチェックスキルが Hermes 内で実行できる
- ☐異常検知時の Discord 通知が動く(人為的にディスクを満杯近くにして実証)
Phase 8: バックアップ
- ☐
/usr/local/bin/hermes-backup.shを手動実行して成功 - ☐tarball の整合性 OK (
tar tzf <file>がエラーなし) - ☐cron 登録済み
- ☐復元テストを1回実施(別ディレクトリで展開してみる)
- ☐
/etc/logrotate.d/hermes配置済み、logrotate -dで syntax OK
ネットワークとセキュリティ
- ☐gateway ポート 8642 が 127.0.0.1 のみで listen (
ss -ltn | grep 8642) - ☐dashboard ポート 9119 が 127.0.0.1 のみで listen
- ☐
.envファイル群が mode 600(~/.hermes/.env、~/hermes-stack/.envの両方) - ☐バックアップディレクトリへの一般ユーザーアクセス不可
- ☐Discord bot の許可ユーザーが自分のみに制限
- ☐hermes ユーザーへ SSH 直接ログイン不可
§ 14日常運用のチートシート
# === 作業ユーザー: 管理者 ===
# Hermes に切り替え
sudo -iu hermes
# === 以下は作業ユーザー: hermes ===
# (alias `hermes` が .bashrc 設定済みの前提)
# 状態確認
cd ~/hermes-stack
docker compose ps
docker compose logs --tail 50 hermes
# Hermes と対話
hermes # インタラクティブ
hermes chat -q "なにかつぶやいて" # ワンショット
hermes sessions list # 過去セッション
hermes --resume <session_id> # 再開
# 設定確認
hermes config show
hermes doctor
# 完全再起動
docker compose restart hermes
# 完全リセット(データ保持)
docker compose down
docker compose up -d
sleep 90 && docker compose ps # healthy 確認
# バージョンアップ
vi .env # HERMES_VERSION 書き換え
docker compose pull && docker compose up -d
sleep 90 && docker compose ps
hermes doctor
# === 作業ユーザー: 管理者(戻る場合は exit) ===
exit
# ヘルスチェック手動実行
sudo /usr/local/bin/hermes-health-collect.sh
cat /var/lib/hermes-health/*.json | jq .
# バックアップ手動実行
sudo /usr/local/bin/hermes-backup.sh
# ログ確認
sudo tail -f /var/log/hermes-backup.log
sudo journalctl -u docker -f
§ 15参考リンク
- Hermes Agent 公式ドキュメント: https://hermes-agent.nousresearch.com/docs/
- Docker デプロイガイド: https://hermes-agent.nousresearch.com/docs/user-guide/docker
- Google Workspace スキル: https://hermes-agent.nousresearch.com/docs/user-guide/skills/google-workspace
- リリースノート: https://github.com/NousResearch/hermes-agent/releases
- Issues(バグ・機能要望): https://github.com/NousResearch/hermes-agent/issues
- Docker Hub: https://hub.docker.com/r/nousresearch/hermes-agent
- Docker Engine 公式インストール: https://docs.docker.com/engine/install/ubuntu/
- Ubuntu 26.04 LTS リリースノート: https://documentation.ubuntu.com/release-notes/26.04/
§ 16次の拡張ポイント
セットアップが安定したあとに検討する余地のある拡張:
- Discord 通知の経路: Hermes 内蔵の Discord gateway を使うか、別途 webhook 経由にするか
- MCP サーバー追加: Obsidian MCP との連携、個人の作業フロー統合
- collector の拡張: nginx アクセスログ、Docker コンテナ自体のメトリクス、外部 ping 監視
- second VPS への複製: 同一構成を別 VPS に立てて、相互ヘルスチェック
- rclone でのオフサイトバックアップ: Google Drive / R2 / B2 等
- rootless Docker への移行: docker グループ=実質 root の問題を解消
- Claude Code 経由の Anthropic OAuth: Claude Max の overage credits を Hermes に共有する経路の検討
これらは別途設計が必要だが、現在の構成を素直に拡張できる範囲。
§ 17変更履歴
v1 → v2 の変更点
- Phase 順序を整理: Docker インストール → hermes ユーザー作成
- hermes ユーザーを system user として作成
- SSH 直接ログイン禁止 (
DenyUsers) を明示 - データ永続化の検証ステップを Phase 4 に追加
- UID マッピング周りの注意を追記
v2 → v3 の変更点
- Docker インストール手順を公式現行版(DEB822 形式と
.asc鍵)に更新 - 競合パッケージ削除リストを公式の網羅版に差し替え
gpg --dearmorステップが不要に
v3 → v4 の変更点
- docker-compose.yml の UID/GID 扱いを修正:
user:指定を削除し、environmentでHERMES_UID/HERMES_GIDを渡す方式に変更 - setup wizard を compose 起動より前に実行する手順に変更
- API キーを compose の environment から削除
- logging オプションを追加
- collector スクリプトを改善
- fail2ban jail 名と journal 単位名の事前確認手順を追加
- logrotate 設定を追加
- Phase 4 永続化検証に dashboard 起動確認と UID マッピング検証を追加
- 12章の auth.json と UID マッピング項を
user:非推奨に書き換え
v4 → v4.1 の変更点
- Ubuntu 26.04 LTS(Resolute Raccoon)対応を明示
- Phase 3 末尾の「以降の管理方法」を Phase 3 時点で実行可能な内容に修正
- 各 Phase に「作業ユーザー」明示を追加
- 表記規約セクションを冒頭に追加
v4.1 → v4.2 の変更点
実機セットアップで判明した重要な仕様と落とし穴を全反映:
- OAuth ベース Provider(Grok / Anthropic OAuth)の setup wizard は
--network hostで起動する必要がある: コールバックを受け取れる経路を確保するため - Cursor / VSCode Remote-SSH のループバック自動転送について明記: これがあると
--network hostだけで OAuth が通る - 素の SSH の場合の対処:
ssh -Lでのポートフォワード手順を追加 - hermes バイナリの絶対パスを明記: コンテナ内で
/opt/hermes/.venv/bin/hermes docker execのデフォルトユーザー注意: root で実行されるため-u hermesを必須化- alias 設定の推奨: 日常運用での煩雑さを軽減
- healthcheck の修正: gateway は
/healthエンドポイントを提供しないため、TCP 接続確認方式へ変更 - system user UID が環境によって 998 / 999 等で異なる点を明記: Xserver VPS は 999
- Provider 設定が
.envに書かれない場合の説明: OAuth は credentials ファイルに保存される - setup wizard 中断・時間切れ時の対処手順を追加
- OAuth の「コードコピペ画面」は Hermes 側に貼り付け先が無い旨を明記: Grok 側のフォールバック表示なので、Hermes 側では使えない
- ファイル作成手順を Phase 4 で全部
cat > ... <<EOF形式に統一
v4.2 → v4.3 の変更点
- Phase 3 動作確認ブロックのユーザー切り替え事故を修正: 1ブロック内で「管理者 → hermes → 管理者」と切り替わる構成だったため、
exit忘れで hermes プロンプトのままsudo grepを実行→拒否されるケースがあった。管理者で実行する確認を前半にまとめ、hermes への切り替えをブロック末尾に移動。「hermes は sudo 不可」の注記も追加