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.comserves the Hermes dashboard./api/statusconfirms the gateway and dashboard state./kanbanserves a small web Kanban view./kanban/api/tasksexposes 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:
- Wait for Hermes to ship a native web Kanban endpoint.
- 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.