Release: multi-container Docker gateway API forwarding (#6917) (#6995)

* feat(docker): enable gateway API and password forwarding in compose files

- Added API_SERVER_KEY forwarding to enable the gateway API listener in multi-container setups.
- Configured HERMES_WEBUI_PASSWORD to ensure password authentication works as expected.
- Updated documentation to clarify API_SERVER_KEY requirements and forwarding behavior.
- Added regression tests to cover the new functionality and ensure proper configuration.

* build(docker): force LF line endings for shell scripts via .gitattributes

A CRLF shebang (#!/bin/bash\r) breaks locally-built Docker images: the
kernel can't resolve the interpreter /bin/bash\r, so 'docker compose up
-d --build' on Windows with core.autocrlf=true fails with
'exec /hermeswebui_init.bash: no such file or directory'. The repo blobs
and CI/GHCR builds are LF and unaffected; only Windows local builds
baked the CRLF checkout into the image.

* fixup(docker): dedup API_SERVER_HOST after rebase onto #6968 (which already added it)

* docs(changelog): stamp #6917 (multi-container gateway API forwarding)

---------

Co-authored-by: Yang <yang_qwq@qq.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-08-13 18:21:14 +00:00
1 parent 0b69346970
commit 07118df53c
8 files changed
+168 -14

No files matched your search

+15
View File
@@ -45,6 +45,9 @@ GID=1000
# 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.
#
# All three compose files forward this into the WebUI container, so
# uncommenting it here is sufficient — no compose edit needed:
# HERMES_WEBUI_PASSWORD=change-me-to-something-strong
# ──────────────────────────────────────────────────────────────────────────
@@ -118,3 +121,15 @@ GID=1000
# 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.
# ──────────────────────────────────────────────────────────────────────────
# Gateway API server — multi-container only
# ──────────────────────────────────────────────────────────────────────────
# The two/three-container compose files forward this into the hermes-agent
# container. The agent only starts the gateway API listener (port 8642) when
# a usable API_SERVER_KEY is present (>=16 chars) — API_SERVER_ENABLED alone
# does nothing. Without a key, the gateway daemon still runs (messaging +
# cron) but the WebUI's Tasks/System pill shows "Gateway endpoint not
# reachable". Set a long random string here to enable it; the same value is
# forwarded to the WebUI as HERMES_WEBUI_GATEWAY_API_KEY automatically:
# API_SERVER_KEY=replace-with-a-long-random-string
+10
View File
@@ -0,0 +1,10 @@
# Normalize line endings to LF in the repository.
* text=auto
# Shell scripts must stay LF on checkout too. A CRLF shebang (`#!/bin/bash\r`)
# breaks the Docker image: the kernel can't find interpreter `/bin/bash\r`, so
# local `docker compose up -d --build` on Windows machines with
# `core.autocrlf=true` fails with `exec /hermeswebui_init.bash: no such file
# or directory`.
*.sh text eol=lf
*.bash text eol=lf
+2
View File
@@ -5,6 +5,8 @@
### Fixed
- **Multi-container Docker: the WebUI can now reach the agent gateway API without hand-editing compose files.** The two- and three-container Compose topologies now forward the gateway API server settings end to end — `API_SERVER_ENABLED`/`API_SERVER_HOST`/`API_SERVER_KEY` into the agent container and the matching `HERMES_WEBUI_GATEWAY_API_KEY` into the WebUI container — so setting `API_SERVER_KEY` in `.env` is enough to enable Tasks/cron and gateway-backed features (the agent only binds port 8642 when the key is a usable value). Password vars are now `${HERMES_WEBUI_PASSWORD:-}` substitutions (set once in `.env`, no compose edit), and a new `.gitattributes` forces LF on shell scripts so a Windows `core.autocrlf` checkout no longer breaks the image build (`exec …init.bash: no such file or directory`). Thanks @Yang-qwq. (#6917)
- **The chat view no longer bounces vertically while a reply streams in.** As each word faded in during streaming, the browser replaced the finished word's DOM node with a fresh text node — and every replacement made the browser's native scroll anchoring re-pick an anchor while the transcript was still growing, producing a visible vertical bounce. The two fade-cleanup handlers now keep each faded-in word's node stable (they just clear the transient animation class and timing variable) instead of replacing it, so the scroll position stays put. The settled reply is still rebuilt as plain text exactly as before, and text selection during streaming is steadier as a bonus. Thanks @ruizanthony. (#6257)
- **The model selector no longer lists providers whose API key you've removed.** A provider that stayed in `config.yaml` with only metadata (e.g. `openai-api: {name: "OpenAI API"}`) kept appearing in the chat model dropdown after its credential was removed, so picking it failed. A config-listed provider now appears only when it was actually detected from a credential source (env var, hermes auth, or the credential pool), with provider aliases resolved on both sides (so `x-ai`/`xai`, `google`/`gemini` match correctly). Route-configured and active-model providers are unaffected. Thanks @webtecnica. (#6338)
+15 -6
View File
@@ -51,6 +51,15 @@ services:
# Defaults to 1000 to match WANTED_UID/WANTED_GID in the webui service.
- HERMES_UID=${UID:-1000}
- HERMES_GID=${GID:-1000}
# Gateway API server (port 8642). The WebUI's Tasks/cron health probe
# reaches this listener via HERMES_API_URL below. The agent only starts
# the listener when API_SERVER_KEY is a usable value (>=16 chars); set
# it in .env to enable the gateway API. Without a key the gateway daemon
# still runs (messaging + cron) but port 8642 stays unbound and the
# WebUI reports "Gateway endpoint not reachable".
- API_SERVER_ENABLED=true
- API_SERVER_HOST=0.0.0.0
- API_SERVER_KEY=${API_SERVER_KEY:-}
# 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.
@@ -60,10 +69,6 @@ services:
# The agent's container detection (/.dockerenv) already auto-skips
# credential chmod inside Docker, so HERMES_SKIP_CHMOD is redundant here.
# - HERMES_HOME_MODE=0750
# Bind to 0.0.0.0 so that other services can reach the agent's api.
# We map the service to 127.0.0.1 with docker, so it won't be reachable
# outside the host
- API_SERVER_HOST=0.0.0.0
restart: unless-stopped
deploy:
resources:
@@ -136,14 +141,18 @@ services:
# echo "UID=$(id -u)" >> .env && echo "GID=$(id -g)" >> .env
- WANTED_UID=${UID:-1000}
- WANTED_GID=${GID:-1000}
# Forward the API server key so the WebUI's gateway health probe can
# authenticate (must match the agent's API_SERVER_KEY above).
- HERMES_WEBUI_GATEWAY_API_KEY=${API_SERVER_KEY:-}
# Optional: enable password auth. Set HERMES_WEBUI_PASSWORD in .env
# (required if you expose the port beyond 127.0.0.1).
- HERMES_WEBUI_PASSWORD=${HERMES_WEBUI_PASSWORD:-}
# NOTE: When using bind-mount volumes shared across containers, ALL containers
# that write to the same host directory must run as the same UID/GID.
# If hermes-agent initialises the state dir as root (UID 0), hermes-webui
# will get a PermissionError accessing those paths — including a crash on every
# HTTP request if the auth signing-key file is unreadable. Either set WANTED_UID
# to match the agent container's UID, or use a named Docker volume (preferred).
# Optional: set a password for remote access
# - 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
+15 -6
View File
@@ -67,6 +67,15 @@ services:
# The agent image's entrypoint already supports usermod remapping.
- HERMES_UID=${UID:-1000}
- HERMES_GID=${GID:-1000}
# Gateway API server (port 8642). The WebUI's Tasks/cron health probe
# reaches this listener via HERMES_API_URL below. The agent only starts
# the listener when API_SERVER_KEY is a usable value (>=16 chars); set
# it in .env to enable the gateway API. Without a key the gateway daemon
# still runs (messaging + cron) but port 8642 stays unbound and the
# WebUI reports "Gateway endpoint not reachable".
- API_SERVER_ENABLED=true
- API_SERVER_HOST=0.0.0.0
- API_SERVER_KEY=${API_SERVER_KEY:-}
# 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.
@@ -76,10 +85,6 @@ services:
# The agent's container detection (/.dockerenv) already auto-skips
# credential chmod inside Docker, so HERMES_SKIP_CHMOD is redundant here.
# - HERMES_HOME_MODE=0750
# Bind to 0.0.0.0 so that other services can reach the agent's api.
# We map the service to 127.0.0.1 with docker, so it won't be reachable
# outside the host
- API_SERVER_HOST=0.0.0.0
restart: unless-stopped
networks:
- hermes-net
@@ -121,8 +126,12 @@ services:
# echo "UID=$(id -u)" >> .env && echo "GID=$(id -g)" >> .env
- WANTED_UID=${UID:-1000}
- WANTED_GID=${GID:-1000}
# Optional: set a password for remote access
# - HERMES_WEBUI_PASSWORD=your-secret-password
# Forward the API server key so the WebUI's gateway health probe can
# authenticate (must match the agent's API_SERVER_KEY above).
- HERMES_WEBUI_GATEWAY_API_KEY=${API_SERVER_KEY:-}
# Optional: enable password auth. Set HERMES_WEBUI_PASSWORD in .env
# (required if you expose the port beyond 127.0.0.1).
- HERMES_WEBUI_PASSWORD=${HERMES_WEBUI_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
+3 -2
View File
@@ -42,8 +42,9 @@ services:
- HERMES_WEBUI_STATE_DIR=/home/hermeswebui/.hermes/webui
# Default workspace directory shown on first launch
# - HERMES_WEBUI_DEFAULT_WORKSPACE=/workspace
# Optional: set a password for remote access
# - HERMES_WEBUI_PASSWORD=your-secret-password
# Optional: enable password auth. Set HERMES_WEBUI_PASSWORD in .env
# (required if you expose the port beyond 127.0.0.1).
- HERMES_WEBUI_PASSWORD=${HERMES_WEBUI_PASSWORD:-}
#
# Bind-mount permission handling (fixes #1389, #1399):
# When you mount an EXISTING ~/.hermes directory (the common case),
+16
View File
@@ -193,6 +193,22 @@ cp .env.docker.example .env
docker compose -f docker-compose.two-container.yml up -d
```
The compose files forward `API_SERVER_KEY` from `.env` into the `hermes-agent`
container. The agent only starts the gateway API listener (port 8642) when
`API_SERVER_KEY` is a usable value (>=16 chars) — `API_SERVER_ENABLED` alone
does nothing. Without a key, the gateway daemon still runs but port 8642 stays
unbound and the WebUI keeps showing **"Gateway endpoint not reachable"**. To
enable scheduled ticking and the green gateway pill, set a long random string
in `.env`:
```bash
echo "API_SERVER_KEY=$(openssl rand -hex 24)" >> .env
docker compose -f docker-compose.two-container.yml up -d --force-recreate
```
The compose file forwards the same value to the WebUI as
`HERMES_WEBUI_GATEWAY_API_KEY`, so the health probe authenticates automatically.
The three-container layout adds the dashboard but is otherwise the same shape. If you must stay single-container, you can run `hermes gateway` inside the container as a long-lived background process, but the compose split is sturdier.
If you maintain a custom compose file, make sure the **WebUI service** points at
@@ -0,0 +1,92 @@
"""Regression coverage for two Docker-compose gaps found in the field.
1. Gateway API not reachable out of the box. The two/three-container compose
files set ``HERMES_API_URL=http://hermes-agent:8642`` on the WebUI but
never configured the agent to listen on 8642. The agent image only starts
its API-server listener when ``API_SERVER_KEY`` is a usable value (>=16
chars); ``API_SERVER_ENABLED`` alone does nothing. The compose files now
forward ``API_SERVER_KEY`` from ``.env`` and bind the listener on
0.0.0.0, and the WebUI receives the matching
``HERMES_WEBUI_GATEWAY_API_KEY`` so its health probe authenticates.
2. ``HERMES_WEBUI_PASSWORD`` set in ``.env`` had no effect. Docker Compose
uses ``.env`` only for variable interpolation — a value is not injected
into a container unless the compose file references it. All three compose
files now forward ``HERMES_WEBUI_PASSWORD`` into the WebUI service.
"""
from __future__ import annotations
from pathlib import Path
REPO = Path(__file__).resolve().parents[1]
MULTI = ("docker-compose.two-container.yml", "docker-compose.three-container.yml")
ALL = ("docker-compose.yml",) + MULTI
# ── 1: gateway API server forwarding (multi-container only) ────────────────
def test_agent_service_enables_api_server():
"""The agent service must carry the API-server env block so a user who
sets API_SERVER_KEY in .env gets a listening 8642 without editing the
compose file."""
for fn in MULTI:
src = (REPO / fn).read_text(encoding="utf-8")
assert "- API_SERVER_ENABLED=true" in src, (
f"{fn}: agent must declare API_SERVER_ENABLED."
)
assert "- API_SERVER_HOST=0.0.0.0" in src, (
f"{fn}: API server must bind 0.0.0.0 — the default 127.0.0.1 is "
"unreachable from the WebUI container over the compose network."
)
assert "- API_SERVER_KEY=${API_SERVER_KEY:-}" in src, (
f"{fn}: agent must forward API_SERVER_KEY from .env so the gateway "
"API listener can start (the agent requires a usable key, >=16 chars)."
)
def test_webui_service_forwards_gateway_api_key():
"""The WebUI must receive the same API_SERVER_KEY so its /health/detailed
probe authenticates instead of 401ing into 'Gateway endpoint not
reachable'."""
for fn in MULTI:
src = (REPO / fn).read_text(encoding="utf-8")
assert "- HERMES_WEBUI_GATEWAY_API_KEY=${API_SERVER_KEY:-}" in src, (
f"{fn}: WebUI must forward HERMES_WEBUI_GATEWAY_API_KEY from the "
"same API_SERVER_KEY so the gateway health probe authenticates."
)
# ── 2: HERMES_WEBUI_PASSWORD forwarding (all compose files) ────────────────
def test_webui_service_forwards_password():
"""HERMES_WEBUI_PASSWORD set in .env must reach the WebUI container.
.env is only for compose interpolation; without this forwarding line the
password silently does nothing."""
for fn in ALL:
src = (REPO / fn).read_text(encoding="utf-8")
assert "- HERMES_WEBUI_PASSWORD=${HERMES_WEBUI_PASSWORD:-}" in src, (
f"{fn}: WebUI must forward HERMES_WEBUI_PASSWORD from .env — "
"setting it only in .env does nothing unless the compose file "
"references it."
)
def test_env_example_documents_api_server_key():
""".env.docker.example must tell users the API server needs a usable
API_SERVER_KEY (>=16 chars) and that it is forwarded automatically."""
example = (REPO / ".env.docker.example").read_text(encoding="utf-8")
assert "API_SERVER_KEY" in example, (
".env.docker.example must document API_SERVER_KEY."
)
assert ">=16" in example, (
".env.docker.example must state the API_SERVER_KEY length floor "
"(>=16 chars) — shorter keys are silently ignored by the agent."
)
assert "HERMES_WEBUI_GATEWAY_API_KEY" in example, (
".env.docker.example must mention that the WebUI key is forwarded "
"automatically from the same API_SERVER_KEY."
)