Deploy the Hermes agent
This worked example deploys Nous Research's
Hermes agent (Docker
image nousresearch/hermes-agent:latest) onto a Miren cluster. Hermes ships only as a
Docker image, so Miren pulls and runs that image directly.
The end state: the Hermes dashboard served over HTTPS behind basic auth, the
OpenAI-compatible API server intentionally disabled, and all agent state persisted
on a Miren disk at /opt/data.
Along the way it exercises a lot of real Miren behavior: direct image deploys,
the web service convention, 0.0.0.0 binding, port_timeout, and network disks — so
it doubles as a tour of what matters when you bring your own image.
For getting your own source code onto Miren, start with Deployment and the Language Guides. This page is about running a prebuilt third-party image.
Prerequisites
mirenCLI installed and authenticated (miren whoami).- Access to the target cluster and its org.
- An Anthropic API key for the agent's LLM calls. (This example uses Anthropic; the
OPENAI_API_KEYinapp.tomlis optional — supply it too if your agent uses OpenAI.)
Select the target cluster
If the cluster isn't configured locally yet, list what your identity can see and add it (this pins the TLS fingerprint):
# List clusters available to your cloud identity
miren cluster add -i cloud
# Add it by name + public address (from the list above)
miren cluster add -c hermes -a <your-cluster-ip>:8443 -i cloud
miren whoami -C hermes
The commands below target it explicitly with -C hermes. Omit -C to use your default
cluster.
The app.toml
name = "hermes"
[services.web]
image = "nousresearch/hermes-agent:latest"
args = ["gateway", "run"]
port = 9119
port_type = "http"
port_timeout = "180s"
[services.web.concurrency]
mode = "fixed"
num_instances = 1
[[services.web.disks]]
name = "data"
provider = "miren"
mount_path = "/opt/data"
size_gb = 10
filesystem = "ext4"
# Dashboard (the single routed, health-checked port), basic auth.
[[env]]
key = "HERMES_DASHBOARD"
value = "1"
[[env]]
key = "HERMES_DASHBOARD_HOST"
value = "0.0.0.0"
[[env]]
key = "HERMES_DASHBOARD_PORT"
value = "9119"
[[env]]
key = "HERMES_DASHBOARD_BASIC_AUTH_USERNAME"
value = "admin" # change to any username you prefer
[[env]]
key = "HERMES_DASHBOARD_BASIC_AUTH_PASSWORD"
required = true
sensitive = true
[[env]]
key = "ANTHROPIC_API_KEY"
required = true
sensitive = true
[[env]]
key = "OPENAI_API_KEY"
sensitive = true
The image's ENTRYPOINT is /opt/hermes/docker/entrypoint-dispatch.sh. When it runs as
PID 1, the dispatcher starts s6-overlay and passes the arguments through to Hermes' main
wrapper. The args field supplies gateway and run as separate arguments without
replacing that startup path. The image doesn't expose a port, so the recipe declares the
dashboard's port explicitly.
webMiren's HTTP ingress routes a hostname to the app's web service by default, so the
easiest layout is to name the HTTP service web. Name it something else and miren route set
rejects it unless you pass --service <name>. If you want a portless background worker
instead (reachable only via a messaging platform or miren sandbox exec), name the service
something else and omit port/port_type — then Miren does no HTTP ingress and no port
health check.
Two details in that config bite if you skip them. Miren health-checks and routes to the
port from outside the container, but Hermes components default to 127.0.0.1 — set
HERMES_DASHBOARD_HOST=0.0.0.0 (and API_SERVER_HOST=0.0.0.0 if you ever enable the API
server) or the health check reports nothing is listening on :<port>. And first boot is
slow: s6 init, first-boot seeding, and a config-schema migration all run before the port
binds, so port_timeout = "180s" keeps the check from giving up at the 15-second default.
A provider = "miren" (network) disk holds one exclusive lease, so it requires
concurrency mode = "fixed" with num_instances = 1. On redeploy the new instance can't
mount /opt/data until the old one releases the lease, so miren deploy frequently prints
did not become healthy while the instance actually comes up a few seconds later —
re-check miren app status / miren sandbox list before assuming failure. If you'd rather
have snappy rollouts and don't need node-independent storage, use provider = "local" (no
lease, no fixed-instance requirement; data is node-local).
Hermes warns that a network-accessible OpenAI-compatible API server with the default
local terminal backend runs agent work as the host user with full file and terminal
access. This config leaves it off. Enable it only deliberately and sandboxed
(terminal.backend: docker) with the port firewalled.
Deploy
Non-secret config lives in app.toml. Pass secrets with -s (masked in output, stored
server-side); never bake them into the image:
# Generate the dashboard password once and save it somewhere safe:
DASH_PW=$(openssl rand -base64 18 | tr -d '/+=' | cut -c1-20)
miren deploy -a hermes -C hermes -f \
-s "ANTHROPIC_API_KEY=sk-ant-..." \
-s "HERMES_DASHBOARD_BASIC_AUTH_PASSWORD=$DASH_PW"
Validate the config without building at any time:
miren deploy --analyze -a hermes -C hermes
miren env delete redeploys the current server specTo remove a previously-set secret (manual) env var, use miren env delete — but it
builds the new version by copying the current server-side spec, not your local
app.toml. Order matters: deploy your new app.toml first, then delete the stale env
var. Config-source vars (declared in app.toml) drop automatically when you remove them
and redeploy.
Add a hostname route
miren route set gw.hermes.clusters.miren.run hermes -C hermes
miren route list -C hermes
route set <host> <app> routes the host to the app's web service by default; pass
--service <name> to target another HTTP service. *.clusters.miren.run DNS and TLS
are managed by the cluster; give it a few seconds to provision.
Verify
miren app status -a hermes -C hermes # Current Version + active
miren sandbox list -C hermes # one running sandbox, service "web"
miren logs -a hermes -C hermes --since 5m # look for HERMES_DASHBOARD_READY port=9119
# Route should 302 -> /login (200), served by uvicorn:
curl -sL -o /dev/null -w "%{http_code} %{url_effective}\n" https://gw.hermes.clusters.miren.run/
Healthy signs in the logs: s6-rc: service main-hermes successfully started,
Fixing ownership of /opt/data, config schema 0 -> NN, and
HERMES_DASHBOARD_READY port=9119.
Connecting to the agent
- Dashboard:
https://gw.hermes.clusters.miren.run(the basic-auth username fromapp.toml—adminunless you changed it — plus the password from the deploy step). - CLI (no port needed):
miren sandbox exec <sandbox-id> -C hermes, then run thehermesCLI inside the container. - Messaging platform (no inbound port): set
TELEGRAM_BOT_TOKEN; the agent dials out and you DM the bot.
Roadblock checklist
- Set
args = ["gateway", "run"]so Miren preserves the image's dispatcherENTRYPOINTand supplies the gateway subcommand. - Name the HTTP service
web(or pass--service <name>tomiren route set). - Bind services to
0.0.0.0, not127.0.0.1. port_timeout = "180s"for the slow s6 first boot.- A
mirendisk meansfixed/num_instances = 1; expectdid not become healthynoise on redeploy (verify state before retrying). Uselocalfor snappy rollouts. - Deploy the new
app.tomlbeforemiren env delete(it copies the live spec). - Keep the OpenAI API server off unless sandboxed — it runs agent work as the host user.
Next steps
- App Configuration — the full
app.tomlreference in context - Persistent Storage — Miren disks vs. local disks
- Traffic Routing — how the
webservice and routes fit together - Services — image defaults,
args, and full command overrides