From 5671059f62ab28fa118b15fa148d5ae9a4200574 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 4 May 2026 15:37:27 +1000 Subject: [PATCH] =?UTF-8?q?feat(docker):=20launch=20dashboard=20as=20side-?= =?UTF-8?q?process=20via=20HERMES=5FDASHBOARD=3D1=20Adds=20an=20optional?= =?UTF-8?q?=20dashboard=20side-process=20to=20the=20container=20entrypoint?= =?UTF-8?q?,=20toggled=20by=20`HERMES=5FDASHBOARD=3D1`=20(also=20accepts?= =?UTF-8?q?=20`true`=20/=20`yes`).=20=20When=20set,=20the=20entrypoint=20b?= =?UTF-8?q?ackgrounds=20`hermes=20dashboard`=20before=20`exec`-ing=20the?= =?UTF-8?q?=20main=20command=20so=20the=20user's=20chosen=20foreground=20p?= =?UTF-8?q?rocess=20(gateway,=20chat,=20`sleep=20infinity`,=20=E2=80=A6)?= =?UTF-8?q?=20remains=20PID-of-interest=20for=20the=20container=20runtime.?= =?UTF-8?q?=20=20=20docker=20run=20-d=20\=20=20=20=20=20-v=20~/.hermes:/op?= =?UTF-8?q?t/data=20\=20=20=20=20=20-p=208642:8642=20-p=209119:9119=20\=20?= =?UTF-8?q?=20=20=20=20-e=20HERMES=5FDASHBOARD=3D1=20\=20=20=20=20=20nousr?= =?UTF-8?q?esearch/hermes-agent=20gateway=20run=20Defaults=20chosen=20for?= =?UTF-8?q?=20the=20container=20case:=20=20-=20Host:=200.0.0.0=20(reachabl?= =?UTF-8?q?e=20through=20published=20port;=20can=20override=20to=20=20=20?= =?UTF-8?q?=20127.0.0.1=20via=20HERMES=5FDASHBOARD=5FHOST=20for=20sidecar/?= =?UTF-8?q?reverse-proxy=20setups)=20=20-=20Port:=209119=20(matches=20`her?= =?UTF-8?q?mes=20dashboard`)=20=20-=20Auto-adds=20`--insecure`=20when=20bi?= =?UTF-8?q?nding=20to=20non-localhost,=20matching=20the=20=20=20=20dashboa?= =?UTF-8?q?rd's=20own=20safety=20gate=20for=20exposing=20API=20keys=20=20-?= =?UTF-8?q?=20HERMES=5FDASHBOARD=5FTUI=20is=20read=20by=20`hermes=20dashbo?= =?UTF-8?q?ard`=20directly=20=E2=80=94=20no=20=20=20=20entrypoint=20plumbi?= =?UTF-8?q?ng=20needed=20Dashboard=20output=20is=20prefixed=20with=20`[das?= =?UTF-8?q?hboard]`=20via=20`stdbuf`+`sed=20-u`=20so=20it's=20easy=20to=20?= =?UTF-8?q?separate=20from=20gateway=20logs=20in=20`docker=20logs`.=20=20N?= =?UTF-8?q?o=20supervision:=20if=20the=20dashboard=20crashes=20it=20stays?= =?UTF-8?q?=20down=20until=20the=20container=20restarts=20(documented=20in?= =?UTF-8?q?=20the=20`:::note`=20panel).=20Other=20changes=20bundled=20in:?= =?UTF-8?q?=20=20-=20Deprecate=20GATEWAY=5FHEALTH=5FURL=20/=20GATEWAY=5FHE?= =?UTF-8?q?ALTH=5FTIMEOUT=20env=20vars=20in=20=20=20=20hermes=5Fcli/web=5F?= =?UTF-8?q?server.py=20with=20a=20DEPRECATED=20block=20comment=20and=20a?= =?UTF-8?q?=20=20=20=20`..=20deprecated::`=20note=20on=20=5Fprobe=5Fgatewa?= =?UTF-8?q?y=5Fhealth.=20=20The=20feature=20still=20=20=20=20works=20for?= =?UTF-8?q?=20this=20release;=20it'll=20be=20removed=20alongside=20the=20m?= =?UTF-8?q?ove=20to=20a=20=20=20=20first-class=20dashboard=20config=20key.?= =?UTF-8?q?=20=20-=20Rewrite=20the=20"Running=20the=20dashboard"=20doc=20s?= =?UTF-8?q?ection=20around=20the=20new=20=20=20=20single-container=20patte?= =?UTF-8?q?rn.=20=20Drops=20the=20previously-documented=20=20=20=20dashboa?= =?UTF-8?q?rd-as-its-own-container=20setup=20=E2=80=94=20that=20pattern=20?= =?UTF-8?q?relied=20on=20the=20=20=20=20deprecated=20env=20vars=20for=20cr?= =?UTF-8?q?oss-container=20gateway-liveness=20detection,=20=20=20=20and=20?= =?UTF-8?q?without=20them=20the=20dashboard=20would=20permanently=20report?= =?UTF-8?q?=20the=20gateway=20=20=20=20as=20"not=20running".=20=20-=20Coll?= =?UTF-8?q?apse=20the=20two-service=20Compose=20example=20(gateway=20+=20d?= =?UTF-8?q?ashboard=20=20=20=20container)=20into=20a=20single=20service=20?= =?UTF-8?q?with=20HERMES=5FDASHBOARD=3D1.=20=20Removes=20=20=20=20the=20no?= =?UTF-8?q?w-unnecessary=20bridge=20network=20and=20`depends=5Fon`.=20=20-?= =?UTF-8?q?=20Drop=20the=20":::warning"=20caveat=20about=20"Running=20a=20?= =?UTF-8?q?dashboard=20container=20=20=20=20alongside=20the=20gateway=20is?= =?UTF-8?q?=20safe"=20=E2=80=94=20that=20case=20no=20longer=20exists.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/entrypoint.sh | 35 +++++++++++++++ hermes_cli/web_server.py | 13 ++++++ website/docs/user-guide/docker.md | 71 +++++++++++-------------------- 3 files changed, 74 insertions(+), 45 deletions(-) diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 299aab97a..65386e53d 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -86,6 +86,41 @@ if [ -d "$INSTALL_DIR/skills" ]; then python3 "$INSTALL_DIR/tools/skills_sync.py" fi +# Optionally start `hermes dashboard` as a side-process. +# +# Toggled by HERMES_DASHBOARD=1 (also accepts "true"/"yes", case-insensitive). +# Host/port/TUI can be overridden via: +# HERMES_DASHBOARD_HOST (default 0.0.0.0 — exposed outside the container) +# HERMES_DASHBOARD_PORT (default 9119, matches `hermes dashboard` default) +# HERMES_DASHBOARD_TUI (already honored by `hermes dashboard` itself) +# +# The dashboard is a long-lived server. We background it *before* the final +# `exec hermes "$@"` so the user's chosen foreground command (chat, gateway, +# sleep infinity, …) remains PID-of-interest for the container runtime. When +# the container stops the whole process tree is torn down, so no explicit +# cleanup is needed. +case "${HERMES_DASHBOARD:-}" in + 1|true|TRUE|True|yes|YES|Yes) + dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}" + dash_port="${HERMES_DASHBOARD_PORT:-9119}" + dash_args=(--host "$dash_host" --port "$dash_port" --no-open) + # Binding to anything other than localhost requires --insecure — the + # dashboard refuses otherwise because it exposes API keys. Inside a + # container this is the expected deployment (host reaches it via + # published port), so opt in automatically. + if [ "$dash_host" != "127.0.0.1" ] && [ "$dash_host" != "localhost" ]; then + dash_args+=(--insecure) + fi + echo "Starting hermes dashboard on ${dash_host}:${dash_port} (background)" + # Prefix dashboard output so it's distinguishable from the main + # process in `docker logs`. stdbuf keeps the pipe line-buffered. + ( + stdbuf -oL -eL hermes dashboard "${dash_args[@]}" 2>&1 \ + | sed -u 's/^/[dashboard] /' + ) & + ;; +esac + # Final exec: two supported invocation patterns. # # docker run -> exec `hermes` with no args (legacy default) diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index 014a938e0..97ebf9e29 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -470,10 +470,23 @@ except (ValueError, TypeError): ) _GATEWAY_HEALTH_TIMEOUT = 3.0 +# DEPRECATED (scheduled for removal): GATEWAY_HEALTH_URL / GATEWAY_HEALTH_TIMEOUT. +# Cross-container / cross-host gateway liveness detection will be folded into a +# first-class dashboard config key so it's no longer Docker-adjacent lore buried +# in env vars. The env vars still work for now so existing Compose deployments +# don't break. Do not add new callers — wire new uses through the planned +# config surface. + def _probe_gateway_health() -> tuple[bool, dict | None]: """Probe the gateway via its HTTP health endpoint (cross-container). + .. deprecated:: + Driven by the deprecated ``GATEWAY_HEALTH_URL`` / + ``GATEWAY_HEALTH_TIMEOUT`` env vars. Scheduled for removal alongside + a move to a first-class dashboard config key. See + :data:`_GATEWAY_HEALTH_URL` for context. + Uses ``/health/detailed`` first (returns full state), falling back to the simpler ``/health`` endpoint. Returns ``(is_alive, body_dict)``. diff --git a/website/docs/user-guide/docker.md b/website/docs/user-guide/docker.md index 21f8246ac..2a13fe666 100644 --- a/website/docs/user-guide/docker.md +++ b/website/docs/user-guide/docker.md @@ -45,28 +45,33 @@ Opening any port on an internet facing machine is a security risk. You should no ## Running the dashboard -The built-in web dashboard can run alongside the gateway as a separate container. - -To run the dashboard as its own container, point it at the gateway's health endpoint so it can detect gateway status across containers: +The built-in web dashboard runs as an optional side-process inside the same container as the gateway. Set `HERMES_DASHBOARD=1` and expose port `9119` alongside the gateway's `8642`: ```sh docker run -d \ - --name hermes-dashboard \ + --name hermes \ --restart unless-stopped \ -v ~/.hermes:/opt/data \ + -p 8642:8642 \ -p 9119:9119 \ - -e GATEWAY_HEALTH_URL=http://$HOST_IP:8642 \ - nousresearch/hermes-agent dashboard + -e HERMES_DASHBOARD=1 \ + nousresearch/hermes-agent gateway run ``` -Replace `$HOST_IP` with the IP address of the machine running the gateway container (e.g. `192.168.1.100`), or use a Docker network hostname if both containers share a network (see the [Compose example](#docker-compose-example) below). +The entrypoint starts `hermes dashboard` in the background (running as the non-root `hermes` user) before `exec`-ing the main command. Dashboard output is prefixed with `[dashboard]` in `docker logs` so it's easy to separate from gateway logs. | Environment variable | Description | Default | |---------------------|-------------|---------| -| `GATEWAY_HEALTH_URL` | Base URL of the gateway's API server, e.g. `http://gateway:8642` | *(unset — local PID check only)* | -| `GATEWAY_HEALTH_TIMEOUT` | Health probe timeout in seconds | `3` | +| `HERMES_DASHBOARD` | Set to `1` (or `true` / `yes`) to launch the dashboard alongside the main command | *(unset — dashboard not started)* | +| `HERMES_DASHBOARD_HOST` | Bind address for the dashboard HTTP server | `0.0.0.0` | +| `HERMES_DASHBOARD_PORT` | Port for the dashboard HTTP server | `9119` | +| `HERMES_DASHBOARD_TUI` | Set to `1` to expose the in-browser Chat tab (embedded `hermes --tui` via PTY/WebSocket) | *(unset)* | -Without `GATEWAY_HEALTH_URL`, the dashboard falls back to local process detection — which only works when the gateway runs in the same container or on the same host. +The default `HERMES_DASHBOARD_HOST=0.0.0.0` is required for the host to reach the dashboard through the published port; the entrypoint automatically passes `--insecure` to `hermes dashboard` in that case. Override to `127.0.0.1` if you want to restrict the dashboard to in-container access only (e.g. behind a reverse proxy in a sidecar). + +:::note +The dashboard side-process is **not supervised** — if it crashes, it stays down until the container restarts. Running it as a separate container is not supported: the dashboard's gateway-liveness detection requires a shared PID namespace with the gateway process. +::: ## Running interactively (CLI chat) @@ -102,7 +107,7 @@ The `/opt/data` volume is the single source of truth for all Hermes state. It ma | `skins/` | Custom CLI skins | :::warning -Never run two Hermes **gateway** containers against the same data directory simultaneously — session files and memory stores are not designed for concurrent write access. Running a dashboard container alongside the gateway is safe since the dashboard only reads data. +Never run two Hermes **gateway** containers against the same data directory simultaneously — session files and memory stores are not designed for concurrent write access. ::: ## Multi-profile support @@ -188,49 +193,24 @@ services: restart: unless-stopped command: gateway run ports: - - "8642:8642" + - "8642:8642" # gateway API + - "9119:9119" # dashboard (only reached when HERMES_DASHBOARD=1) volumes: - ~/.hermes:/opt/data - networks: - - hermes-net - # Uncomment to forward specific env vars instead of using .env file: - # environment: - # - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} - # - OPENAI_API_KEY=${OPENAI_API_KEY} - # - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN} + environment: + - HERMES_DASHBOARD=1 + # Uncomment to forward specific env vars instead of using .env file: + # - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} + # - OPENAI_API_KEY=${OPENAI_API_KEY} + # - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN} deploy: resources: limits: memory: 4G cpus: "2.0" - - dashboard: - image: nousresearch/hermes-agent:latest - container_name: hermes-dashboard - restart: unless-stopped - command: dashboard --host 0.0.0.0 --insecure - ports: - - "9119:9119" - volumes: - - ~/.hermes:/opt/data - environment: - - GATEWAY_HEALTH_URL=http://hermes:8642 - networks: - - hermes-net - depends_on: - - hermes - deploy: - resources: - limits: - memory: 512M - cpus: "0.5" - -networks: - hermes-net: - driver: bridge ``` -Start with `docker compose up -d` and view logs with `docker compose logs -f`. +Start with `docker compose up -d` and view logs with `docker compose logs -f`. Dashboard output is prefixed with `[dashboard]` so it's easy to filter from gateway logs. ## Resource limits @@ -273,6 +253,7 @@ The entrypoint script (`docker/entrypoint.sh`) bootstraps the data volume on fir - Copies default `config.yaml` if missing - Copies default `SOUL.md` if missing - Syncs bundled skills using a manifest-based approach (preserves user edits) +- Optionally launches `hermes dashboard` as a background side-process when `HERMES_DASHBOARD=1` (see [Running the dashboard](#running-the-dashboard)) - Then runs `hermes` with whatever arguments you pass ## Upgrading