hermes: single-container deploy (gateway + dashboard), as deployed on bob
All checks were successful
images / hermes (push) Successful in 37s
All checks were successful
images / hermes (push) Successful in 37s
The image's command selects mode; no command = interactive CLI which crash-loops under systemd. Switched to the supported headless setup: one container running `gateway run` with the dashboard supervised alongside via HERMES_DASHBOARD=1 (same netns so the dashboard can reach the gateway, which two bridge-networked containers could not). Image fails closed on a 0.0.0.0 dashboard bind, so HERMES_DASHBOARD_INSECURE=1 opts into the chosen trusted-LAN exposure on :5100. Verified live on bob: gateway stable, dash HTTP 200 across the LAN, inference endpoint reachable, enrolled in podman-auto-update.timer. Dropped the redundant separate dashboard quadlet. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011D3YeWKpjg5bT488fVanCH
This commit is contained in:
@@ -1,24 +1,23 @@
|
||||
# Reference quadlet for deploying Hermes on bob (bob.hanzalova.internal).
|
||||
# Deploy to /etc/containers/systemd/hermes.container (rootful, matching the
|
||||
# existing agent-zero.container and open-webui.container), then:
|
||||
# sudo install -d -o 10000 -g 10000 /var/lib/hermes # /opt/data owner = HERMES_UID
|
||||
# sudo install -o 10000 -g 10000 /path/to/config.yaml /var/lib/hermes/config.yaml
|
||||
# sudo install -o 10000 -g 10000 /path/to/.env /var/lib/hermes/.env # if needed
|
||||
# sudo systemctl daemon-reload && sudo systemctl start hermes.service
|
||||
# Hermes Agent for bob — single-container "recommended s6" mode.
|
||||
# The image runs `hermes gateway run` under s6 supervision, with the dashboard
|
||||
# web UI supervised alongside in the SAME container when HERMES_DASHBOARD is set
|
||||
# (per the image's own startup guidance). Running both in one container keeps
|
||||
# them in one network namespace, so the dashboard can reach the gateway exactly
|
||||
# as upstream's host-networked compose assumes — which two bridge-networked
|
||||
# containers could not.
|
||||
#
|
||||
# Gated on git.lair.cafe/lair/hermes:latest being published by the `images`
|
||||
# workflow first. After that it's a normal pull + AutoUpdate=registry quadlet —
|
||||
# same lifecycle as the other two services, and now enrolled in the (enabled)
|
||||
# podman-auto-update.timer.
|
||||
# Command must be explicit: the image default (no command) is the interactive
|
||||
# CLI, which exits without a TTY under systemd and crash-loops.
|
||||
#
|
||||
# Dashboard: the image binds the dashboard on 0.0.0.0:9119 by default
|
||||
# (HERMES_DASHBOARD_HOST / HERMES_DASHBOARD_PORT), so bridge networking +
|
||||
# PublishPort below exposes it on the LAN at :5100 with no override needed.
|
||||
# ⚠ The dashboard stores provider API keys and has NO auth — keep it on a trusted
|
||||
# LAN only; front it with an authenticating reverse proxy for anything wider.
|
||||
# Setup: sudo install -d -o 10000 -g 10000 /var/lib/hermes ; drop config.yaml
|
||||
# (provider: custom -> http://hanzalova.internal:31313/v1) ; daemon-reload ; start.
|
||||
# Dashboard publishes to the LAN at :5100 (agent-zero=5080, open-webui=5090).
|
||||
# ⚠ The dashboard stores provider API keys and has NO auth — trusted LAN only;
|
||||
# front it with an authenticating reverse proxy for any wider exposure.
|
||||
# AutoUpdate=registry keeps it current via the enabled podman-auto-update.timer.
|
||||
|
||||
[Unit]
|
||||
Description=Hermes Agent
|
||||
Description=Hermes Agent (gateway + dashboard)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
@@ -26,15 +25,19 @@ Wants=network-online.target
|
||||
Image=git.lair.cafe/lair/hermes:latest
|
||||
ContainerName=hermes
|
||||
AutoUpdate=registry
|
||||
# Keeps the 50X0 LAN convention (agent-zero=5080, open-webui=5090, hermes=5100).
|
||||
Exec=gateway run
|
||||
PublishPort=5100:9119
|
||||
Volume=/var/lib/hermes:/opt/data:Z
|
||||
# Upstream drops to the non-root hermes user (uid/gid 10000); /var/lib/hermes
|
||||
# must be owned 10000:10000 on the host (see install -d above).
|
||||
Environment=HERMES_UID=10000
|
||||
Environment=HERMES_GID=10000
|
||||
# LLM backend (local sovereign inference) is configured in
|
||||
# /var/lib/hermes/config.yaml via provider: "custom" -> see readme.md.
|
||||
# Enable the supervised dashboard web UI inside this container (binds 0.0.0.0:9119).
|
||||
Environment=HERMES_DASHBOARD=1
|
||||
# The image fails closed: it refuses a non-loopback (0.0.0.0) dashboard bind
|
||||
# unless OAuth is configured OR --insecure is opted in. We expose on the trusted
|
||||
# LAN without auth (the chosen "LAN port like the others" tradeoff), so opt in.
|
||||
# ⚠ Anyone on the LAN can reach the API-key-storing UI. To switch to real auth,
|
||||
# drop this and set HERMES_DASHBOARD_OAUTH_CLIENT_ID (+ portal) instead.
|
||||
Environment=HERMES_DASHBOARD_INSECURE=1
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
|
||||
@@ -24,47 +24,51 @@ rebuild via the workflow's `force` dispatch input, or locally:
|
||||
HERMES_REF=v0.2.0 ./build.sh
|
||||
```
|
||||
|
||||
## One image, two roles
|
||||
## How it runs (single container, gateway + dashboard)
|
||||
|
||||
Upstream's compose runs a `gateway` (the agent) and a `dashboard` (web UI on
|
||||
`127.0.0.1:9119`) from the **same image**. Persistent state — `config.yaml`,
|
||||
`.env`, sessions, memory, skills — all lives under `/opt/data` (the single
|
||||
volume). Provider keys and the model backend go in those mounted files, never in
|
||||
the image.
|
||||
The image is an s6-overlay supervisor. The **command selects the mode** — the
|
||||
default (no command) is the *interactive CLI*, which exits without a TTY under
|
||||
systemd and crash-loops. The supported headless setup (per the image's own
|
||||
startup guidance) is **one container running `gateway run` with the dashboard
|
||||
supervised alongside** via `HERMES_DASHBOARD`:
|
||||
|
||||
- `Exec=gateway run` → the agent daemon (`hermes gateway run --replace`), s6-supervised.
|
||||
- `HERMES_DASHBOARD=1` → the dashboard web UI (binds `0.0.0.0:9119`) in the **same**
|
||||
container — which is what lets it reach the gateway (two bridge-networked
|
||||
containers could not, unlike upstream's host-networked compose).
|
||||
- The image **fails closed** on a non-loopback dashboard bind: it refuses
|
||||
`0.0.0.0` unless OAuth is configured *or* `HERMES_DASHBOARD_INSECURE=1` is set.
|
||||
We expose on the trusted LAN without auth, so we opt in. ⚠ Anyone on the LAN
|
||||
can reach the API-key-storing UI — switch to `HERMES_DASHBOARD_OAUTH_CLIENT_ID`
|
||||
for real auth if that's not acceptable.
|
||||
|
||||
Persistent state — `config.yaml`, `.env`, sessions, memory, skills — all lives
|
||||
under `/opt/data` (the single `:Z` volume). Keys/backend go in those mounted
|
||||
files, never in the image or quadlet.
|
||||
|
||||
## Deploying on bob
|
||||
|
||||
See [`hermes.container`](hermes.container) — a rootful quadlet matching the
|
||||
existing `agent-zero` / `open-webui` services on bob. Summary:
|
||||
existing `agent-zero` / `open-webui` services. As deployed:
|
||||
|
||||
1. `git.lair.cafe/lair/hermes:latest` must be published first (run the `images`
|
||||
workflow).
|
||||
1. Publish `git.lair.cafe/lair/hermes:latest` (the `images` workflow).
|
||||
2. `sudo install -d -o 10000 -g 10000 /var/lib/hermes`
|
||||
3. Drop `config.yaml` into `/var/lib/hermes` (owned `10000:10000`) — **LLM backend
|
||||
→ local sovereign inference.** Hermes exposes a `custom` provider for any
|
||||
OpenAI-compatible endpoint, so point it at the same endpoint open-webui uses:
|
||||
→ local sovereign inference** via Hermes' `custom` OpenAI-compatible provider:
|
||||
|
||||
```yaml
|
||||
# /var/lib/hermes/config.yaml
|
||||
model:
|
||||
provider: "custom" # OpenAI-compatible endpoint
|
||||
base_url: "http://hanzalova.internal:31313/v1"
|
||||
api_key: "beast" # matches open-webui's OPENAI_API_KEY
|
||||
default: "<model-id-your-endpoint-serves>" # see: curl http://hanzalova.internal:31313/v1/models
|
||||
# context_length: 32768 # optional
|
||||
# max_tokens: 4096 # optional, output ceiling
|
||||
provider: "custom"
|
||||
base_url: "http://hanzalova.internal:31313/v1" # same endpoint open-webui uses
|
||||
api_key: "<your-inference-key>"
|
||||
default: "<model-id>" # see: curl …/v1/models
|
||||
# context_length / max_tokens optional
|
||||
```
|
||||
|
||||
Any other secrets (web-search/tool keys, messaging tokens) go in
|
||||
`/var/lib/hermes/.env`, never in the quadlet.
|
||||
Other secrets (web-search/tool keys, messaging tokens) go in
|
||||
`/var/lib/hermes/.env`.
|
||||
4. Install `hermes.container` to `/etc/containers/systemd/`, `daemon-reload`,
|
||||
`start hermes.service`.
|
||||
|
||||
### Dashboard LAN exposure (resolved)
|
||||
|
||||
The image binds the dashboard on **`0.0.0.0:9119` by default**
|
||||
(`HERMES_DASHBOARD_HOST` / `HERMES_DASHBOARD_PORT`), so bridge networking +
|
||||
`PublishPort=5100:9119` in the quadlet exposes it on the LAN at `:5100` with no
|
||||
override. ⚠ The dashboard **stores provider API keys and has no auth** — keep it
|
||||
on a trusted LAN only, and front it with an authenticating reverse proxy for any
|
||||
wider exposure.
|
||||
`start hermes.service`. Dashboard then serves on the LAN at
|
||||
`http://bob.hanzalova.internal:5100/`; `AutoUpdate=registry` enrolls it in the
|
||||
host's `podman-auto-update.timer`.
|
||||
|
||||
Reference in New Issue
Block a user