17 KiB
Hermes WebUI — Docker setup guide
This is the comprehensive Docker reference. For a 5-minute quickstart, see the README Docker section.
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 — see #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.
Production image security model
The production Docker image is hardened for the normal single-tenant container threat model:
Hermes WebUI assumes one operator controls the container, mounted Hermes home, and workspace.
The image does not install sudo, does not add runtime users to a sudo group, and does not
grant NOPASSWD escalation. If an agent/tool process gains a shell as hermeswebui, it should
not be able to become root with a passwordless sudo command.
The entrypoint still starts as root for a narrow init phase because Docker bind mounts often need
UID/GID alignment and ownership preparation before the app can read ~/.hermes, /workspace,
/app, and /uv_cache. After that setup, docker_init.bash re-execs itself as the unprivileged
hermeswebui user and starts the server there. Init scratch files under /tmp/hermeswebui_init
are owner-only (0700 directory, 0600 files), not world-writable.
For multi-tenant or hostile-container environments, rebuild with your own runtime user, mount policy, and supervisor assumptions. Development images that need package-manager convenience should add those tools in a dev-only Dockerfile instead of reintroducing passwordless sudo to production.
5-minute quickstart (single container)
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 for a real personal Docker install. Your existing ~/.hermes
directory is mounted, your ~/workspace is browsable, and the WebUI
auto-detects your UID/GID from the mounted volume.
For troubleshooting, reinstall, or onboarding reproduction trials, do not mount
your real ~/.hermes unless you intentionally want to test real state. Use an
isolated Hermes home and follow
docs/onboarding-agent-checklist.md instead.
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:
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 entirelyHERMES_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_MODEhas DIFFERENT semantics in the agent image vs. the WebUI:
- WebUI: credential FILE mode threshold (
0640allows group bits on.env)- Agent:
HERMES_HOMEdirectory mode (default0700)
0640on a directory has no owner-execute bit, so the agent can't traverse its own home → bricked. For multi-container setups, useHERMES_HOME_MODE=0750(group-traversable) or0701(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
Dockerfileto install the tools you need - Combined image (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, or use the community all-in-one image.
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. The WebUI mount is read-only; the agent container is the only writer.
Upgrading the agent container
The hermes-agent-src named volume is initialised from the agent image's /opt/hermes on first up. Docker reuses the volume verbatim on every subsequent up — even after docker pull of a newer agent image. The cached volume content masks the new image's source tree, so a fresh docker pull of nousresearch/hermes-agent:latest does not by itself give you the new agent code, dependencies, or entrypoint.
This is the root cause of #1416: the symptom looked like a missing entrypoint, but the entrypoint was actually present in the new image and hidden behind the stale named volume.
To upgrade the agent image cleanly, drop the source volume before recreating:
# Two-container setup
docker compose -f docker-compose.two-container.yml down
docker volume rm <project>_hermes-agent-src
docker compose -f docker-compose.two-container.yml pull
docker compose -f docker-compose.two-container.yml up -d
# Three-container setup
docker compose -f docker-compose.three-container.yml down
docker volume rm <project>_hermes-agent-src
docker compose -f docker-compose.three-container.yml pull
docker compose -f docker-compose.three-container.yml up -d
Replace <project> with your Compose project name (the parent directory by default; check with docker volume ls). The hermes-home volume (config, sessions, state) is left untouched — only hermes-agent-src (the agent's installed Python source) is recreated.
The single-container setup (
docker-compose.yml) does not usehermes-agent-srcand is not affected by this upgrade pattern — pulling a newer WebUI image anddocker compose up -d --force-recreateis sufficient.
What the multi-container setup isolates (and what it doesn't)
The two- and three-container setups give you process, network, and resource isolation between the gateway and the chat UI:
- Each service has its own PID namespace and lifecycle — the agent process can crash without taking down the chat UI and vice versa.
- The gateway API (port 8642) is bound by the agent service only; the WebUI cannot bind it. Other containers reach the gateway via the
hermes-netDocker network. - Resource limits (
deploy.resources.limitsindocker-compose.three-container.yml) apply per service, so you can cap the agent independently of the dashboard. - Restart policies, log streams, and container health checks are scoped per service.
What multi-container does not isolate:
- Filesystem boundary. Both services share
hermes-home(config, sessions, state), and the WebUI mounts the agent's installed source fromhermes-agent-src. The WebUI mount is read-only (since v0.51.84), but the agent service still has write access, and both services share the home volume. - UID/GID boundary. Both services default to
${UID:-1000}so files written by one are readable by the other. If you align them to different UIDs you'll get permission errors on the shared volume. - Trust boundary on the agent source. The WebUI installs Python dependencies from the shared
hermes-agent-srcvolume at startup. The read-only mount means a compromised WebUI cannot rewrite the agent source, but it does run code from that volume.
If you need filesystem isolation between the chat UI and the agent (e.g. you don't trust the WebUI to read agent state), the multi-container setup is not enough — run the agent on a separate host and connect the WebUI to it via the gateway HTTP API. If you don't need any boundary, the single-container setup is simpler.
The direct source mount is a compatibility bridge, not the long-term API contract. The current source/API boundary inventory and decoupling task list live in docs/rfcs/agent-source-boundary.md for #2453. If you customize the compose files with bind mounts, keep the WebUI-side agent source mount read-only unless you are intentionally doing local development; docker_init.bash warns at startup when that path is writable.
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.):
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:
- The host directory MUST be readable by your container UID. Run
id -uon the host and ensure~/.hermesis owned by that UID (or readable via group bits). - ALL containers sharing the volume must run as the SAME UID/GID. Set
UID=$(id -u)andGID=$(id -g)in.env. - If your host
.envis mode 0640, setHERMES_SKIP_CHMOD=1orHERMES_HOME_MODE=0640so the startup hook doesn't try to enforce 0600.
Reference
docker-compose.yml— single container (recommended)docker-compose.two-container.yml— agent + webuidocker-compose.three-container.yml— agent + dashboard + webui.env.docker.example— environment variable templateDockerfile— single-container builddocker_init.bash— container entrypoint script
Related issues
- #1416 — agent-image upgrade requires removing
hermes-agent-srcnamed volume (see Upgrading the agent container) - #1389 —
HERMES_HOME_MODEoverride (fixed in v0.50.254 — agent honorsHERMES_SKIP_CHMODandHERMES_HOME_MODE) - #1399 — UID alignment in compose files (fixed in v0.50.260 via PR #1428 + this guide)
- #858 — two-container
/opt/hermespath 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 with:
- Which compose file you used
- The error from
docker logs hermes-webui docker exec hermes-webui idoutputdocker exec hermes-webui ls -la /home/hermeswebui/.hermesoutput