Files
containers/images/hermes/readme.md
grenade d53e06d784
All checks were successful
images / hermes (push) Successful in 1m55s
hermes: two-stage build, make /opt/hermes writable by uid 10000
Upstream ships /opt/hermes (app + .venv + scripts) read-only root, which
blocks the agent self-modifying and the gateway auto-installing the
WhatsApp bridge's node_modules in place. Add a derived Containerfile layer
(FROM the upstream build) that chowns/chmods /opt/hermes writable by the
runtime hermes user. Done in the image, not a volume: a volume over
/opt/hermes copies-up once then freezes the app, silently defeating
AutoUpdate=registry. Persistence stays on the /opt/data volume.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011D3YeWKpjg5bT488fVanCH
2026-06-23 18:31:32 +03:00

4.0 KiB

hermes

NousResearch Hermes Agent — a self-improving AI agent — packaged for lair infra and published to git.lair.cafe/lair/hermes.

How it's built

Upstream ships its own Dockerfile (debian 13 + s6-overlay), so we build in two stages — build.sh and the images workflow both do this:

  1. Upstream — build straight from the upstream git context at the release tag into a local tag: podman build github.com/NousResearch/hermes-agent.git#<tag>.
  2. DerivedContainerfile does FROM that and makes the /opt/hermes app tree writable by the runtime hermes user (uid 10000), then publishes git.lair.cafe/lair/hermes:{<version>,latest}.

The writable-tree layer gives the agent "untied hands" (self-modify; the gateway can auto-install the WhatsApp bridge's node_modules in place) without the volume trap: a volume over /opt/hermes would copy-up once and freeze the app, silently defeating AutoUpdate=registry. Baked into the image, it ships hands-free on every pull and refreshes cleanly on update. Persistence belongs on /opt/data (skills, memory, sessions, the WhatsApp bridge via bridge_script), never in /opt/hermes.

Builds are release-triggered (daily poll of the GitHub releases API; a build runs only when that version isn't already in our registry) and self-healing (a failed build leaves the version absent, so the next poll retries). When the build definition changes (e.g. this writable layer) for an already-published version, republish with the workflow's force dispatch input. Locally:

HERMES_REF=v0.17.0 ./build.sh

How it runs (single container, gateway + dashboard)

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 — a rootful quadlet matching the existing agent-zero / open-webui services. As deployed:

  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 via Hermes' custom OpenAI-compatible provider:

    # /var/lib/hermes/config.yaml
    model:
      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
    

    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 then serves on the LAN at http://bob.hanzalova.internal:5100/; AutoUpdate=registry enrolls it in the host's podman-auto-update.timer.