--- name: hermes-webui-docker description: "Deploy and troubleshoot Hermes WebUI inside Docker containers — volume mounts, agent source resolution, compose wiring, and the 'AIAgent not available' failure path." version: 1.0.0 author: Hermes Agent platforms: [linux, macos] metadata: hermes: tags: [hermes, webui, docker, deployment, troubleshooting] related_skills: [hermes-agent, lxc-container-gpu-tools] --- # Hermes WebUI Docker Deployment Deploy and troubleshoot Hermes WebUI inside Docker containers. Covers volume mounts, agent source resolution, compose wiring, and the common "AIAgent not available" failure. ## Quick Start ```bash # Basic single-container (uses host ~/.hermes and ~/workspace) cd /path/to/hermes-webui docker compose up -d # Open http://localhost:8787 ``` ## Key Volumes | Host mount | Container mount | Purpose | |-----------|----------------|---------| | `~/.hermes` | `/home/hermeswebui/.hermes` | Config, sessions, state | | `~/workspace` | `/workspace` | Filesystem browser | | `~/.hermes/hermes-agent` | `/opt/hermes` | **Agent source code** (build + import dependency) | ## Why `/opt/hermes` is required The WebUI container installs Hermes Agent at startup via `uv pip install /opt/hermes` (`docker_init.bash` line 347). Without this mount, the container has no hermes-agent code on `PYTHONPATH`. At runtime the WebUI calls `from run_agent import AIAgent`. The startup script resolves the agent directory from `sys.path`; it copies the agent source from whichever path it finds into `/app/` and installs it into `/app/venv`, so `run_agent.py` is importable by the server Python. ## Common Failure: "AIAgent not available -- check that hermes-agent is on sys.path" ### Root causes 1. **Agent source not mounted into container** — the most common cause with a custom or adjusted `docker-compose.yml`. 2. **Wrong profile path as HERMES_HOME** — if your HERMES_HOME is `~/.hermes/profiles/telegram`, the agent source at `~/.hermes/hermes-agent` is outside that mount and invisible inside the container. 3. **Mount path mismatch** — the init script expects either `/home/hermeswebui/.hermes/hermes-agent` or `/opt/hermes`. ### Diagnostic commands ```bash # Is the container even importing the agent? docker exec hermes-webui /app/venv/bin/python -c "from run_agent import AIAgent; print('OK')" # Where did the init script find the agent source? docker logs hermes-webui | grep -iE "agent dir|hermes-agent" # Verify the mount exists in the container docker exec hermes-webui ls /opt/hermes/agent/__init__.py ``` ### Fix: docker-compose.yml Add the agent source volume mount and set the environment variable so the WebUI bootstrap can locate it: ```yaml services: hermes-webui: volumes: - ${HERMES_HOME:-${HOME}/.hermes}:/home/hermeswebui/.hermes - ${HERMES_WORKSPACE:-${HOME}/workspace}:/workspace - ${HERMES_AGENT_SRC:-${HOME}/.hermes/hermes-agent}:/opt/hermes # <-- REQUIRED environment: - HERMES_WEBUI_AGENT_DIR=/opt/hermes # <-- REQUIRED ``` Then recreate: ```bash cd /path/to/hermes-webui docker compose down docker compose up -d --force-recreate ``` ### Troubleshooting: WebUI starts but agent dir is "NOT FOUND" Check startup log lines after "== Running hermes-webui". You want: ``` agent dir : /opt/hermes [ok] ``` If you see: ``` agent dir : NOT FOUND [XX] ``` Either the volume didn't mount, or `HERMES_WEBUI_AGENT_DIR` isn't pointing at it. Possible order-of-operations pitfall: if you change `docker-compose.yml` environment lines and do `docker restart` (not recreate), Docker may cache the old env. Always use `--force-recreate` after compose changes. ### Pitfall: `providers: {}` Docker artifact in profile configs Docker WebUI containers sometimes write `providers: {}` into profile config.yaml files. This is the write-only bug — the key is empty and useless. The correct provider config lives in `custom_providers:` list and `model.provider`. During cleanup after a mount fix, delete this key from all affected configs. ### Pitfall: Stale sticky default after mount fix When the WebUI container's profiles were copied to the local filesystem during a mount fix, `~/.hermes/active_profile` may point to a Docker-origin profile (e.g., `telegram`). Running plain `hermes` then targets that profile instead of the intended CLI profile. Fix: `hermes profile use general` or `hermes profile use default`. ### Full cleanup after Docker WebUI decommission See `references/webui-removal-checklist.md` for the complete 8-section teardown checklist covering: inventory, pre-removal backups, container stop, Docker artifact removal, host source tree deletion, WebUI state cleanup inside bind mounts (global + per-profile `webui/` dirs, `state-snapshots/`, `providers: {}` config artifacts), explicit "DO NOT REMOVE" section for shared CLI paths, post-removal verification, and a copy-paste block for steps 2–5. Quick summary of the most commonly missed items: 1. Dangling `` image layers from rebuilds (`docker image prune -f`) 2. Per-profile `webui/` state dirs (not just the global `~/.hermes/webui/`) 3. `providers: {}` empty-key lines in profile configs (Docker write-only artifact) 4. `state-snapshots/` dirs at both `~/.hermes/` and `~/.hermes/profiles//` 5. The `.env` file in the source tree contains `HERMES_WEBUI_PASSWORD` in plaintext — delete with the source tree, don't echo it - `/opt/hermes` must be writable during init — the init script copies source into `/app`, installs into `/app/venv`, and `chown`s to the container user. Do **not** use `:ro` for this mount unless the build already happens at image-build time. - The `WANTED_UID`/`WANTED_GID` numeric values must match the filesystem owner of your bind mounts. When using profiles (e.g. `HERMES_HOME=~/.hermes/profiles/telegram`), the UID/GID of the host user must match what the container remaps to. ## Common Failure: Stale model dropdown (wrong models, old names, missing aliases) The WebUI model selector reads `models_cache.json` from the profile's webui directory. When this cache is stale, the dropdown shows wrong models, raw technical IDs instead of friendly alias names, or models from a previous configuration. ### Root causes 1. **Fingerprint points to container path** — the cache's `_source_fingerprint.config_yaml.path` is `/home/hermeswebui/.hermes/config.yaml` (Docker-internal) instead of the actual host config path. The WebUI can't detect config changes and never regenerates. 2. **Raw model IDs as labels** — the cache shows `kimi-k2.6:cloud` instead of the alias `kimi`. The WebUI doesn't auto-resolve `model_aliases` from config.yaml. 3. **Missing badges** — no "Primary" or "Recommended" indicators on model groups, so users can't tell which model is the default. 4. **`custom_providers` with `discover_models: true` not probed for live models** — `get_available_models()` in `/app/api/config.py` only probes `model.base_url` (the active provider) for live `/v1/models`. It never probes each `custom_provider`'s `base_url`, even when `discover_models: true` is set. The Ollama group shows only the static models from config.yaml, missing any models added since the config was last edited. See `references/custom-provider-live-discovery.md` for the code fix. ### Fix: rebuild models_cache.json Delete the stale cache and rebuild it from the actual config: ```bash # Remove stale cache rm -f ~/.hermes/profiles//webui/models_cache.json # Rebuild from config.yaml (see hermes-agent skill reference: config-model-change-cleanup.md) # Key steps: # 1. Read config.yaml → custom_providers (group by name), model_aliases (map model→alias) # 2. Build groups with alias names as labels, raw model IDs as ids # 3. Set active_provider/default_model from model.default/model.provider # 4. Add badges: "primary" for default, "recommended" for smart_model_routing.cheap_model # 5. Set fingerprint to the actual host config path (not /home/hermeswebui/) # Restart WebUI to pick up the new cache docker restart hermes-webui ``` ### Verification ```bash # Check the cache inside the container docker exec hermes-webui cat /home/hermeswebui/.hermes/profiles//webui/models_cache.json | python3 -m json.tool | head -30 # Verify fingerprint path is the host path, not /home/hermeswebui/ # Verify groups show alias names (kimi, deep, glm) not raw IDs # Verify badges include "primary" and "recommended" entries ``` ### Pitfall: host config edits vs WebUI dropdown cache The WebUI bind-mounts `~/.hermes` from the host, so config.yaml changes are live **for the CLI picker and any code reading the config directly inside the container** — `docker exec hermes-webui grep discover_models /home/hermeswebui/.hermes/config.yaml` will reflect the host edit instantly. No container restart needed for `discover_models` flips or new `custom_providers` entries. **However**, the WebUI's chat-page model dropdown reads a *separate* cache file: `~/.hermes/profiles//webui/models_cache.json` (also mounted into the container). That file is generated once and fingerprinted; the WebUI only regenerates it when the host config path's mtime changes via the cache's own invalidation logic. If the dropdown still shows the old static list after you bulk-edited configs: 1. Delete the cache: `rm -f ~/.hermes/profiles//webui/models_cache.json` (and equivalents in any other profile the WebUI uses) 2. Hit the WebUI's model-list endpoint (or just open the dropdown) to trigger a rebuild 3. Verify the new cache shows live-discovered models: `docker exec hermes-webui cat /home/hermeswebui/.hermes/profiles//webui/models_cache.json | python3 -c 'import sys,json; d=json.load(sys.stdin); print([m["id"] for g in d.get("groups",[]) for m in g.get("models",[])][:20])'` If `discover_models: true` is set and the dropdown still shows the old static 6, the cache is stale — not the config. ## Hindsight Memory in WebUI (canonical) The canonical memory provider for all 20 Hermes profiles is **Hindsight** (since 2026-06-29). It runs against a local daemon (Qdrant for storage at 10.0.0.22:6333 + vLLM for LLM at 10.0.0.26:8000) and requires no per-container install — the host's `~/.hermes/hindsight/config.json` is read directly. ### Requirements - `~/.hermes/hindsight/config.json` exists (single file shared by all profiles) - `memory.provider: hindsight` in the profile's `config.yaml` - vLLM reachable from host and any WebUI containers: `curl http://10.0.0.26:8000/v1/models` - Qdrant reachable: `curl http://10.0.0.22:6333/collections/memories` should report ~24K points ### Common Failure: Hindsight tools not appearing **Root cause**: The Hindsight daemon isn't running, OR `~/.hermes/hindsight/config.json` is missing/malformed, OR vLLM at 10.0.0.26:8000 is unreachable. Check the daemon log and verify all three endpoints from the host and from the WebUI container (`docker exec hermes-webui curl ...`). **Fix**: See `references/hindsight-setup-verification.md` (when it exists) for the full troubleshooting playbook. For now, verify: 1. `cat ~/.hermes/hindsight/config.json` returns valid JSON with `bank_id: hermes` 2. `curl http://10.0.0.26:8000/v1/models` lists at least one model 3. `curl http://10.0.0.22:6333/collections/memories` returns points_count > 20000 Then `/reset` or start a new session — memory provider loads at session start. ### Profile replication for Hindsight Hindsight config is a single file (`~/.hermes/hindsight/config.json`) shared by all profiles. There is **no per-profile replication needed** — each profile just needs `memory.provider: hindsight` in its `config.yaml`. See `devops/hermes-config-bulk-update/references/memory-provider-switch.md` for the bulk-migration playbook. --- ## Mem0 OSS Memory in WebUI — LEGACY (deprecated 2026-06-29) The `mem0_oss` plugin was the canonical memory provider for all 20 profiles until 2026-06-29. It has been replaced by Hindsight. This section is preserved for historical reference and rollback only — **do not use these instructions for new setups**. The original content covered: - `mem0.json` config file at `/mem0.json` - `mem0_oss` plugin directory at `/plugins/mem0_oss/` - `memory.provider: mem0_oss` in config.yaml - `pip install mem0ai ollama` in BOTH host and `/app/venv/` - Profile replication commands (`cp mem0.json`, `cp -r plugins/mem0_oss/`) - Common failure: `mem0ai`/`ollama` missing in WebUI container's venv - End-to-end verification script (`docker exec hermes-webui /app/venv/bin/python3 -c "..."`) The full original section was replaced with the Hindsight version above. Original references still in tree: - `references/mem0-webui-verification.md` — full original verification script (preserved as legacy reference) - `references/mem0-cross-profile-replication.md` — original June 2026 replication procedure (preserved as legacy reference) ## Per-Profile Workspace Binding The WebUI's file browser resolves each profile to its own workspace directory via the `workspace:` key in config.yaml. Without this key, every profile falls back to the global `/workspace` mount — all profiles share the same file browser view. ### How it works `api/workspace.py::_profile_default_workspace()` checks config.yaml keys in priority order: 1. `workspace` — explicit WebUI workspace key 2. `default_workspace` — alternate explicit key 3. `terminal.cwd` — agent working directory (most common fallback) The WebUI container mounts `~/workspace` at `/workspace` via docker-compose, so config values must use `/workspace/` (absolute container path), not `~/workspace/` (host-relative). ### Setup: bind all profiles to their workspaces ```bash # 1. Create workspace directories on host for p in default ai automation coding comfy dgx experimental general llm minimal people personal research telegram tts work; do mkdir -p ~/workspace/$p done # 2. Add workspace key to every profile config (use execute_code for bulk) # Pattern: insert `workspace: /workspace/` before `terminal:` section # in base config.yaml AND every ~/.hermes/profiles//config.yaml # 3. Create AGENTS.md in each workspace (see workspace-context-organization skill) ``` ### Verification ```bash # Test per-profile resolution inside the container docker exec hermes-webui /app/venv/bin/python3 -c " from api.profiles import set_request_profile, clear_request_profile from api.workspace import _profile_default_workspace for p in ['general', 'telegram', 'ai', 'comfy']: set_request_profile(p) ws = _profile_default_workspace() print(f'{p}: {ws}') clear_request_profile() " # Expected: general: /workspace/general, telegram: /workspace/telegram, etc. ``` ### Pitfall: `~` expansion in container Config values like `workspace: ~/workspace/general` expand to `/home/hermeswebui/workspace/general` inside the container — NOT `/workspace/general`. Always use absolute `/workspace/` paths. ### Pitfall: missing workspace directories If a profile's workspace directory doesn't exist on the host, the WebUI file browser shows an empty or error state. Create the directory on the host before adding the config key. ## WebUI-Host Mismatch Audit When the WebUI shows wrong models, missing features, or stale state compared to the CLI, run this systematic audit. All checks read from the host filesystem (bind-mounted into the container). ### 1. `providers: {}` Docker Artifact ```bash grep -rn 'providers: {}' ~/.hermes/profiles/*/config.yaml ~/.hermes/config.yaml ``` The WebUI container writes this empty key into config files. It's harmless clutter but indicates the container touched those files. Delete it during cleanup. The correct provider config lives in `custom_providers:` list and `model.provider`. ### 2. Missing `models_cache.json` ```bash find ~/.hermes/profiles -name 'models_cache.json' ``` If no cache files exist, the WebUI model dropdown may be empty or using a fallback. The cache is generated on first dropdown open. If the dropdown shows nothing, trigger a rebuild by opening the model selector in the WebUI, or delete any stale cache to force regeneration. ### 3. Stale Model Dropdown (Wrong Models) ```bash # Check cache fingerprint — must point to host path, not /home/hermeswebui/ docker exec hermes-webui cat /home/hermeswebui/.hermes/profiles//webui/models_cache.json | python3 -m json.tool | grep -A5 fingerprint ``` If the fingerprint path is `/home/hermeswebui/.hermes/config.yaml` (container-internal), the WebUI can't detect host config changes and never regenerates. Delete the cache to force rebuild. ### 4. Profile Mount Leak ```bash find ~/.hermes/profiles -name 'profiles' -type d ``` Nested `profiles//profiles/` directories indicate the WebUI container created profile trees inside the bind mount. These persist on the host after decommission. Remove them. ### 5. Sticky Default Profile ```bash cat ~/.hermes/active_profile ``` If this points to a Docker-origin profile (e.g., `telegram`), running plain `hermes` targets the wrong profile. Fix: `hermes profile use general`. ### 6. Hindsight Config (single-file, all profiles) ```bash # Verify ~/.hermes/hindsight/config.json exists test -f ~/.hermes/hindsight/config.json && echo "OK" || echo "MISSING" # Verify every profile has memory.provider: hindsight for p in ~/.hermes/profiles/*/config.yaml; do grep -q "memory.provider: hindsight" "$p" || echo "NOT HINDSIGHT: $p" done ``` Hindsight config is a single file (`~/.hermes/hindsight/config.json`) shared by all profiles — no per-profile replication needed. Each profile only needs `memory.provider: hindsight` in its `config.yaml`. ### 7. Per-Profile Workspace Binding ```bash for p in default ai automation coding comfy dgx experimental general llm minimal people personal research telegram tts work; do cfg="$HOME/.hermes/profiles/$p/config.yaml" test "$p" = "default" && cfg="$HOME/.hermes/config.yaml" grep "^workspace:" "$cfg" 2>/dev/null || echo "MISSING: $p" done ``` Without a `workspace:` key, all profiles share the global `/workspace` mount in the WebUI file browser. ### Quick Audit Script ```bash echo "=== 1. providers:{} ===" && grep -rn 'providers: {}' ~/.hermes/profiles/*/config.yaml ~/.hermes/config.yaml | wc -l echo "=== 2. models_cache.json ===" && find ~/.hermes/profiles -name 'models_cache.json' | wc -l echo "=== 3. Stale fingerprint ===" && docker exec hermes-webui cat /home/hermeswebui/.hermes/profiles/general/webui/models_cache.json 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('_source_fingerprint',{}).get('config_yaml',{}).get('path','N/A'))" 2>/dev/null || echo "(no cache)" echo "=== 4. Mount leak ===" && find ~/.hermes/profiles -name 'profiles' -type d | wc -l echo "=== 5. Sticky default ===" && cat ~/.hermes/active_profile 2>/dev/null || echo "(default)" echo "=== 6. Hindsight config ===" && test -f ~/.hermes/hindsight/config.json && echo "OK" || echo "MISSING" echo "=== 7. Workspace keys ===" && for p in default general telegram; do cfg="$HOME/.hermes/profiles/$p/config.yaml"; test "$p" = "default" && cfg="$HOME/.hermes/config.yaml"; echo -n "$p: "; grep "^workspace:" "$cfg" 2>/dev/null || echo "MISSING"; done ``` ## Docker Profile Mount Leak When the WebUI container creates profile directories inside the bind-mounted `HERMES_HOME`, those directories persist on the host after the container is decommissioned or the mount is fixed. This leaves nested `profiles//profiles/` trees, stale `state-snapshots/`, and container-internal fingerprint paths in caches. See `references/profile-mount-leak-migration.md` for identification and cleanup steps.