# Quadlet unit: Hermex webui (Hermex) container (rootless, boot-persistent). # # Install path on dankclaw: ~/.config/containers/systemd/hermes-webui.container # Unit name: hermes-webui.service (container name: hermes-webui) # # Runs the open-source hermes-webui (github.com/nesquena/hermes-webui) in # foreground mode: bootstrap.py creates .venv inside the mounted checkout, # installs WebUI + agent deps, then execs server.py as a single foreground # process. It reads HERMES_HOME=/data, so it drives the SAME agent state # (memory, skills, sessions) as the gateway container. # # Host networking on purpose: the agent's chrome-devtools MCP client spawns # npx chrome-devtools-mcp with --browserUrl=http://127.0.0.1:9222, and the CDP # daemon binds loopback-only in the host network namespace (see # chrome-devtools-protocol.container). Only processes sharing that namespace # can drive the browser; a bridge-network client could not reach it at all. # The server binds 0.0.0.0:8787 of the host netns, and that is deliberate: # this unit is host-networked (the CDP daemon above needs a shared loopback), so # Caddy cannot reach it by container name and fronts the vhost # hermes.franksdank.cloud with `reverse_proxy host.containers.internal:8787` # (netavark's host address, 169.254.1.2) — not the host LAN IP, which Caddy's # bridge netns cannot reach for a bare host-netns bind; the measured asymmetry # with published ports is recorded in the hermes block of dankclaw/Caddyfile. # A loopback bind was tried on 2026-09-16 for a tighter posture and broke that # vhost, because a bind is also the only way an off-host process can reach a # host-networked container: the LAN address and the tunnel's localhost hop are # both served by the wildcard, and a loopback-only bind serves only the latter. # Controls are the auth chain in front of the vhost plus the password. [Unit] Description=Hermes webui (Hermex) container [Container] ContainerName=hermes-webui Image=localhost/hermes-agent:latest Pull=never Network=host EnvironmentFile=/home/dank/serverAppConfigs/dankclaw/.env Environment=HERMES_WEBUI_HOST=0.0.0.0 Environment=HERMES_WEBUI_PORT=8787 Environment=HERMES_WEBUI_PASSWORD=${HERMES_WEBUI_PASSWORD} Environment=HERMES_HOME=/data # Headless host: never show the first-run wizard (a profile already # exists in /data from the gateway). Environment=HERMES_WEBUI_SKIP_ONBOARDING=1 Volume=/home/dank/Containers/hermes/data:/data Volume=/home/dank/Containers/hermes/webui:/webui Exec=python3 /webui/bootstrap.py --foreground # JSON exec form on purpose: this quadlet generator joins a bare HealthCmd # into ONE argv element with \x20 escapes and then shell-executes it — the # python one-liner's quoting would be mangled. A JSON array is passed through # verbatim. Start period covers first-boot pip installs into /webui/.venv. HealthCmd=["python3","-c","import urllib.request;urllib.request.urlopen('http://127.0.0.1:8787/health')"] HealthInterval=30s HealthTimeout=10s HealthRetries=5 HealthStartPeriod=600s [Service] Restart=always RestartSec=5 EnvironmentFile=/home/dank/serverAppConfigs/dankclaw/.env [Install] WantedBy=default.target