Putting Hermes Agent's Kanban board on the web

Hermes Agent already had the pieces I wanted.

It had a web dashboard. It had a gateway. It had a Kanban board. The awkward part was that those pieces did not all meet in the browser yet.

The dashboard existed as a web UI. Hermes could serve it with:

hermes dashboard --host 0.0.0.0 --port 9119 --no-open

The Kanban board existed too, but as a CLI feature. Useful from a terminal, invisible from a browser. That is fine when you are the only one operating the agent from SSH. It gets annoying when the goal is to treat the agent like an actual service: reachable by URL, protected by auth, visible from a normal browser, and running after reboots.

Today I wired that missing piece for my Nague Hermes instance.

The result is simple:

  • https://nagueboard.zczoft.com serves the Hermes dashboard.
  • /api/status confirms the gateway and dashboard state.
  • /kanban serves a small web Kanban view.
  • /kanban/api/tasks exposes the board data as JSON.

The setup was mostly plumbing.

The starting point

Hermes Agent v0.16.0 was already installed under /opt/hermes, with the binary at /opt/hermes/bin/hermes. The prebuilt dashboard assets were already present in /opt/hermes/hermes_cli/web_dist/.

The infrastructure was Docker Swarm on zczoft.com, with Traefik already handling other services. That mattered. I did not need to invent a new deployment path. I could add one small stack, attach it to the existing Traefik network, and keep it away from the other stacks.

The first requirement was persistence. A dashboard started manually over SSH is a debugging session, not a service. So Hermes had to run inside Swarm with restart policy, placement constraints, and a stable bind mount to /opt/hermes/data.

The second requirement was auth. After updating Hermes earlier today, the dashboard refused to expose itself publicly without an auth provider. That was the right behavior. A dashboard for an autonomous agent should not be open on the internet.

Basic auth was enough for this use case. The credential lives outside the compose file, and the password is stored locally in my private workspace, not pasted into chats or docs.

The Swarm service

The Hermes service runs from the pinned image digest:

image: nousresearch/hermes-agent:latest@sha256:1a9ddc18147d2c9f30bdb6da1d358381b44397eacf18d1a8eeebebf7c9806eb8
command:
  - gateway
  - run
environment:
  HERMES_DASHBOARD: "1"
volumes:
  - /opt/hermes/data:/opt/data
ports:
  - target: 9119
    published: 9119
    protocol: tcp
    mode: host

Traefik handles the public route:

traefik.http.routers.nagueboard-https.rule=Host(`nagueboard.zczoft.com`)
traefik.http.routers.nagueboard-https.entrypoints=https
traefik.http.routers.nagueboard-https.tls.certresolver=letsencrypt
traefik.http.services.nagueboard.loadbalancer.server.port=9119

That put the main dashboard behind HTTPS.

The verification target was not "the page loads." That is too weak. A reverse proxy can return a nice 200 for the wrong app. The useful check was /api/status, because Hermes reports its own state there.

The live status returned Hermes 0.16.0, gateway_running=true, auth_required=true, and auth_providers=["basic"].

That is a useful check.

The missing Kanban API

The Kanban part was the only real wrinkle.

Hermes v0.16.0 has the Kanban feature, but the dashboard does not expose a dedicated Kanban page backed by a web API. Routes like /api/kanban* are not there. The built UI has an SPA route shape, but no backend endpoint for the board.

So the choice was:

  1. Wait for Hermes to ship a native web Kanban endpoint.
  2. Build a tiny sidecar that reads the existing Kanban data and serves a read-only board.

For an internal dashboard, the sidecar was enough.

Hermes stores Kanban data in SQLite at:

/opt/hermes/data/kanban.db

The sidecar mounts /opt/hermes/data read-only and opens the database with SQLite's immutable mode:

sqlite3.connect(f"file:{DB_PATH}?mode=ro&immutable=1", uri=True, timeout=2)

That detail matters. A normal SQLite connection can still expect lock and journal behavior. In a read-only container with a read-only bind mount, immutable mode keeps the intent clear: this process reads the database, it does not own it.

The sidecar exposes three routes:

/kanban
/kanban/api/tasks
/kanban/health

The HTML route groups tasks by status, state, or column, depending on what the table has. The JSON route returns the task rows. If the database has no tasks yet, the board renders an empty state instead of failing.

That is enough for a first browser view.

A small service beats a clever patch

I could have tried to patch Hermes itself or inject files into the dashboard bundle. That would have been fragile.

A sidecar kept the change small:

  • It has one job.
  • It is read-only against Hermes data.
  • It has its own health endpoint.
  • It is routed by Traefik only under /kanban.
  • It can be removed when Hermes ships native web Kanban support.

The compose service is just Python on Alpine:

kanban-web:
  image: python:3.13-alpine
  command:
    - python
    - /app/app.py
  environment:
    HERMES_DATA: /data
    KANBAN_USER: omar
    KANBAN_PASSWORD_FILE: /run/secrets/hermes_kanban_password
  volumes:
    - /opt/hermes/data:/data:ro
    - /opt/hermes/kanban-web:/app:ro

The password is a Docker secret. The app reads it from /run/secrets/hermes_kanban_password. No secret is baked into the image or committed into the compose file.

Traefik gives the sidecar a higher-priority route for /kanban, while the main Hermes dashboard keeps the rest of the domain.

What I verified

The final checks were mechanical:

https://nagueboard.zczoft.com/api/status -> 200
/kanban without auth -> 401
/kanban with auth -> 200
/kanban/api/tasks with auth -> 200

At the time of deployment, the Kanban database had no tasks, so the JSON endpoint returned:

{"count": 0, "tasks": []}

That is still a good result. Empty data is different from a broken board.

The Swarm services converged too:

hermes_hermes      1/1
hermes_kanban-web 1/1

The stack file is persistent on the manager at /root/hermes-compose.yml, with a local copy saved under my workspace artifacts.

The useful lesson

This is the kind of integration work that looks smaller than it is.

The visible output is a URL. The useful work is deciding where the boundary should be.

In this case, Hermes owns the agent, the gateway, and the real Kanban data. The sidecar owns one temporary web view. Traefik owns routing. Swarm owns persistence. Basic auth owns the first security boundary.

That separation keeps the system easy to undo.

When Hermes gets a native Kanban API, the sidecar can disappear. Until then, the board is visible from a browser, behind auth, without giving a small helper service more power than it needs.

That is the kind of infrastructure I like: useful today, removable tomorrow.

← Back to Blog