v0.50.260: Docker reliability batch - PR #1428 + broader UX/docs improvements + Opus advisor fixes
Combines PR #1428 (UID/GID alignment) with a broader Docker reliability pass that addresses recurring user reports about compose files not working. Constituent PR: - #1428 sunnysktsang - Align agent UID/GID with webui (fixes #1399). Two- and three-container compose files had agent at UID 10000 (image default) and webui at UID 1000 (WANTED_UID default), causing permission denied on shared hermes-home volume. All services now use ${UID:-1000}. Plus broader Docker UX overhaul: - All 3 compose files document HERMES_SKIP_CHMOD/HERMES_HOME_MODE escape hatches inline (the v0.50.254 fix wasn't surfaced for Docker users). - New .env.docker.example template covering UID/GID, paths, password, permission handling. UID/GID are uncommented with placeholder values per Opus advisor (so macOS users don't skim past). - New docs/docker.md - comprehensive guide: 5-min quickstart, failure mode table with one-line fixes, bind-mount migration, multi-container architecture diagram, macOS Docker Desktop VirtioFS note, link to community sunnysktsang/hermes-suite all-in-one image. - README Docker section rewritten - clearer quickstart, failure-mode table, link to docs/docker.md. Stale /root/.hermes references removed. Plus Opus pre-release advisor MUST-FIX: - HERMES_HOME_MODE has DIFFERENT semantics in the WebUI vs the agent image. WebUI: credential-file mode threshold (0640 allows group bits). Agent: HERMES_HOME directory mode (default 0700). 0640 on a directory has no owner-execute bit, so the agent can't traverse its own home and bricks. My initial draft recommended HERMES_HOME_MODE=0640 in agent service blocks - corrected to 0750 across all 4 surfaces (compose files, .env.docker.example, docs/docker.md). 3 regression tests pin the asymmetry. 12 regression tests total in test_v050260_docker_invariants.py. Full suite: 3627 passed, 0 failed. Nathan explicitly authorized merge with my own review + Opus only, no independent review needed.
This commit is contained in:
89
.env.docker.example
Normal file
89
.env.docker.example
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
# Hermes Web UI — Docker Compose configuration template
|
||||||
|
#
|
||||||
|
# Copy this file to `.env` next to your docker-compose.yml.
|
||||||
|
# All variables are optional — Docker Compose substitutes defaults if unset.
|
||||||
|
#
|
||||||
|
# cp .env.docker.example .env
|
||||||
|
# # edit values you care about, then:
|
||||||
|
# docker compose up -d
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────
|
||||||
|
# UID / GID — host user mapping
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────
|
||||||
|
# Critical when bind-mounting an EXISTING host directory (e.g. ~/.hermes).
|
||||||
|
# The container runs as UID/GID and must match your host file ownership,
|
||||||
|
# otherwise the container can't read your config.yaml or write sessions.
|
||||||
|
#
|
||||||
|
# Find yours: id -u (UID) | id -g (GID)
|
||||||
|
#
|
||||||
|
# On macOS, UIDs start at 501 (not 1000), so you MUST set these.
|
||||||
|
# On Linux, the default of 1000 usually matches the first interactive user.
|
||||||
|
#
|
||||||
|
# REPLACE THESE WITH YOUR ACTUAL VALUES (run `id -u` and `id -g`):
|
||||||
|
UID=1000
|
||||||
|
GID=1000
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────
|
||||||
|
# Hermes home directory — single-container compose only
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────
|
||||||
|
# Where on the host your config, sessions, skills, and state live.
|
||||||
|
# Default: ~/.hermes (works for everyone with a standard install)
|
||||||
|
# Override if your .hermes is elsewhere:
|
||||||
|
# HERMES_HOME=/opt/hermes-data
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────
|
||||||
|
# Workspace directory — single-container compose
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────
|
||||||
|
# Path to your code/project directory. The WebUI's file browser shows
|
||||||
|
# this at /workspace inside the container.
|
||||||
|
# Default: ~/workspace
|
||||||
|
# HERMES_WORKSPACE=/home/me/dev
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────
|
||||||
|
# Password — protect remote access
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────
|
||||||
|
# REQUIRED if you expose the container on anything other than 127.0.0.1.
|
||||||
|
# Without a password, anyone who can reach the port can run commands as
|
||||||
|
# the agent.
|
||||||
|
# HERMES_WEBUI_PASSWORD=change-me-to-something-strong
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────
|
||||||
|
# Permission handling for bind-mounted .hermes — advanced
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────
|
||||||
|
# By default, the WebUI's startup credential-permission fixer enforces
|
||||||
|
# 0600 mode on .env, auth.json, and similar credential files in HERMES_HOME.
|
||||||
|
# This is the right behavior on a clean install, but it can clash with:
|
||||||
|
#
|
||||||
|
# - Bind-mounting an EXISTING ~/.hermes whose .env is intentionally 0640
|
||||||
|
# (e.g. group-readable for a Docker user group)
|
||||||
|
# - HERMES_HOME_MODE configured at the agent level for a multi-user setup
|
||||||
|
#
|
||||||
|
# To bypass the WebUI's fixer entirely:
|
||||||
|
# HERMES_SKIP_CHMOD=1
|
||||||
|
#
|
||||||
|
# OR to allow group bits while still stripping world-readable:
|
||||||
|
# HERMES_HOME_MODE=0640
|
||||||
|
#
|
||||||
|
# ⚠️ MULTI-CONTAINER WARNING: HERMES_HOME_MODE has DIFFERENT semantics in
|
||||||
|
# the WebUI vs. the agent image:
|
||||||
|
# - WebUI: credential FILE mode threshold (0640 = allow group bits)
|
||||||
|
# - Agent: HERMES_HOME *directory* mode (default 0700)
|
||||||
|
# 0640 on a directory has NO execute bit, so the agent can't enter its own
|
||||||
|
# home → broken. If you set HERMES_HOME_MODE for a multi-container setup,
|
||||||
|
# use 0750 (group-traversable) or 0701 (x-only for non-owner traversal).
|
||||||
|
# The compose files document both correctly per-service.
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────
|
||||||
|
# Multi-container only — used by docker-compose.two-container.yml and
|
||||||
|
# docker-compose.three-container.yml
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────
|
||||||
|
# These compose files use named Docker volumes by default (recommended).
|
||||||
|
# Set the variables above to your host UID/GID — both the agent container
|
||||||
|
# (HERMES_UID/HERMES_GID) and the webui container (WANTED_UID/WANTED_GID)
|
||||||
|
# are derived from $UID/$GID so files written by one are readable by the
|
||||||
|
# other.
|
||||||
|
#
|
||||||
|
# If you switch to bind mounts (replacing `hermes-home: {}` with a `device:`
|
||||||
|
# bind), ALL THREE containers must mount the SAME host path and run as the
|
||||||
|
# SAME UID/GID. Mismatched UIDs → "Permission denied" → the WebUI crashes
|
||||||
|
# on every HTTP request because it can't read its own auth signing key.
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -12,10 +12,11 @@ __pycache__/
|
|||||||
# Archive directory (pre-git backups, kept on disk but not tracked)
|
# Archive directory (pre-git backups, kept on disk but not tracked)
|
||||||
archive/
|
archive/
|
||||||
|
|
||||||
# Local environment and secrets (but keep the example template)
|
# Local environment and secrets (but keep the example templates)
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
!.env.docker.example
|
||||||
.claude/
|
.claude/
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
AGENTS.md
|
AGENTS.md
|
||||||
@@ -39,6 +40,7 @@ Thumbs.db
|
|||||||
docs/*
|
docs/*
|
||||||
!docs/ui-ux/
|
!docs/ui-ux/
|
||||||
!docs/ui-ux/**
|
!docs/ui-ux/**
|
||||||
|
!docs/docker.md
|
||||||
|
|
||||||
# Local-only PR review harness: rendering drivers, sample bank, fixtures.
|
# Local-only PR review harness: rendering drivers, sample bank, fixtures.
|
||||||
# Used by Claude during deep reviews; never shared in the repo.
|
# Used by Claude during deep reviews; never shared in the repo.
|
||||||
|
|||||||
17
CHANGELOG.md
17
CHANGELOG.md
@@ -2,6 +2,23 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [v0.50.260] — 2026-05-01
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Docker compose UID/GID alignment** (#1428, fixes #1399) — the two- and three-container compose files had a UID mismatch between containers sharing the `hermes-home` volume: `hermes-agent` and `hermes-dashboard` ran as UID 10000 (image default) while `hermes-webui` ran as UID 1000 (`WANTED_UID` default), causing `Permission denied` errors on every shared file. All services now read from `${UID:-1000}` and `${GID:-1000}` so they align by construction. Empirically tested on both two- and three-container setups by the contributor. (`docker-compose.two-container.yml`, `docker-compose.three-container.yml`) @sunnysktsang — PR #1428
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Docker UX overhaul** — Docker reliability has been a recurring pain point. This release ships a coordinated set of doc/config improvements:
|
||||||
|
- **All 3 compose files** now document the `HERMES_SKIP_CHMOD` and `HERMES_HOME_MODE` escape hatches inline (the v0.50.254 fix for #1389 wasn't surfaced for Docker users).
|
||||||
|
- **New `.env.docker.example`** template specifically for Docker users, covering UID/GID, paths, password, and permission-handling escape hatches with explicit `UID=1000`/`GID=1000` placeholders so macOS users don't skim past the warning.
|
||||||
|
- **New `docs/docker.md`** — comprehensive guide covering all 3 compose files, common failure modes (with one-line fixes), bind-mount migration recipe, multi-container architecture diagram, macOS Docker Desktop file-sharing implementation note, and pointer to the [community all-in-one image](https://github.com/sunnysktsang/hermes-suite) for Podman 3.4 / multi-arch users.
|
||||||
|
- **README Docker section rewritten** — clearer 5-minute quickstart pointing at the single-container setup; failure-mode table with one-line fixes; pointer to `docs/docker.md` for the deep dive; **stale `/root/.hermes` reference removed** (the agent images use `/home/hermes/.hermes`).
|
||||||
|
- **12 regression tests** in `tests/test_v050260_docker_invariants.py` — UID/GID alignment positive + negative-pattern guards, escape-hatch documentation, `.env.docker.example` shape, `docs/docker.md` failure-mode coverage, README link integrity, and YAML validity for all 3 compose files. (`docker-compose.yml`, `docker-compose.two-container.yml`, `docker-compose.three-container.yml`, `.env.docker.example`, `docs/docker.md`, `README.md`, `tests/test_v050260_docker_invariants.py`)
|
||||||
|
|
||||||
|
### Changed (Opus pre-release advisor)
|
||||||
|
- **`HERMES_HOME_MODE` semantic asymmetry warning** — Opus advisor caught a footgun in my initial draft: `HERMES_HOME_MODE` means **different things** in the WebUI vs. the agent image. WebUI's `HERMES_HOME_MODE` is a credential-FILE mode threshold (e.g. `0640` allows group bits on `.env`), but the agent's `HERMES_HOME_MODE` is the HERMES_HOME *directory* mode (default `0700`). `0640` on a directory has no owner-execute bit, so the agent can't traverse its own home directory and bricks. My initial draft recommended `HERMES_HOME_MODE=0640` as the example value in agent service blocks — corrected to `0750` (group-traversable) for multi-container setups. All three surfaces now match: compose files (per-service comments), `.env.docker.example` (multi-container warning section), `docs/docker.md` (failure mode #2 callout). 3 new regression tests pin the asymmetry: `test_agent_service_does_not_recommend_invalid_home_mode`, `test_compose_files_warn_about_home_mode_asymmetry`, `test_env_docker_example_warns_about_home_mode_asymmetry`. (`docker-compose.two-container.yml`, `docker-compose.three-container.yml`, `.env.docker.example`, `docs/docker.md`, `tests/test_v050260_docker_invariants.py`)
|
||||||
|
|
||||||
|
|
||||||
## [v0.50.259] — 2026-05-01
|
## [v0.50.259] — 2026-05-01
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
190
README.md
190
README.md
@@ -125,157 +125,91 @@ If provider setup is still incomplete after install, the onboarding wizard will
|
|||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
**Pre-built images** (amd64 + arm64) are published to GHCR on every release:
|
**Pre-built images** (amd64 + arm64) are published to GHCR on every release.
|
||||||
|
|
||||||
Make sure the `HERMES_WEBUI_STATE_DIR` (by default `~/.hermes/webui-mvp`, as detailed in the `.env.example` file) folder exist with the UID/GID of the owner of the `.hermes` folder.
|
For a comprehensive setup guide covering all 3 compose files, common failure modes, and bind-mount migration, see [`docs/docker.md`](docs/docker.md). The README covers the 5-minute happy path.
|
||||||
The container will also mount your configured "workspace" (also from the example .env.example) as `/workspace`. adapt the location as needed.
|
|
||||||
|
|
||||||
|
### 5-minute quickstart (single container)
|
||||||
|
|
||||||
|
The simplest setup: one WebUI container that runs the agent in-process.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/nesquena/hermes-webui
|
||||||
|
cd hermes-webui
|
||||||
|
cp .env.docker.example .env
|
||||||
|
# Edit .env if your host UID isn't 1000 (e.g. macOS where UIDs start at 501)
|
||||||
|
docker compose up -d
|
||||||
|
# Open http://localhost:8787
|
||||||
|
```
|
||||||
|
|
||||||
|
The container auto-detects your UID/GID from the mounted `~/.hermes` volume so files written by the agent stay readable by you on the host.
|
||||||
|
|
||||||
|
To enable password protection (required if you expose the port outside `127.0.0.1`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "HERMES_WEBUI_PASSWORD=change-me-to-something-strong" >> .env
|
||||||
|
docker compose up -d --force-recreate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual `docker run` (no compose)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker pull ghcr.io/nesquena/hermes-webui:latest
|
docker pull ghcr.io/nesquena/hermes-webui:latest
|
||||||
docker run -d \
|
docker run -d \
|
||||||
-e WANTED_UID=`id -u` -e WANTED_GID=`id -g` \
|
-e WANTED_UID=$(id -u) -e WANTED_GID=$(id -g) \
|
||||||
-v ~/.hermes:/home/hermeswebui/.hermes -e HERMES_WEBUI_STATE_DIR=/home/hermeswebui/.hermes/webui-mvp \
|
-v ~/.hermes:/home/hermeswebui/.hermes \
|
||||||
-v ~/workspace:/workspace \
|
-e HERMES_WEBUI_STATE_DIR=/home/hermeswebui/.hermes/webui \
|
||||||
-p 8787:8787 ghcr.io/nesquena/hermes-webui:latest
|
-v ~/workspace:/workspace \
|
||||||
|
-p 127.0.0.1:8787:8787 \
|
||||||
|
ghcr.io/nesquena/hermes-webui:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
Or run with Docker Compose (recommended):
|
### Build locally
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check the docker-compose.yml and make sure to adapt as needed, at minimum WANTED_UID/WANTED_GID
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
Or build locally:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -t hermes-webui .
|
docker build -t hermes-webui .
|
||||||
docker run -d \
|
docker run -d \
|
||||||
-e WANTED_UID=`id -u` -e WANTED_GID=`id -g` \
|
-e WANTED_UID=$(id -u) -e WANTED_GID=$(id -g) \
|
||||||
-v ~/.hermes:/home/hermeswebui/.hermes -e HERMES_WEBUI_STATE_DIR=/home/hermeswebui/.hermes/webui-mvp \
|
-v ~/.hermes:/home/hermeswebui/.hermes \
|
||||||
-v ~/workspace:/workspace \
|
-e HERMES_WEBUI_STATE_DIR=/home/hermeswebui/.hermes/webui \
|
||||||
-p 8787:8787 hermes-webui
|
-v ~/workspace:/workspace \
|
||||||
|
-p 127.0.0.1:8787:8787 \
|
||||||
|
hermes-webui
|
||||||
```
|
```
|
||||||
|
|
||||||
Open http://localhost:8787 in your browser.
|
### Multi-container setups
|
||||||
|
|
||||||
To enable password protection:
|
If you want the agent and WebUI in separate containers (for isolation, or because you're already running an agent gateway elsewhere):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d \
|
# Agent + WebUI
|
||||||
-e WANTED_UID=`id -u` -e WANTED_GID=`id -g` \
|
docker compose -f docker-compose.two-container.yml up -d
|
||||||
-v ~/.hermes:/home/hermeswebui/.hermes -e HERMES_WEBUI_STATE_DIR=/home/hermeswebui/.hermes/webui-mvp \
|
|
||||||
-v ~/workspace:/workspace \
|
# Agent + Dashboard + WebUI
|
||||||
-p 8787:8787 -e HERMES_WEBUI_PASSWORD=your-secret ghcr.io/nesquena/hermes-webui:latest
|
docker compose -f docker-compose.three-container.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Both compose files use **named Docker volumes** by default, which solves the UID/GID problem by construction. If you need bind mounts to share an existing host directory, see [`docs/docker.md`](docs/docker.md) for the full migration recipe.
|
||||||
|
|
||||||
|
> **Known limitation (#681)**: in the two-container setup, tools triggered from the WebUI run in the **WebUI container**, not the agent container. If you need git/node/etc. on the WebUI's filesystem, either use the single-container setup, extend the WebUI Dockerfile, or use the community [all-in-one image](https://github.com/sunnysktsang/hermes-suite).
|
||||||
|
|
||||||
|
### Common failure modes
|
||||||
|
|
||||||
|
| Symptom | Likely cause | Fix |
|
||||||
|
|---|---|---|
|
||||||
|
| `PermissionError` at startup | UID mismatch on bind mount | Set `UID=$(id -u)` in `.env` |
|
||||||
|
| `.env: permission denied` (#1389) | `fix_credential_permissions()` enforced 0600 | Set `HERMES_SKIP_CHMOD=1` in `.env` |
|
||||||
|
| Workspace appears empty | UID mismatch on `/workspace` mount | Set `UID=$(id -u)` in `.env` |
|
||||||
|
| `git: command not found` in chat | Two-container architectural limit (#681) | Use single-container or extend Dockerfile |
|
||||||
|
| WebUI can't find agent source | `hermes-agent-src` volume misconfigured | Use the named volumes from compose files as-is |
|
||||||
|
| Podman shared `.hermes` fails | Podman 3.4 `keep-id` limitation | Use Podman 4+ or single-container |
|
||||||
|
|
||||||
|
For the deep dive on each of these, see [`docs/docker.md`](docs/docker.md).
|
||||||
|
|
||||||
> **Note:** By default, Docker Compose binds to `127.0.0.1` (localhost only).
|
> **Note:** By default, Docker Compose binds to `127.0.0.1` (localhost only).
|
||||||
> To expose on a network, change the port to `"8787:8787"` in `docker-compose.yml`
|
> To expose on a network, change the port to `"8787:8787"` in `docker-compose.yml`
|
||||||
> and set `HERMES_WEBUI_PASSWORD` to enable authentication.
|
> and set `HERMES_WEBUI_PASSWORD` to enable authentication.
|
||||||
|
|
||||||
### Two-container setup (Agent + WebUI)
|
|
||||||
|
|
||||||
If you run the Hermes Agent in its own Docker container and want the WebUI
|
|
||||||
in a separate container:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f docker-compose.two-container.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
This starts both containers with shared volumes:
|
|
||||||
|
|
||||||
- **`hermes-home`** — shared `~/.hermes` for config, sessions, skills, memory
|
|
||||||
- **`hermes-agent-src`** — the agent's source code, mounted into the WebUI
|
|
||||||
container so it can install the agent's Python dependencies at startup
|
|
||||||
|
|
||||||
> **Volume type:** The compose files use named Docker volumes by default.
|
|
||||||
> If you prefer bind mounts to an existing directory (e.g. for sharing state
|
|
||||||
> with an agent container you already run), both containers must mount the
|
|
||||||
> same host path — the agent writes to `/root/.hermes`, the WebUI reads from
|
|
||||||
> `/home/hermeswebui/.hermes`. See `docker-compose.two-container.yml` for
|
|
||||||
> a bind-mount example.
|
|
||||||
|
|
||||||
The WebUI's init script automatically installs hermes-agent and all its
|
|
||||||
dependencies (openai, anthropic, etc.) into its own Python environment on
|
|
||||||
first boot. Subsequent restarts reuse the installed packages.
|
|
||||||
|
|
||||||
> **How it works:** The WebUI imports hermes-agent's Python modules directly
|
|
||||||
> (not via HTTP). The shared volume makes the agent source available, and
|
|
||||||
> the init script runs `uv pip install` to set up the dependencies. Both
|
|
||||||
> containers share the same `~/.hermes` directory for config and state.
|
|
||||||
|
|
||||||
See `docker-compose.two-container.yml` for the full configuration.
|
|
||||||
|
|
||||||
### Running alongside hermes-dashboard (three-container setup)
|
|
||||||
|
|
||||||
To run the Hermes Agent, Hermes Dashboard, and the WebUI together on a
|
|
||||||
shared volume, use the three-container Compose file:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f docker-compose.three-container.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
This brings up:
|
|
||||||
- **`hermes-agent`** — gateway API on port 8642
|
|
||||||
- **`hermes-dashboard`** — monitoring UI on port 9119
|
|
||||||
- **`hermes-webui`** — browser chat interface on port 8787
|
|
||||||
|
|
||||||
All three services share the same `hermes-home` named volume so config,
|
|
||||||
sessions, skills, and memory are consistent across all surfaces.
|
|
||||||
|
|
||||||
#### Why UIDs must match
|
|
||||||
|
|
||||||
The `hermes-home` volume is a bind-mount in practice — all three containers
|
|
||||||
write to the same filesystem tree under `~/.hermes`. If the containers run
|
|
||||||
as different UIDs, whichever container creates a file first becomes its
|
|
||||||
owner, and the others hit `PermissionError` on subsequent writes.
|
|
||||||
|
|
||||||
The fix is to make all containers run as **your host user's UID and GID**.
|
|
||||||
|
|
||||||
#### Variable name asymmetry
|
|
||||||
|
|
||||||
> ⚠️ **The two image families use different environment variable names** for
|
|
||||||
> the UID/GID setting:
|
|
||||||
>
|
|
||||||
> | Image | Variable |
|
|
||||||
> |---|---|
|
|
||||||
> | `nousresearch/hermes-agent` (agent + dashboard) | `HERMES_UID` / `HERMES_GID` |
|
|
||||||
> | `ghcr.io/nesquena/hermes-webui` | `WANTED_UID` / `WANTED_GID` |
|
|
||||||
>
|
|
||||||
> You must set **both pairs** when using a `.env` file.
|
|
||||||
|
|
||||||
#### Recommended setup
|
|
||||||
|
|
||||||
For a standard Linux user (UID ≥ 1000):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Create a .env file with your host UID/GID
|
|
||||||
echo "UID=$(id -u)" >> .env
|
|
||||||
echo "GID=$(id -g)" >> .env
|
|
||||||
# hermes-agent / hermes-dashboard
|
|
||||||
echo "HERMES_UID=$(id -u)" >> .env
|
|
||||||
echo "HERMES_GID=$(id -g)" >> .env
|
|
||||||
```
|
|
||||||
|
|
||||||
For NAS/Unraid deployments where a fixed service account is preferred, use
|
|
||||||
`10000:10000` (or your NAS service UID) instead of `$(id -u)`.
|
|
||||||
|
|
||||||
If you get `PermissionError` on an **existing** `~/.hermes` directory, run
|
|
||||||
the one-time ownership fix:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
chown -R $(id -u):$(id -g) ~/.hermes
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Volume mount mode
|
|
||||||
|
|
||||||
The dashboard container needs **read-write** access to the shared volume
|
|
||||||
(it writes session logs and dashboard state). Do **not** add `:ro` to the
|
|
||||||
`hermes-home` volume in `hermes-dashboard`'s `volumes:` entry.
|
|
||||||
|
|
||||||
See `docker-compose.three-container.yml` for the full reference configuration.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## What start.sh discovers automatically
|
## What start.sh discovers automatically
|
||||||
|
|||||||
@@ -1,24 +1,37 @@
|
|||||||
# Three-container Docker Compose: Hermes Agent + Dashboard + WebUI
|
# Three-container Docker Compose: Hermes Agent + Dashboard + WebUI
|
||||||
#
|
#
|
||||||
|
# QUICK START:
|
||||||
|
# docker compose -f docker-compose.three-container.yml up -d
|
||||||
|
# Open http://localhost:8787 (chat) and http://localhost:9119 (dashboard)
|
||||||
|
#
|
||||||
# This extends the two-container setup with the Hermes Dashboard for
|
# This extends the two-container setup with the Hermes Dashboard for
|
||||||
# monitoring agent activity, sessions, and resource usage.
|
# monitoring agent activity, sessions, and resource usage.
|
||||||
#
|
#
|
||||||
# Usage:
|
|
||||||
# docker compose -f docker-compose.three-container.yml up -d
|
|
||||||
#
|
|
||||||
# Services:
|
# Services:
|
||||||
# hermes-agent — gateway API on port 8642 (CLI, Telegram, cron, tools)
|
# hermes-agent — gateway API on port 8642 (CLI, Telegram, cron, tools)
|
||||||
# hermes-dashboard — monitoring dashboard on port 9119
|
# hermes-dashboard — monitoring dashboard on port 9119
|
||||||
# hermes-webui — browser chat interface on port 8787
|
# hermes-webui — browser chat interface on port 8787
|
||||||
#
|
#
|
||||||
# All three share the same hermes-home volume so config, sessions,
|
# All three share the same hermes-home volume so config, sessions,
|
||||||
# skills, and memory are consistent across all surfaces.
|
# skills, and memory are consistent across all surfaces.
|
||||||
#
|
#
|
||||||
|
# WHEN NOT TO USE THIS:
|
||||||
|
# - You hit "Permission denied" trying to share an existing ~/.hermes directory
|
||||||
|
# → use docker-compose.yml (single-container) instead, OR
|
||||||
|
# → keep this file but switch to NAMED VOLUMES (the default) instead of bind mounts
|
||||||
|
# - You're on Podman 3.4 or older without keep-id namespace support
|
||||||
|
# → see https://github.com/sunnysktsang/hermes-suite for an all-in-one image
|
||||||
|
#
|
||||||
|
# KNOWN LIMITATION (#681): tools triggered from the WebUI run in the WebUI
|
||||||
|
# container, not the agent container. See docker-compose.two-container.yml
|
||||||
|
# for context.
|
||||||
|
#
|
||||||
# NOTE ON VOLUMES:
|
# NOTE ON VOLUMES:
|
||||||
# This file uses named Docker volumes (hermes-home, hermes-agent-src) which
|
# This file uses named Docker volumes (hermes-home, hermes-agent-src) which
|
||||||
# work out of the box. If you prefer bind mounts (e.g. to an existing directory),
|
# work out of the box. If you prefer bind mounts (e.g. to an existing
|
||||||
# see the two-container compose file for a bind-mount example.
|
# directory), see docker-compose.two-container.yml for a bind-mount example.
|
||||||
# When using bind mounts, ALL containers must mount the same host path.
|
# When using bind mounts, ALL THREE containers must mount the same host path
|
||||||
|
# AND run as the same UID/GID (set via the UID/GID env vars below).
|
||||||
|
|
||||||
services:
|
services:
|
||||||
hermes-agent:
|
hermes-agent:
|
||||||
@@ -38,6 +51,15 @@ services:
|
|||||||
# Defaults to 1000 to match WANTED_UID/WANTED_GID in the webui service.
|
# Defaults to 1000 to match WANTED_UID/WANTED_GID in the webui service.
|
||||||
- HERMES_UID=${UID:-1000}
|
- HERMES_UID=${UID:-1000}
|
||||||
- HERMES_GID=${GID:-1000}
|
- HERMES_GID=${GID:-1000}
|
||||||
|
# Bind-mount permission handling for the agent — narrow set of overrides.
|
||||||
|
# NOTE: The agent's HERMES_HOME_MODE applies to the HERMES_HOME *directory*
|
||||||
|
# mode (default 0700) — NOT to credential files like the WebUI's variant.
|
||||||
|
# If you set this, you MUST keep the owner-execute bit so the agent can
|
||||||
|
# traverse its own home directory. 0640 BREAKS the agent (no x bit → no
|
||||||
|
# traversal). Use 0750 for group-traversable or 0701 for x-only.
|
||||||
|
# The agent's container detection (/.dockerenv) already auto-skips
|
||||||
|
# credential chmod inside Docker, so HERMES_SKIP_CHMOD is redundant here.
|
||||||
|
# - HERMES_HOME_MODE=0750
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
@@ -112,6 +134,13 @@ services:
|
|||||||
# to match the agent container's UID, or use a named Docker volume (preferred).
|
# to match the agent container's UID, or use a named Docker volume (preferred).
|
||||||
# Optional: set a password for remote access
|
# Optional: set a password for remote access
|
||||||
# - HERMES_WEBUI_PASSWORD=your-secret-password
|
# - HERMES_WEBUI_PASSWORD=your-secret-password
|
||||||
|
# Bind-mount permission handling for the WebUI (fixes #1389, #1399).
|
||||||
|
# NOTE: WebUI's HERMES_HOME_MODE is a credential-file threshold (allow
|
||||||
|
# group bits on .env/.signing_key/etc.), DIFFERENT from the agent's
|
||||||
|
# which applies to the HERMES_HOME directory itself. 0640 is correct
|
||||||
|
# for the WebUI; do NOT copy this value to the agent service block.
|
||||||
|
# - HERMES_SKIP_CHMOD=1
|
||||||
|
# - HERMES_HOME_MODE=0640
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- hermes-net
|
- hermes-net
|
||||||
|
|||||||
@@ -1,32 +1,49 @@
|
|||||||
# Two-container Docker Compose: Hermes Agent + Hermes WebUI
|
# Two-container Docker Compose: Hermes Agent + Hermes WebUI
|
||||||
#
|
#
|
||||||
# This runs the agent and web UI in separate containers connected via
|
# QUICK START:
|
||||||
# shared volumes. The WebUI installs the agent's Python dependencies
|
|
||||||
# at startup from the shared agent source volume.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# docker compose -f docker-compose.two-container.yml up -d
|
# docker compose -f docker-compose.two-container.yml up -d
|
||||||
|
# Open http://localhost:8787
|
||||||
#
|
#
|
||||||
# The agent container runs the gateway (CLI, Telegram, cron, etc.).
|
# This runs the agent and web UI in separate containers connected via shared
|
||||||
# The WebUI container serves the browser interface on port 8787.
|
# Docker volumes. The WebUI installs the agent's Python dependencies from the
|
||||||
# Both share ~/.hermes for config, sessions, and state.
|
# shared agent source volume at startup.
|
||||||
|
#
|
||||||
|
# WHEN TO USE THIS:
|
||||||
|
# - You want isolation between the agent gateway and the WebUI
|
||||||
|
# - You're already running hermes-agent in its own container
|
||||||
|
# - You don't need the dashboard (use docker-compose.three-container.yml for that)
|
||||||
|
#
|
||||||
|
# WHEN NOT TO USE THIS:
|
||||||
|
# - You hit "Permission denied" trying to share an existing ~/.hermes directory
|
||||||
|
# → use docker-compose.yml (single-container) instead, OR
|
||||||
|
# → keep this file but switch to NAMED VOLUMES (the default) instead of bind mounts
|
||||||
|
# - You're on Podman 3.4 or older without keep-id namespace support
|
||||||
|
# → see https://github.com/sunnysktsang/hermes-suite for an all-in-one image
|
||||||
|
#
|
||||||
|
# KNOWN LIMITATION (#681): tools triggered from the WebUI run in the WebUI
|
||||||
|
# container, not the agent container. If you need git/node/etc. on the
|
||||||
|
# WebUI's filesystem, install them in the WebUI image — or use a single-
|
||||||
|
# container setup where everything lives in one place.
|
||||||
#
|
#
|
||||||
# NOTE ON VOLUMES:
|
# NOTE ON VOLUMES:
|
||||||
# This file uses named Docker volumes (hermes-home, hermes-agent-src) which
|
# This file uses named Docker volumes (hermes-home, hermes-agent-src) which
|
||||||
# work out of the box. If you prefer bind mounts (e.g. to an existing directory),
|
# work out of the box on every Docker installation. If you prefer bind mounts
|
||||||
# replace the named volumes at the bottom. Example for hermes-agent-src:
|
# to share an existing host directory:
|
||||||
#
|
#
|
||||||
# hermes-agent-src:
|
# volumes:
|
||||||
# driver: local
|
# hermes-home:
|
||||||
# driver_opts:
|
# driver: local
|
||||||
# type: none
|
# driver_opts:
|
||||||
# o: bind
|
# type: none
|
||||||
# device: /opt/hermes-agent
|
# o: bind
|
||||||
|
# device: /home/youruser/.hermes
|
||||||
#
|
#
|
||||||
# When using bind mounts, BOTH containers must mount the same host path.
|
# When using bind mounts, BOTH containers must mount the same host path,
|
||||||
# The agent exposes source at /opt/hermes, the WebUI reads it from
|
# AND your host directory must be readable by UID 1000 (the default). Run:
|
||||||
# /home/hermeswebui/.hermes/hermes-agent — as long as both point to the
|
# id -u && id -g
|
||||||
# same host directory, the paths align correctly.
|
# to find your host UID/GID, then put them in a .env file:
|
||||||
|
# echo "UID=$(id -u)" >> .env
|
||||||
|
# echo "GID=$(id -g)" >> .env
|
||||||
|
|
||||||
services:
|
services:
|
||||||
hermes-agent:
|
hermes-agent:
|
||||||
@@ -47,8 +64,18 @@ services:
|
|||||||
- HERMES_HOME=/home/hermes/.hermes
|
- HERMES_HOME=/home/hermes/.hermes
|
||||||
# Align UID/GID across containers sharing the hermes-home volume.
|
# Align UID/GID across containers sharing the hermes-home volume.
|
||||||
# Defaults to 1000 to match WANTED_UID/WANTED_GID in the webui service.
|
# Defaults to 1000 to match WANTED_UID/WANTED_GID in the webui service.
|
||||||
|
# The agent image's entrypoint already supports usermod remapping.
|
||||||
- HERMES_UID=${UID:-1000}
|
- HERMES_UID=${UID:-1000}
|
||||||
- HERMES_GID=${GID:-1000}
|
- HERMES_GID=${GID:-1000}
|
||||||
|
# Bind-mount permission handling for the agent — narrow set of overrides.
|
||||||
|
# NOTE: The agent's HERMES_HOME_MODE applies to the HERMES_HOME *directory*
|
||||||
|
# mode (default 0700) — NOT to credential files like the WebUI's variant.
|
||||||
|
# If you set this, you MUST keep the owner-execute bit so the agent can
|
||||||
|
# traverse its own home directory. 0640 BREAKS the agent (no x bit → no
|
||||||
|
# traversal). Use 0750 for group-traversable or 0701 for x-only.
|
||||||
|
# The agent's container detection (/.dockerenv) already auto-skips
|
||||||
|
# credential chmod inside Docker, so HERMES_SKIP_CHMOD is redundant here.
|
||||||
|
# - HERMES_HOME_MODE=0750
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- hermes-net
|
- hermes-net
|
||||||
@@ -85,7 +112,14 @@ services:
|
|||||||
- WANTED_UID=${UID:-1000}
|
- WANTED_UID=${UID:-1000}
|
||||||
- WANTED_GID=${GID:-1000}
|
- WANTED_GID=${GID:-1000}
|
||||||
# Optional: set a password for remote access
|
# Optional: set a password for remote access
|
||||||
# - HERMES_WEBUI_PASSWORD=***
|
# - HERMES_WEBUI_PASSWORD=your-secret-password
|
||||||
|
# Bind-mount permission handling for the WebUI (fixes #1389, #1399).
|
||||||
|
# NOTE: WebUI's HERMES_HOME_MODE is a credential-file threshold (allow
|
||||||
|
# group bits on .env/.signing_key/etc.), DIFFERENT from the agent's
|
||||||
|
# which applies to the HERMES_HOME directory itself. 0640 is correct
|
||||||
|
# for the WebUI; do NOT copy this value to the agent service block.
|
||||||
|
# - HERMES_SKIP_CHMOD=1
|
||||||
|
# - HERMES_HOME_MODE=0640
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- hermes-net
|
- hermes-net
|
||||||
|
|||||||
@@ -1,4 +1,15 @@
|
|||||||
version: "3.8"
|
# Hermes WebUI — single-container Docker Compose
|
||||||
|
#
|
||||||
|
# QUICK START (most users):
|
||||||
|
# 1. (Optional) Copy .env.docker.example to .env and edit values
|
||||||
|
# 2. docker compose up -d
|
||||||
|
# 3. Open http://localhost:8787
|
||||||
|
#
|
||||||
|
# This is the simplest setup: one WebUI container that runs the agent in-process.
|
||||||
|
# The WebUI auto-detects host UID/GID from the mounted .hermes volume.
|
||||||
|
#
|
||||||
|
# For multi-container setups (separate agent + webui or agent+webui+dashboard),
|
||||||
|
# see docker-compose.two-container.yml or docker-compose.three-container.yml.
|
||||||
|
|
||||||
services:
|
services:
|
||||||
hermes-webui:
|
hermes-webui:
|
||||||
@@ -33,5 +44,14 @@ services:
|
|||||||
# - HERMES_WEBUI_DEFAULT_WORKSPACE=/workspace
|
# - HERMES_WEBUI_DEFAULT_WORKSPACE=/workspace
|
||||||
# Optional: set a password for remote access
|
# Optional: set a password for remote access
|
||||||
# - HERMES_WEBUI_PASSWORD=your-secret-password
|
# - HERMES_WEBUI_PASSWORD=your-secret-password
|
||||||
|
#
|
||||||
|
# Bind-mount permission handling (fixes #1389, #1399):
|
||||||
|
# When you mount an EXISTING ~/.hermes directory (the common case),
|
||||||
|
# the WebUI's startup credential-permission fixer can clash with
|
||||||
|
# your host file modes (e.g. 0640 group-readable .env files).
|
||||||
|
# Set HERMES_SKIP_CHMOD=1 to bypass the fixer entirely, OR set
|
||||||
|
# HERMES_HOME_MODE=0640 to allow group bits while still stripping
|
||||||
|
# world-readable. Both are documented in api/startup.py.
|
||||||
|
# - HERMES_SKIP_CHMOD=1
|
||||||
|
# - HERMES_HOME_MODE=0640
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
|||||||
202
docs/docker.md
Normal file
202
docs/docker.md
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
# Hermes WebUI — Docker setup guide
|
||||||
|
|
||||||
|
This is the comprehensive Docker reference. For a 5-minute quickstart, see the [README Docker section](../README.md#docker).
|
||||||
|
|
||||||
|
## TL;DR — pick one
|
||||||
|
|
||||||
|
| Setup | When to use | File |
|
||||||
|
|---|---|---|
|
||||||
|
| **Single-container** (recommended) | You just want chat working. WebUI runs the agent in-process. | `docker-compose.yml` |
|
||||||
|
| **Two-container** | You want isolation between gateway (CLI/Telegram/cron) and chat UI. | `docker-compose.two-container.yml` |
|
||||||
|
| **Three-container** | Two-container PLUS the dashboard for monitoring. | `docker-compose.three-container.yml` |
|
||||||
|
| **All-in-one image** (community fork — third-party, not maintained by us) | Podman 3.4 / multi-arch / supervisord-style preference. | [sunnysktsang/hermes-suite](https://github.com/sunnysktsang/hermes-suite) — see [#1399](https://github.com/nesquena/hermes-webui/issues/1399) for the original discussion |
|
||||||
|
|
||||||
|
If something stops working, **start with the single-container setup** — it's the simplest path and fixes most permission/UID/path-mismatch issues by construction.
|
||||||
|
|
||||||
|
## 5-minute quickstart (single container)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/nesquena/hermes-webui
|
||||||
|
cd hermes-webui
|
||||||
|
cp .env.docker.example .env
|
||||||
|
# Edit .env if needed (most users can skip this on Linux)
|
||||||
|
docker compose up -d
|
||||||
|
open http://localhost:8787
|
||||||
|
```
|
||||||
|
|
||||||
|
That's it. Your existing `~/.hermes` directory is mounted, your `~/workspace` is browsable, and the WebUI auto-detects your UID/GID from the mounted volume.
|
||||||
|
|
||||||
|
## What goes wrong (and how to fix it)
|
||||||
|
|
||||||
|
### 1. "Permission denied" at startup
|
||||||
|
|
||||||
|
**Symptom**: Container starts but immediately crashes, logs show:
|
||||||
|
```
|
||||||
|
PermissionError: [Errno 13] Permission denied: '/home/hermeswebui/.hermes/...'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause**: The container's user (UID 1000 by default) can't read your bind-mounted directory because your host files are owned by a different UID.
|
||||||
|
|
||||||
|
**Fix**: Set `UID` and `GID` in `.env` to match your host:
|
||||||
|
```bash
|
||||||
|
echo "UID=$(id -u)" >> .env
|
||||||
|
echo "GID=$(id -g)" >> .env
|
||||||
|
docker compose down && docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
On macOS, host UIDs start at 501. On Linux, the first interactive user is usually UID 1000.
|
||||||
|
|
||||||
|
> **macOS Docker Desktop**: if UID mapping still misbehaves after the env fix, try toggling **Settings → General → File sharing implementation** between VirtioFS and gRPC-FUSE. Different implementations preserve UIDs across the host/container boundary differently.
|
||||||
|
|
||||||
|
### 2. ".env file mode 0640 → permission denied" (#1389)
|
||||||
|
|
||||||
|
**Symptom**: You set `HERMES_HOME_MODE=0640` (or some other group-readable mode) on your host `.env` file, container starts, then errors out:
|
||||||
|
```
|
||||||
|
[security] fixed permissions on .env (0o640 -> 0600)
|
||||||
|
failed to load .env: open .env: permission denied
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause**: WebUI's `fix_credential_permissions()` startup hook enforces 0600 by default. This is the right thing for a clean install but conflicts with operator-set modes.
|
||||||
|
|
||||||
|
**Fix**: Set one of these env vars in your `.env`:
|
||||||
|
- `HERMES_SKIP_CHMOD=1` — bypass the fixer entirely
|
||||||
|
- `HERMES_HOME_MODE=0640` — allow group bits, only strip world-readable
|
||||||
|
|
||||||
|
Both are documented in `api/startup.py::fix_credential_permissions()`.
|
||||||
|
|
||||||
|
> ⚠️ **Multi-container warning**: `HERMES_HOME_MODE` has DIFFERENT semantics in the agent image vs. the WebUI:
|
||||||
|
> - **WebUI**: credential FILE mode threshold (`0640` allows group bits on `.env`)
|
||||||
|
> - **Agent**: `HERMES_HOME` *directory* mode (default `0700`)
|
||||||
|
>
|
||||||
|
> `0640` on a directory has no owner-execute bit, so the agent can't traverse its own home → bricked. For multi-container setups, use `HERMES_HOME_MODE=0750` (group-traversable) or `0701` (x-only). The compose files have per-service comments that match each side's semantics.
|
||||||
|
|
||||||
|
### 3. "Workspace appears empty even though my files are there"
|
||||||
|
|
||||||
|
**Symptom**: WebUI loads but `/workspace` shows no files.
|
||||||
|
|
||||||
|
**Cause**: Same as #1 — UID mismatch on the bind mount.
|
||||||
|
|
||||||
|
**Fix**: Same as #1 — match host UID/GID via `.env`.
|
||||||
|
|
||||||
|
### 4. "Two-container setup: WebUI can't find agent source" (#858)
|
||||||
|
|
||||||
|
**Symptom**: WebUI logs at startup:
|
||||||
|
```
|
||||||
|
!! WARNING: hermes-agent source not found.
|
||||||
|
!! Looked in: /home/hermeswebui/.hermes/hermes-agent
|
||||||
|
!! /opt/hermes
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause**: The agent's source (`/opt/hermes` inside the agent container) needs to be exposed to the WebUI container via a shared volume. The two-container compose file does this via `hermes-agent-src` named volume, but if you're using bind mounts incorrectly the path won't resolve.
|
||||||
|
|
||||||
|
**Fix**: Use the named volumes that ship with `docker-compose.two-container.yml` — don't replace them with bind mounts unless you know what you're doing. The agent container writes its source to `/opt/hermes`, and the WebUI mounts that volume at `/home/hermeswebui/.hermes/hermes-agent`.
|
||||||
|
|
||||||
|
If you must use a bind mount: pick a host path, then mount it to `/opt/hermes` in the agent container AND `/home/hermeswebui/.hermes/hermes-agent` in the WebUI container.
|
||||||
|
|
||||||
|
### 5. "Tools (git, node, etc.) missing in two-container setup" (#681)
|
||||||
|
|
||||||
|
**Symptom**: You ask the agent to run `git status` in chat and it errors with `command not found`.
|
||||||
|
|
||||||
|
**Cause**: This is **architectural, not a bug**. In the two-container setup, agent processes started by the WebUI run **inside the WebUI container**, not the agent container. The WebUI image doesn't include git/node by design (it's a UI image, not a tool host).
|
||||||
|
|
||||||
|
**Workarounds**:
|
||||||
|
- **Single-container setup** (`docker-compose.yml`) — everything in one container, no boundary
|
||||||
|
- **Custom WebUI image** — extend the `Dockerfile` to install the tools you need
|
||||||
|
- **Combined image** ([sunnysktsang/hermes-suite](https://github.com/sunnysktsang/hermes-suite)) — community fork that ships agent+webui+dashboard in one container
|
||||||
|
|
||||||
|
### 6. "config.yaml not loaded"
|
||||||
|
|
||||||
|
**Symptom**: You have a `config.yaml` in your host `~/.hermes/`, but the WebUI shows "no model configured" or doesn't pick up your custom providers.
|
||||||
|
|
||||||
|
**Cause**: Either the file isn't readable (UID/GID issue, see #1) or it's not in the expected path inside the container.
|
||||||
|
|
||||||
|
**Fix**:
|
||||||
|
- Verify: `docker exec hermes-webui ls -la /home/hermeswebui/.hermes/config.yaml`
|
||||||
|
- If it doesn't exist: your host bind mount is pointing at the wrong directory.
|
||||||
|
- If it exists but is unreadable: see #1 for the UID/GID fix.
|
||||||
|
|
||||||
|
### 7. "On Podman: can't share .hermes between containers"
|
||||||
|
|
||||||
|
**Symptom**: Two-container setup works on Docker but fails on Podman with permission errors no matter what UID/GID you set.
|
||||||
|
|
||||||
|
**Cause**: Podman 3.4 (Ubuntu 22.04 default) has limited support for `userns_mode: keep-id` across multiple containers — files written by one container appear with a different UID in the other.
|
||||||
|
|
||||||
|
**Fix**: Either upgrade to Podman 4+ (which fixes this), or use the [single-container setup](#5-minute-quickstart-single-container), or use the [community all-in-one image](https://github.com/sunnysktsang/hermes-suite).
|
||||||
|
|
||||||
|
## Multi-container architecture
|
||||||
|
|
||||||
|
The two- and three-container setups use **named Docker volumes** (not bind mounts) by default for a reason: named volumes solve the UID/GID problem by construction. Docker creates the volume's root directory with the correct ownership, all containers reading/writing to it see the same files, no host-side permission setup required.
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ hermes-home (volume) │
|
||||||
|
│ (config, sessions, state, ...) │
|
||||||
|
└─────────────────────────────────┘
|
||||||
|
↑ ↑
|
||||||
|
│ rw │ rw
|
||||||
|
│ │
|
||||||
|
┌──────────────┐ │ │ ┌──────────────┐
|
||||||
|
│ hermes-agent │────┘ └────│ hermes-webui │
|
||||||
|
│ (port 8642) │ │ (port 8787) │
|
||||||
|
└──────────────┘ └──────────────┘
|
||||||
|
│ ↑
|
||||||
|
│ rw │ ro
|
||||||
|
↓ │
|
||||||
|
┌─────────────────────────┐ │
|
||||||
|
│ hermes-agent-src (vol) │─────────────────────┘
|
||||||
|
│ (agent's Python source) │
|
||||||
|
└─────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
The WebUI container doesn't ship with the agent's Python deps — at startup it runs `uv pip install /home/hermeswebui/.hermes/hermes-agent` to install them from the shared volume.
|
||||||
|
|
||||||
|
## Bind-mount migration (advanced)
|
||||||
|
|
||||||
|
If you really need to bind-mount an existing host `~/.hermes` (e.g. you're keeping config in dotfiles, sharing with a non-Docker `hermes` install, etc.):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
hermes-home:
|
||||||
|
driver: local
|
||||||
|
driver_opts:
|
||||||
|
type: none
|
||||||
|
o: bind
|
||||||
|
device: /home/youruser/.hermes
|
||||||
|
hermes-agent-src:
|
||||||
|
driver: local
|
||||||
|
driver_opts:
|
||||||
|
type: none
|
||||||
|
o: bind
|
||||||
|
device: /opt/hermes-agent-source
|
||||||
|
```
|
||||||
|
|
||||||
|
**Critical requirements**:
|
||||||
|
|
||||||
|
1. The host directory MUST be readable by your container UID. Run `id -u` on the host and ensure `~/.hermes` is owned by that UID (or readable via group bits).
|
||||||
|
2. ALL containers sharing the volume must run as the SAME UID/GID. Set `UID=$(id -u)` and `GID=$(id -g)` in `.env`.
|
||||||
|
3. If your host `.env` is mode 0640, set `HERMES_SKIP_CHMOD=1` or `HERMES_HOME_MODE=0640` so the startup hook doesn't try to enforce 0600.
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
- [`docker-compose.yml`](../docker-compose.yml) — single container (recommended)
|
||||||
|
- [`docker-compose.two-container.yml`](../docker-compose.two-container.yml) — agent + webui
|
||||||
|
- [`docker-compose.three-container.yml`](../docker-compose.three-container.yml) — agent + dashboard + webui
|
||||||
|
- [`.env.docker.example`](../.env.docker.example) — environment variable template
|
||||||
|
- [`Dockerfile`](../Dockerfile) — single-container build
|
||||||
|
- [`docker_init.bash`](../docker_init.bash) — container entrypoint script
|
||||||
|
|
||||||
|
## Related issues
|
||||||
|
|
||||||
|
- #1389 — `HERMES_HOME_MODE` override (fixed in v0.50.254 — agent honors `HERMES_SKIP_CHMOD` and `HERMES_HOME_MODE`)
|
||||||
|
- #1399 — UID alignment in compose files (fixed in v0.50.260 via PR #1428 + this guide)
|
||||||
|
- #858 — two-container `/opt/hermes` path confusion
|
||||||
|
- #681 — tools running in WebUI container, not agent container (architectural)
|
||||||
|
- #668 — auto-detect UID/GID from mounted volume
|
||||||
|
- #569 — UID/GID detection priority order
|
||||||
|
|
||||||
|
If you hit a new failure mode not covered here, please [open an issue](https://github.com/nesquena/hermes-webui/issues/new) with:
|
||||||
|
|
||||||
|
1. Which compose file you used
|
||||||
|
2. The error from `docker logs hermes-webui`
|
||||||
|
3. `docker exec hermes-webui id` output
|
||||||
|
4. `docker exec hermes-webui ls -la /home/hermeswebui/.hermes` output
|
||||||
264
tests/test_v050260_docker_invariants.py
Normal file
264
tests/test_v050260_docker_invariants.py
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
"""Regression tests for v0.50.260 — Docker compose file invariants.
|
||||||
|
|
||||||
|
PR #1428 fixed a UID/GID mismatch between the agent container and the webui
|
||||||
|
container in the two- and three-container compose files. This test module
|
||||||
|
pins the invariants that prevented the original bug from coming back AND
|
||||||
|
extends coverage to the related fixes shipped alongside #1428:
|
||||||
|
|
||||||
|
- All compose files reference the same UID/GID source (`${UID}` / `${GID}`)
|
||||||
|
- All compose files document the bind-mount permission escape hatches
|
||||||
|
(`HERMES_SKIP_CHMOD`, `HERMES_HOME_MODE`) inline so users hit by #1389
|
||||||
|
or #1399 see the fix in the file they're reading
|
||||||
|
- The `.env.docker.example` template ships and documents the same vars
|
||||||
|
- `docs/docker.md` exists and covers the multi-container architecture
|
||||||
|
- Stale README references to `/root/.hermes` are gone (the agent images
|
||||||
|
use `/home/hermes/.hermes`)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
# ── 1: UID/GID alignment across compose files (PR #1428) ────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def test_two_container_compose_aligns_agent_uid_with_webui():
|
||||||
|
"""REGRESSION (#1399, fixed in #1428): the two-container compose file
|
||||||
|
must align the agent's UID/GID with the webui's. Before #1428 the
|
||||||
|
agent had no HERMES_UID/HERMES_GID at all and used the image default
|
||||||
|
of 10000, while the webui used 1000 — bind-mounted files written by
|
||||||
|
the agent were unreadable by the webui."""
|
||||||
|
src = (REPO / "docker-compose.two-container.yml").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
# Agent must declare HERMES_UID/HERMES_GID
|
||||||
|
assert "HERMES_UID=${UID:-1000}" in src, (
|
||||||
|
"two-container: hermes-agent must set HERMES_UID=${UID:-1000} so it "
|
||||||
|
"matches the webui's WANTED_UID=${UID:-1000}. Before #1428 the agent "
|
||||||
|
"ran as the image default (10000), causing PermissionError on the "
|
||||||
|
"shared hermes-home volume."
|
||||||
|
)
|
||||||
|
assert "HERMES_GID=${GID:-1000}" in src, (
|
||||||
|
"two-container: hermes-agent must set HERMES_GID=${GID:-1000}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# WebUI must use ${UID}/${GID} (same source)
|
||||||
|
assert "WANTED_UID=${UID:-1000}" in src
|
||||||
|
assert "WANTED_GID=${GID:-1000}" in src
|
||||||
|
|
||||||
|
|
||||||
|
def test_three_container_compose_aligns_all_three_services():
|
||||||
|
"""REGRESSION (#1399, fixed in #1428): all three services in the
|
||||||
|
three-container compose file must use ${UID}/${GID} as the source.
|
||||||
|
Before #1428 the agent and dashboard defaulted to 10000 while the
|
||||||
|
webui defaulted to 1000."""
|
||||||
|
src = (REPO / "docker-compose.three-container.yml").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
# Agent + dashboard both use HERMES_UID/HERMES_GID with ${UID:-1000} as source
|
||||||
|
# (Two occurrences each — once per service)
|
||||||
|
assert src.count("HERMES_UID=${UID:-1000}") >= 2, (
|
||||||
|
"three-container: both hermes-agent and hermes-dashboard must set "
|
||||||
|
"HERMES_UID=${UID:-1000}"
|
||||||
|
)
|
||||||
|
assert src.count("HERMES_GID=${GID:-1000}") >= 2
|
||||||
|
|
||||||
|
# WebUI uses WANTED_UID=${UID:-1000}
|
||||||
|
assert "WANTED_UID=${UID:-1000}" in src
|
||||||
|
assert "WANTED_GID=${GID:-1000}" in src
|
||||||
|
|
||||||
|
# The pre-#1428 default of 10000 must NOT appear anywhere
|
||||||
|
# (negative-pattern guard prevents revert)
|
||||||
|
assert "HERMES_UID:-10000" not in src, (
|
||||||
|
"Pre-#1428 default (HERMES_UID:-10000) must not return — that's the "
|
||||||
|
"bug shape. All UIDs should pull from ${UID:-1000}."
|
||||||
|
)
|
||||||
|
assert "HERMES_GID:-10000" not in src
|
||||||
|
|
||||||
|
|
||||||
|
def test_single_container_compose_uses_same_uid_source():
|
||||||
|
"""The single-container compose file should use the same ${UID} default
|
||||||
|
as the multi-container files for consistency."""
|
||||||
|
src = (REPO / "docker-compose.yml").read_text(encoding="utf-8")
|
||||||
|
assert "WANTED_UID=${UID:-1000}" in src
|
||||||
|
assert "WANTED_GID=${GID:-1000}" in src
|
||||||
|
|
||||||
|
|
||||||
|
# ── 2: bind-mount permission escape hatches documented (#1389, #1399) ──────
|
||||||
|
|
||||||
|
|
||||||
|
def test_compose_files_document_skip_chmod_escape_hatch():
|
||||||
|
"""Every compose file must mention HERMES_SKIP_CHMOD inline so users
|
||||||
|
hit by #1389 (the auth.json/.env chmod-override bug) can find the fix
|
||||||
|
in the file they're reading. The fix shipped in v0.50.254 but Docker
|
||||||
|
users may not be reading CHANGELOGs."""
|
||||||
|
for fname in ("docker-compose.yml", "docker-compose.two-container.yml", "docker-compose.three-container.yml"):
|
||||||
|
src = (REPO / fname).read_text(encoding="utf-8")
|
||||||
|
assert "HERMES_SKIP_CHMOD" in src, (
|
||||||
|
f"{fname}: must document HERMES_SKIP_CHMOD as a bind-mount "
|
||||||
|
f"escape hatch so users hit by #1389 find the fix inline."
|
||||||
|
)
|
||||||
|
assert "HERMES_HOME_MODE" in src, (
|
||||||
|
f"{fname}: must document HERMES_HOME_MODE alongside HERMES_SKIP_CHMOD"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── 3: .env.docker.example exists and documents the same vars ──────────────
|
||||||
|
|
||||||
|
|
||||||
|
def test_env_docker_example_exists():
|
||||||
|
"""The .env.docker.example template must ship in the repo so users
|
||||||
|
can `cp .env.docker.example .env` as the first step of the quickstart."""
|
||||||
|
p = REPO / ".env.docker.example"
|
||||||
|
assert p.exists(), ".env.docker.example must exist in repo root"
|
||||||
|
src = p.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
# Must document the critical vars
|
||||||
|
for var in ("UID", "GID", "HERMES_HOME", "HERMES_WORKSPACE",
|
||||||
|
"HERMES_WEBUI_PASSWORD", "HERMES_SKIP_CHMOD", "HERMES_HOME_MODE"):
|
||||||
|
assert var in src, (
|
||||||
|
f".env.docker.example must document {var} — without it, users "
|
||||||
|
f"hit by the related failure mode have no in-template hint."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── 4: docs/docker.md comprehensive guide ──────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def test_docs_docker_md_exists_and_covers_failure_modes():
|
||||||
|
"""The docs/docker.md guide must exist and cover the recurring failure
|
||||||
|
modes seen in #1399, #1389, #858, #681, #668."""
|
||||||
|
p = REPO / "docs" / "docker.md"
|
||||||
|
assert p.exists(), "docs/docker.md must exist as the comprehensive guide"
|
||||||
|
src = p.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
# Must mention each documented failure mode by issue ref
|
||||||
|
for issue in ("#1389", "#1399", "#858", "#681"):
|
||||||
|
assert issue in src, (
|
||||||
|
f"docs/docker.md must reference issue {issue} so users searching "
|
||||||
|
f"for the symptom find the right diagnostic path."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Must explicitly link the alternate single-container community image
|
||||||
|
assert "sunnysktsang/hermes-suite" in src, (
|
||||||
|
"docs/docker.md should point Podman 3.4 / multi-arch users to the "
|
||||||
|
"community all-in-one image as a documented escape hatch."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── 5: stale /root/.hermes references removed from README ──────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def test_readme_no_stale_root_hermes_path():
|
||||||
|
"""REGRESSION: the README's two-container Docker section used to claim
|
||||||
|
'the agent writes to /root/.hermes' which is wrong — current agent
|
||||||
|
images use /home/hermes/.hermes. Stale paths confuse users reading
|
||||||
|
the README to debug their own setup."""
|
||||||
|
src = (REPO / "README.md").read_text(encoding="utf-8")
|
||||||
|
assert "/root/.hermes" not in src, (
|
||||||
|
"README.md must not reference /root/.hermes — the current agent "
|
||||||
|
"image uses /home/hermes/.hermes. Stale paths in docs are worse "
|
||||||
|
"than no docs at all."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_readme_links_to_docker_md():
|
||||||
|
"""The README Docker section should point at docs/docker.md for the
|
||||||
|
deep dive so we don't have to keep two copies of the same content
|
||||||
|
in sync."""
|
||||||
|
src = (REPO / "README.md").read_text(encoding="utf-8")
|
||||||
|
assert "docs/docker.md" in src, (
|
||||||
|
"README.md should reference docs/docker.md so users with deeper "
|
||||||
|
"needs (multi-container, bind mounts, Podman) find the full guide."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── 6: compose files all parse as valid YAML ───────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def test_compose_files_parse_as_valid_yaml():
|
||||||
|
"""Every compose file must parse as valid YAML — without this guard,
|
||||||
|
a stray indentation or unquoted ${VAR} could ship a broken compose
|
||||||
|
file that breaks `docker compose up` for everyone."""
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
for fname in ("docker-compose.yml", "docker-compose.two-container.yml",
|
||||||
|
"docker-compose.three-container.yml"):
|
||||||
|
path = REPO / fname
|
||||||
|
try:
|
||||||
|
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
||||||
|
except yaml.YAMLError as e:
|
||||||
|
raise AssertionError(f"{fname} is not valid YAML: {e}")
|
||||||
|
assert isinstance(data, dict), f"{fname} must parse to a dict"
|
||||||
|
assert "services" in data, f"{fname} must define a `services:` block"
|
||||||
|
|
||||||
|
|
||||||
|
# ── 7: agent vs webui HERMES_HOME_MODE semantic asymmetry ──────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def test_agent_service_does_not_recommend_invalid_home_mode():
|
||||||
|
"""REGRESSION (Opus pre-release advisor): the WebUI's HERMES_HOME_MODE
|
||||||
|
is a credential-file threshold (0640 = allow group bits). The agent's
|
||||||
|
HERMES_HOME_MODE is a DIRECTORY mode (default 0700). 0640 on a directory
|
||||||
|
has no owner-execute bit, so the agent can't traverse its own home and
|
||||||
|
bricks. The agent service blocks must NOT recommend HERMES_HOME_MODE=0640
|
||||||
|
as their example value."""
|
||||||
|
import re
|
||||||
|
|
||||||
|
BAD_VALUES = (
|
||||||
|
"HERMES_HOME_MODE=0640",
|
||||||
|
"HERMES_HOME_MODE=0644",
|
||||||
|
"HERMES_HOME_MODE=0600",
|
||||||
|
"HERMES_HOME_MODE=0660",
|
||||||
|
)
|
||||||
|
|
||||||
|
for fname in ("docker-compose.two-container.yml", "docker-compose.three-container.yml"):
|
||||||
|
src = (REPO / fname).read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
# Find each agent/dashboard service block by name and slice to the next
|
||||||
|
# top-level service or root key.
|
||||||
|
for service_name in ("hermes-agent", "hermes-dashboard"):
|
||||||
|
service_marker = " " + service_name + ":"
|
||||||
|
idx = src.find(service_marker)
|
||||||
|
if idx == -1:
|
||||||
|
continue
|
||||||
|
# Find next service line (2-space-indented name + colon) or root key
|
||||||
|
after = idx + len(service_marker)
|
||||||
|
# Match next " name:" at indent 2 or root-level (no indent)
|
||||||
|
next_match = re.search(r"\n [a-z][a-z0-9-]*:\n|\n[a-z]", src[after:])
|
||||||
|
block_end = after + next_match.start() if next_match else len(src)
|
||||||
|
block = src[idx:block_end]
|
||||||
|
|
||||||
|
for bad in BAD_VALUES:
|
||||||
|
assert bad not in block, (
|
||||||
|
f"{fname} service `{service_name}` recommends `{bad}` — "
|
||||||
|
f"the agent's HERMES_HOME_MODE applies to the HERMES_HOME "
|
||||||
|
f"directory, and a mode without owner-execute prevents "
|
||||||
|
f"traversal. Use 0750 (group-traversable) or 0701 (x-only). "
|
||||||
|
f"See Opus pre-release advisor finding for v0.50.260."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_compose_files_warn_about_home_mode_asymmetry():
|
||||||
|
"""The compose files must explicitly warn about the WebUI vs agent
|
||||||
|
HERMES_HOME_MODE semantic asymmetry so users don't copy the WebUI's
|
||||||
|
valid 0640 value into the agent service."""
|
||||||
|
for fname in ("docker-compose.two-container.yml", "docker-compose.three-container.yml"):
|
||||||
|
src = (REPO / fname).read_text(encoding="utf-8").lower()
|
||||||
|
# Look for a comment that distinguishes directory mode from credential file mode
|
||||||
|
assert "directory" in src and "credential" in src, (
|
||||||
|
f"{fname} must contain comments explaining that HERMES_HOME_MODE "
|
||||||
|
f"means different things for the agent (directory mode) vs the "
|
||||||
|
f"WebUI (credential file threshold)."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_env_docker_example_warns_about_home_mode_asymmetry():
|
||||||
|
"""The .env.docker.example template must warn that HERMES_HOME_MODE has
|
||||||
|
different semantics across services."""
|
||||||
|
src = (REPO / ".env.docker.example").read_text(encoding="utf-8")
|
||||||
|
assert "MULTI-CONTAINER WARNING" in src, (
|
||||||
|
".env.docker.example must include a MULTI-CONTAINER WARNING about "
|
||||||
|
"the HERMES_HOME_MODE semantic asymmetry between WebUI and agent."
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user