Two gaps behind the same near-miss. 1. **No spec covered hermes.franksdank.cloud** — the one guarded vhost whose upstream is a host address (`reverse_proxy 192.168.0.143:8787`) rather than a container name, because its service is host-networked. That is how a loopback-only bind could take the vhost down with a 502 and leave every gate green. tests/e2e/specs/17-hermes-vhost.spec.ts asserts what a dead upstream violates: no 5xx through the guard chain, no Caddy error page, and no app bytes to an unauthenticated raw client. It does not assert SSO-silence, since this app has its own password login. 2. **Specs are matched by number prefix, not globbed.** A spec file whose prefix is in no project silently never runs — which is exactly what happened to the new one until it was registered. The prefix is registered now, the comment in playwright.config.ts says why, and tests/e2e_scaffold.bats fails if any spec file matches no project. The hermes-webui seam assertion is corrected too: it asserted the loopback bind I wrongly imposed, and now asserts the real contract — the unit's bind must serve the address the Caddyfile fronts it on.
7.9 KiB
type, title, description, tags, timestamp
| type | title | description | tags | timestamp | |||||
|---|---|---|---|---|---|---|---|---|---|
| configuration | Hermes-WebUI (Hermex server) | The hermes-webui container on Dankclaw — the API server that the Hermex iPhone app talks to, reachable over an SSH tunnel, from the whole LAN (it binds all interfaces), and from https://hermes.franksdank.cloud (internal-only vhost). |
|
2026-09-14 |
Hermes-WebUI (Hermex server)
hermes-webui is the hermes-webui
server (MIT, third-party) that the Hermex iPhone app connects to. It runs
the Hermes agent in-process from the shared HERMES_HOME, so it is the same
agent — same memory, skills, and sessions — as the Hermes gateway and the
Hermes-Dashboard.
Host: Dankclaw — Port: 0.0.0.0:8787 (all interfaces) — Ingress: SSH tunnel for the phone app; direct LAN access also works
Facts
| Item | Value |
|---|---|
| Image | docker.io/nousresearch/hermes-agent:latest (same as the gateway) |
| Command | python3 /webui/bootstrap.py --foreground |
| Source checkout | /home/dank/Containers/hermes/webui on Dankclaw, bind-mounted at /webui |
| Port | 8787, bound to 0.0.0.0 in the host network namespace (Network=host) — LAN-reachable, not loopback-only |
| Auth | HERMES_WEBUI_PASSWORD (host .env; Hermex signs in with it) |
| Data | bind mount /home/dank/Containers/hermes/data → /data, shared with the gateway and dashboard; anonymous volume at /opt/data |
| Health | HealthCmd= in the Quadlet unit probes http://127.0.0.1:8787/health from inside the unit (works because the webui runs in the host netns); the deployer probes the same path from the host |
Why a container, not a host process
The gateway's state files are owned by the container-mapped uid (109999,
from rootless Podman's 1 → 100000/65536 subuid range). A fresh rootless
namespace on Dankclaw maps identically (verified with podman unshare cat /proc/self/uid_map), so a second container using the same image reads
and writes the shared volume with exactly the ownership the gateway already
uses. A host-side process running as dank (uid 1000) could not read the
gateway's 0600 state files.
How it works
bootstrap.pycreates/webui/.venvon first boot, pip-installs the WebUI and agent requirements, then execsserver.pyas one foreground process (--foreground). systemd-free: Podman supervises it viarestart: unless-stopped.HERMES_WEBUI_SKIP_ONBOARDING=1keeps the first-run wizard off; a profile already exists in/datafrom the gateway.- The healthcheck
start_periodis 600 s because the very first boot spends minutes pip-installing into a fresh venv.
Remote access (Hermex on iOS — persistent SSH tunnel)
Only port 22 is forwarded to Dankclaw at the router, so the phone reaches the
webui through an SSH local forward — no extra router ports, no third-party
relay (no Tailscale, no Cloudflare). Implemented 2026-08-31. The tunnel is an
access path for the phone, not a containment boundary: the server binds
0.0.0.0:8787, so any host that can route to 192.168.0.143 can reach the API
directly and needs only HERMES_WEBUI_PASSWORD to get in (verified 2026-09-02:
an unauthenticated GET /health from another LAN machine returned HTTP 200).
See Container Network Namespaces for why the webui is reachable this way and
the sibling dashboard on 9119 is not.
- Dedicated
tunnelaccount on Dankclaw (/usr/sbin/nologinshell, password locked). Authentication is key-only; the account can do nothing except authenticate —ForceCommand /usr/sbin/nologinkills any session channel while the transport stays up for port forwarding. - sshd drop-in
/etc/ssh/sshd_config.d/99-tunnel.confscopesAllowTcpForwarding yesto that user only (plusPermitTTY no,X11Forwarding no). - Key options on the authorized key pin the blast radius:
command="/usr/sbin/nologin",no-pty,restrict,port-forwarding,permitopen="localhost:8787"— a leaked tunnel key can only forward tolocalhost:8787, never act as a general SOCKS/SSH proxy. - On the phone (client side): create a key on-device (private key never
leaves the device), hand the public key to the admin who appends it to
/home/tunnel/.ssh/authorized_keys, then connect from the SSH client:- In-network target
192.168.0.143:22, off-network target the public hostname (router forwards only port 22 → Dankclaw) — same forward config otherwise. - Local forward
127.0.0.1:8787 → localhost:8787; Hermex server URL is thenhttp://localhost:8787in both locations. - For a persistent tunnel on iOS, Secure ShellFish's personal-VPN mode
(Network Extension keeps the tunnel up and routes
127.0.0.0/8through it); Termius works but only while its session is connected — iOS allows no background SSH daemon.
- In-network target
graph LR
A[Hermex app] -->|http://localhost:8787| B[SSH local forward on iPhone<br/>persistent via personal VPN]
B ==>|encrypted SSH to port 22| C[Dankclaw sshd<br/>tunnel user, forced nologin]
C --> D["hermes-webui 0.0.0.0:8787 (host netns)"]
D --> E["/home/dank/Containers/hermes/data (shared agent state)"]
Deployment
./scripts/deploy.sh dankclaw --service hermes-webui
deploy.sh copies dankclaw/hermes/quadlet/*.container into the quadlet unit
directory and restarts hermes.service, hermes-dashboard.service and
hermes-webui.service in the user manager of dank (linger must be on for
them to come up at boot), so gateway/dashboard/webui restarts stay isolated from
inference deploys.
Before first deploy, set a strong HERMES_WEBUI_PASSWORD in the live
dankclaw/.env.
Gotchas
- The
/webuimount is rw on purpose:bootstrap.pycreates.venvin the repo root. Do not flip it to:ro. - The webui binds
0.0.0.0:8787, and two consumers depend on that: the SSH tunnel (which dials the host'slocalhost) and Caddy, which frontshermes.franksdank.cloudwithreverse_proxy 192.168.0.143:8787— a host address rather than a container name, because this unit is host-networked and there is no port forwarder to bridge the gap (dankclaw/Caddyfilerecords the same constraint). So the SSH tunnel is an access path for the phone, not a security boundary: any LAN host that can route to192.168.0.143reaches the API and needs onlyHERMES_WEBUI_PASSWORD. The webui can run agent commands, so treat that password as a credential with command-execution blast radius. To make loopback-only binding true again you must setHERMES_WEBUI_HOST=127.0.0.1and give Caddy another way in (a host-gateway route or a bridge-network unit) — that is an exposure change to decide deliberately, not a silent fix. It was tried on 2026-09-16 as a silent fix and broke the vhost with a 502 while the whole e2e suite stayed green;tests/e2e/specs/17-hermes-vhost.spec.tsnow asserts that vhost reaches its app through the guard chain, so a dead upstream fails a gate instead of hiding. - The webui and the gateway share one data root (
/datais a bind mount of/home/dank/Containers/hermes/data). A config change made on one side takes effect when the other restarts (same caveat as the dashboard). bootstrap.pyauto-installs a Hermes Agent if it cannot find one; in this image the agent is already present, so that path should never fire. If the log shows an install attempt, the image changed — re-verify the entrypoint.
Citations
- hermes-webui README (env vars,
--foreground,/health): https://github.com/nesquena/hermes-webui - Hermex setup model (server URL + password; Cloudflare/Tailscale are optional, not required): https://hermexapp.com/
See Also
- Hermes — the agent and its Signal gateway
- Hermes-Dashboard — the LAN browser management UI
- Dankclaw Stack
- Container Network Namespaces — why this host-netns service is LAN-reachable