Skip to main content

Overview

Hermes Agent is a self-hosted AI agent from Nous Research. You bring the model — an API key for Anthropic, OpenAI, or any OpenAI-compatible endpoint — and the agent brings the memory, tools, and interfaces around it. This template deploys the supervised gateway as a single stateful replica with an OpenAI-compatible API for your clients, a web dashboard, a pre-registered Control Plane MCP server, and optional browser automation, external webhooks, and a personal WhatsApp bridge, with all agent state on a persistent volume. Every credential — your LLM API key, the gateway’s bearer token, the dashboard password, and the optional webhook signing secret — lives in one dictionary secret you create before installing. No credential passes through Helm or lands in the release.
This template does not create a GVC. It deploys into an existing GVC that you already have.

What Gets Created

The chart creates no secret of its own. The model is external: every completion is dispatched to your LLM provider with the key you supply, and the workload holds no model weights.

Prerequisites

Two things must exist before you install: an LLM API key from your provider, and a dictionary secret that holds it together with the other credentials. A custom domain is needed only for one optional webhook setup.
1

Get an LLM API key

Obtain a key from Anthropic, OpenAI, or any OpenAI-compatible endpoint such as OpenRouter, Ollama, vLLM, or LM Studio (reached with model.provider: custom).Anthropic keys must be workspace-scoped — create the key inside a workspace in the Anthropic console. A default or identity-linked key fails every request with HTTP 400: anthropic-workspace-id is required.
2

Create the dictionary secret

The secret holds up to four values. Name the keys however you like — the defaults below match the chart’s secret.keys mapping, so an existing secret works once you point secret.keys at its key names. The gateway API key must be at least 16 characters. Secrets are org-level, so there is no --gvc flag:
Set secret.name to this name.
3

Optional: a custom domain for webhooks

Only if you want the dashboard or API public on the canonical endpoint and external HTTPS webhooks at the same time. You create and own the domain; the chart never creates one. See Webhooks on a Custom Domain. The canonical-endpoint webhook path (publicAccess.expose: webhooks) needs no domain.
A missing prerequisite secret wedges the install rather than failing it. cpln helm install reports success and creates all four resources, but the workload never starts, and there are no container logs to diagnose from because the container never ran. The only place the missing secret is named is status.versions[].message:
Look for The secret SECRET_NAME no longer exists. Workload updates are paused until the secret is added or the reference to the secret removed. Create the secret and the deployment recovers on its own; to skip the wait, run cpln workload force-redeployment RELEASE_NAME-hermes-agent --gvc GVC_NAME.
The gateway API key must be at least 16 characters. Hermes refuses a shorter key: the gateway starts, but the API never serves and the workload never becomes ready. Generate it with openssl rand -hex 32 as shown above.

Installation

Install the template into an existing GVC, naming the dictionary secret you created:
To install using another method, follow the instructions for your preferred tool:

UI

Browse, install, and manage templates visually

CLI

Manage templates from your terminal

Terraform

Declare templates in your Terraform configurations

Pulumi

Declare templates in your Pulumi programs

Configuration

Image

Model

For example, to route through OpenRouter or a comparable OpenAI-compatible service:
model.name takes the provider’s bare, hyphenated model ID (claude-opus-4-6, not claude-opus-4.6) — with provider: anthropic the chart adds the anthropic/ prefix itself, so a pre-prefixed value renders a doubled anthropic/anthropic/… prefix. Setting a name is recommended for non-Anthropic providers.
Hermes sends a reasoning effort with every request, and models that do not support reasoning reject it with 400: Unsupported parameter: 'reasoning.effort'. Set reasoningEffort: none for those (for example gpt-4o); keep the default for reasoning-capable models.
model.baseUrl has no effect with provider: anthropic — the Anthropic client ignores it, so the chart refuses to render that combination. To route Anthropic models through a proxy, use provider: custom.

Secret

secret.name is the dictionary secret from Prerequisites; the policy grants the workload reveal on exactly that one secret. Each secret.keys.* entry names the key inside your secret that holds a value — override them to match an existing secret rather than renaming its keys. The chart references every value as cpln://secret/SECRET_NAME.KEY, so no credential appears in the workload spec or the Helm release.

Dashboard

With the default publicAccess.expose: dashboard, the chart sets the dashboard’s public URL to the canonical endpoint automatically, which is what makes MCP OAuth callbacks work. Set dashboard.publicUrl only when you front the workload with your own domain. publicAccess.expose: dashboard requires dashboard.enabled: true; the chart refuses to render otherwise.

Browser Automation

The Hermes image ships no launchable browser, so on its own a browser tool call quietly falls back to text-level page fetching (web_extract). Set browser.enabled: true and the chart adds a pinned headless-Chromium container (chromedp/headless-shell) to the workload and seeds browser.cdp_url so the agent attaches to it over loopback CDP and drives real page navigations. The browser profile is ephemeral, the sidecar has no published port and no probe, and it is a real ongoing CPU and memory cost — which is why it is off by default. Keep its maxCpu within 4× its minCpu.

Control Plane MCP

The Control Plane MCP server is registered in the agent’s config so it appears on the dashboard’s MCP page ready to authenticate. It is inert until you click Authenticate — nothing reaches your org before that. Once authenticated, the agent can manage resources in your org and the OAuth session acts as you; tokens persist on the volume across restarts and redeploys. Set cplnMcp.enabled: false to skip registering it. See Connecting MCP Servers That Need OAuth.

Webhook Listener

Turning this on starts the HMAC-signed listener on port 8644 and reads the shared signing secret from secret.keys.webhookSecret, so add that key to your secret before enabling it. See Webhooks under Connecting for how to expose the listener and send a signed event.

WhatsApp

Links the agent to a personal WhatsApp account through the image’s built-in Baileys bridge. On first pairing the gateway installs the bridge’s Node dependencies onto the volume (one-time, reaching only registry.npmjs.org) and stores the session there, so it survives restarts and redeploys. Pairing needs your phone — see Messaging Platforms. The WhatsApp Cloud (business) API is a separate integration this template does not wire.

Resources

The workload idles at the floor and bursts toward the ceiling during heavy agent turns and tool work. Keep maxCpu within 4× minCpu — the platform rejects a wider ratio on a stateful workload. The browser sidecar has its own browser.resources block under the same rule. Values files from template version 1.0.0 still carry resources.cpu / resources.memory; see Upgrading From 1.0.0.

Storage

The volume is mounted at /opt/data and holds everything the agent remembers: the SQLite memory database, sessions, learned skills, agent config, MCP OAuth tokens, messaging-platform tokens, and the WhatsApp session. maxCapacity must be at least capacity when autoscaling is enabled.

Access

A workload gets one canonical HTTPS endpoint, and it fronts a single port. publicAccess.expose picks which surface that is; the other surfaces stay reachable only inside Control Plane. It has no effect until publicAccess.enabled is true.
publicAccess.enabled: true publishes a terminal-capable agent to the internet. With the default expose: dashboard that is a basic-auth login form; with expose: api it is a bearer-guarded API. Whoever gets in operates the agent with full file access as the container user, so use a long random password and key (openssl rand -hex 32) and prefer restricting reach with internalAccess. Public access is off by default.
Keeping the API internal is a valid, safer posture for event-driven deployments: publicAccess.expose: webhooks publishes only the HMAC-signed webhook listener while the API stays on the internal endpoint. An access change takes up to a few minutes to take effect. Re-check before concluding the knob did nothing.

Connecting

A quick check that the gateway is serving, from your own machine:
A 200 means the API is up. A chat completion against the public API (expose: api), or from another workload against the internal address, looks like this — export API_SERVER_KEY from cpln secret reveal SECRET_NAME -o yaml first:
cpln port-forward is for quick checks like the one above. The supported way to use the dashboard in a browser is the public canonical endpoint (publicAccess.enabled: true with the default expose: dashboard); with public access off, the dashboard is reachable only from inside Control Plane.

First Run

The dashboard needs no setup wizard — the admin login is dashboard.username plus the password in your secret. The first-run work is signing in and, if you keep cplnMcp.enabled: true, authorizing the Control Plane MCP server.
1

Install with public access on

Install (or upgrade) with publicAccess.enabled: true, keeping the default publicAccess.expose: dashboard, and wait for the workload to report ready:
2

Open the canonical endpoint and sign in

Read status.canonicalEndpoint from cpln workload get RELEASE_NAME-hermes-agent --gvc GVC_NAME -o yaml, open it in a browser, and sign in with dashboard.username (default admin) and the dashboard password from your secret.
3

Authenticate the Control Plane MCP server

On the dashboard’s MCP page the Control Plane server is already listed. Click Authenticate: your browser goes to Control Plane, you sign in, and it returns to the dashboard. The agent can now manage resources in your org as you — including creating and deleting real infrastructure — so do this deliberately. Nothing reaches your org until you complete this step.
4

Confirm the server is authenticated

Authentication state is not badged in the dashboard. Use the server’s Test button, or from a shell in the container run hermes mcp test cpln:

Webhooks

Set webhooks.enabled: true to start the listener on port 8644; the chart also enables the hermes webhook subscribe CLI inside the container. Each subscription carries its own HMAC signing secret: hermes webhook subscribe generates one and prints it at creation, or pass --secret "$WEBHOOK_SECRET" to reuse the shared value from secret.keys.webhookSecret, which the container sees as $WEBHOOK_SECRET. Callers sign each event as the HMAC-SHA256 of the body in the X-Webhook-Signature header; the gateway also accepts the timestamped X-Webhook-Signature-V2 form for replay protection. There are two ways to make the listener reachable from outside, both HTTPS:

Webhooks on a Custom Domain

This is the only way to have the dashboard (or API) public on the canonical endpoint and accept external HTTPS webhooks at the same time. The domain is a prerequisite you own: a domain is an org-level resource, and only you can add its DNS records.
1

Enable the listener

Install or upgrade with webhooks.enabled: true and a webhook-secret key in your prerequisite secret. The dashboard stays on the canonical endpoint (default publicAccess.expose: dashboard); the listener runs internally on port 8644.
2

Create the domain resource

Save this as domain.yaml, substituting your subdomain, GVC, and release, then apply it:
The configure a domain guide covers the domain resource in depth.
3

Add the two DNS records

Read the exact records from status.dnsConfig:
4

Wait for the domain to reach ready

Re-run cpln domain get webhooks.example.com -o yaml until the status reads ready. It passes through pendingDnsConfig and pendingCertificate while DNS resolves and the certificate is issued.
5

Register a subscription

On the dashboard’s Webhooks page, or from a shell in the container with hermes webhook subscribe NAME --events "*". Copy the subscription’s own secret, or pass --secret "$WEBHOOK_SECRET" to reuse the shared one.
6

Send a signed event

POST to https://webhooks.example.com/webhooks/NAME with the body’s HMAC-SHA256 in X-Webhook-Signature:
A correct signature returns 200 or 202; a wrong one returns 401.
GET / on the webhook domain returns 404 by design — the domain routes to the webhook listener, which serves only /webhooks/NAME. A 404 at the root means the domain is wired correctly.
The dashboard and CLI always display a subscription’s URL as http://localhost:8644/webhooks/NAME, however you expose the listener. This is an upstream display limitation; the app cannot be told its public URL. Only the path matters: take /webhooks/NAME and prepend your domain or the canonical endpoint, and give external callers that address.

Messaging Platforms

Hermes supports chat-platform gateways such as Telegram, Discord, and Slack. They are configured after install with Hermes’s own interactive setup, not through this template’s values, and the configuration is stored on the data volume:
  • Telegram works with a bot token from @BotFather; nothing else is needed.
  • Slack requires an app manifest. Slack caps an app at 25 slash commands and Hermes’s generated manifest emits around 50 — trim it to 25 or fewer before creating the app, or Slack rejects the manifest. Slack’s user allowlist also fails closed when empty — an empty allowlist silently rejects everyone, so set the allowed users explicitly.
  • WhatsApp (personal account) requires whatsapp.enabled: true. Pair after install: open the dashboard’s WhatsApp setup and scan the QR with WhatsApp on your phone (Settings → Linked devices → Link a device), or get a terminal QR with cpln workload exec RELEASE_NAME-hermes-agent --gvc GVC_NAME --container hermes -- hermes whatsapp. Hermes then runs as that WhatsApp account; when someone messages the linked number, Hermes replies with a pairing code that you approve on the dashboard’s Pairing page (or pre-authorize people with an allowlist during setup).
See the Hermes Agent documentation for each platform’s requirements.
A connected chat platform, like an OAuth-connected MCP server, acts as the person who paired or authenticated it. Messages from approved users can invoke the agent’s tools with that person’s permissions — for the Control Plane MCP server, that includes creating and deleting real infrastructure.

Connecting MCP Servers That Need OAuth

Control Plane’s own MCP server (mcp.cpln.io) is pre-registered when cplnMcp.enabled is true, so it appears on the dashboard’s MCP page and you go straight to Authenticate. For any other OAuth MCP server, add it on the MCP page with Authentication: OAuth first. Then:
  • With publicAccess.expose: dashboard (the default): click Authenticate — your browser goes to the provider, you sign in, and it redirects back to the dashboard. The chart sets the dashboard’s public URL to the canonical endpoint automatically, which is what makes the callback work. Tokens persist on the volume across restarts and redeploys.
  • With a custom domain in front of the dashboard, set dashboard.publicUrl to that domain’s base URL so callbacks and asset URLs use it instead of the canonical endpoint.
  • With expose: api the dashboard has no public URL for OAuth callbacks, so use the one-time CLI flow instead: open a shell with cpln workload connect RELEASE_NAME-hermes-agent --gvc GVC_NAME --container hermes, run hermes mcp login NAME, open the printed URL in your browser, and when it lands on a 127.0.0.1:27890/callback connection error (expected), paste that full URL back into the shell.
MCP authentication state is not badged in the dashboard — the Test button (or hermes mcp test NAME) is the reliable way to tell whether a server is authenticated.

Operations

Backing Up

There is no scheduled-backup feature. The volume set takes no scheduled snapshots; the only automatic snapshot is the final one taken when the volume set is deleted, kept for 7 days. Everything the agent remembers lives on that volume — the memory database, sessions, skills, config, MCP tokens, platform tokens, and the WhatsApp session — so take a manual volume set snapshot before a risky change:

Restoring a Backup

Restoring the agent’s state from a volume set snapshot has not been verified on this template. Treat snapshots as a safety net for a risky change rather than a tested recovery path, and keep anything you cannot lose outside the agent.

Rotating a Secret Value

A cpln:// secret reference is resolved when a replica starts and never re-read while it runs, so changing a value in your secret does nothing to the running workload — the old credential keeps working with no error. Update the secret, then force a redeployment:
1

Update the secret

Re-apply the whole secret with the new value in place. Save it as secret.yaml and apply it:
2

If you rotated the LLM API key, clear its on-volume copy

The startup script copies the LLM API key into /opt/data/.env on the volume, where messaging-platform and webhook-triggered turns read it — but only when no entry exists there yet. After rotating that key, remove the old line so the next start writes the new value (use OPENAI_API_KEY for openai and custom providers):
This step follows from the chart’s startup script and has not been verified on a live install. The dashboard and the gateway API pick up the new key on redeploy regardless.
3

Force a redeployment

The replica restarts with the new values. The dashboard password and the gateway API key are read from the environment on every start, so this is all they need.

Upgrading From 1.0.0

Template version 1.1.0 renamed the two ceiling keys in resources, because a block that exposes both a reservation and a limit names the limit explicitly: A values file that still carries an old key is refused at render, so the upgrade stops before touching the release:
Rename the keys, keep the same values, and upgrade in place. Agent state on the volume is unaffected.

Upgrading From 1.2.1 or Earlier

Template version 1.3.0 changed the default publicAccess.expose from api to dashboard. Versions 1.0.0 and 1.1.0 had no expose knob at all — public access always meant the gateway API.
If your install has publicAccess.enabled: true and does not set publicAccess.expose, the upgrade moves the canonical endpoint from the API to the dashboard. Clients calling the API on the public endpoint start receiving the dashboard’s login page, and the API becomes reachable only inside Control Plane at RELEASE_NAME-hermes-agent.GVC_NAME.cpln.local:8642. To keep the API public, set publicAccess.expose: api explicitly before upgrading.
Also new in 1.3.0: expose: dashboard requires dashboard.enabled: true (the render fails otherwise), and secret.keys.webhookSecret was added — it is read only when webhooks.enabled is true, so an existing secret needs no new key unless you turn webhooks on.

Upgrading From 1.3.0 or Earlier

Template version 1.4.0 pre-registers the Control Plane MCP server by default (cplnMcp.enabled: true). After upgrading, the server appears on the dashboard’s MCP page ready to authenticate; it stays inert until someone clicks Authenticate, and nothing reaches your org before that. Set cplnMcp.enabled: false to opt out. Version 1.4.1 also moved to image v2026.9.21 and fixed messaging-platform and webhook-triggered turns, which on a default 1.4.0 install replied that they could not connect to the model.

Scaling and Availability

This template runs exactly one replica by design: the agent’s memory is a single-writer SQLite database, and upstream forbids two gateways sharing one data directory. Do not scale the workload out. State persists on the volume set, so a restart or upgrade loses only in-flight work and causes a brief outage. The first helm upgrade after an install re-applies resources even when no value changed, which restarts the workload; later no-op upgrades do not. Grow storage with volumeset.autoscaling rather than replicas.

Troubleshooting

Symptom: cpln helm install succeeded, but the workload has no running replica and cpln logs returns nothing.Cause: The secret named in secret.name does not exist. The deployment is paused waiting for it.Fix: Confirm with cpln workload get-deployments RELEASE_NAME-hermes-agent --gvc GVC_NAME -o yaml — status.versions[].message reads The secret SECRET_NAME no longer exists. Workload updates are paused until the secret is added or the reference to the secret removed. Create the secret as shown in Prerequisites; the deployment recovers on its own, or run cpln workload force-redeployment RELEASE_NAME-hermes-agent --gvc GVC_NAME to skip the wait.
Symptom: The container is running and logging, but the readiness check on /health never passes and the workload never reports ready.Cause: The gateway API key in your secret is shorter than 16 characters. Hermes refuses it, so the API server never serves.Fix: Generate a longer key with openssl rand -hex 32, update the secret, and force a redeployment — see Rotating a Secret Value.
Symptom: With provider: anthropic, every model call fails with HTTP 400: anthropic-workspace-id is required.Cause: The Anthropic key is a default or identity-linked key, not a workspace-scoped one.Fix: Create a new key inside a workspace in the Anthropic console, put it in your secret, and force a redeployment.
Symptom: Model calls fail with 400: Unsupported parameter: 'reasoning.effort'.Cause: The configured model does not support reasoning, and Hermes sends a reasoning effort with every request.Fix: Set model.reasoningEffort: none and upgrade the release.
Symptom: The gateway API returns 200, but the body carries "finish_reason": "error" and "hermes": {"failed": true} instead of a completion.Cause: The call to your LLM provider failed — a bad key, a rejected model ID, or a provider-side error. Hermes reports provider failures inside the body, not in the HTTP status.Fix: Read the error in the body, or the agent log inside the container: cpln workload exec RELEASE_NAME-hermes-agent --gvc GVC_NAME --container hermes -- tail -n 50 /opt/data/logs/agent.log. Make your clients inspect the body rather than the status code.
Symptom: The dashboard and the gateway API answer normally, but every messaging-platform or webhook-triggered turn replies that it could not connect to the AI model.Cause: Those turns read the LLM API key from /opt/data/.env on the volume rather than from the environment. On template version 1.4.0 and earlier that file was never written, so every channel failed on a default install. On 1.4.1 the same symptom appears after rotating the LLM API key, because the file keeps the old value.Fix: Upgrade to 1.4.1. After a key rotation, clear the stale line and force a redeployment as described in Rotating a Secret Value.
Symptom: Clicking Authenticate on an MCP server fails with registration failed … cleartext http redirect_uris are only allowed for loopback hosts.Cause: The dashboard does not know its public https:// URL, so it registers a cleartext callback that the provider refuses. The chart sets the URL automatically only under publicAccess.expose: dashboard.Fix: Fronting the dashboard with your own domain, set dashboard.publicUrl to that domain’s base URL. Under expose: api, use the CLI login flow in Connecting MCP Servers That Need OAuth.
Symptom: The agent answers browser tasks with extracted page text, and the agent log shows no CDP connection.Cause: browser.enabled is false. The Hermes image ships no browser, so browser tools fall back to web_extract.Fix: Set browser.enabled: true and upgrade the release. A real navigation logs a resolved CDP endpoint in /opt/data/logs/agent.log.
Symptom: The dashboard and hermes webhook CLI show every subscription as http://localhost:8644/webhooks/NAME, even with public access or a custom domain in place.Cause: Upstream Hermes displays the listener’s bind address and cannot be told its public URL.Fix: Nothing is broken. Keep the /webhooks/NAME path and prepend your domain or the canonical endpoint when giving the address to callers.
Symptom: Opening the root of your webhook domain in a browser returns 404.Cause: The domain routes to the webhook listener on port 8644, which serves only /webhooks/NAME.Fix: None needed — a 404 at the root means the domain is wired correctly. Send a signed POST to a subscription path to test it.
Symptom: cpln logs '{gvc="GVC_NAME", workload="RELEASE_NAME-hermes-agent"}' --limit 50 --since 10m shows [config-migrate] WARNING: This config predates version 12 on every start.Cause: The config the chart seeds carries no _config_version, so upstream prints this notice. It is benign: the config is applied, MCP registers, and the memory database is healthy.Fix: None needed. Do not treat it as a defect.
Symptom: cpln helm upgrade stops at render with hermes-agent: resources.cpu was RENAMED to resources.maxCpu (or the resources.memory equivalent).Cause: Your values file still uses the 1.0.0 key names.Fix: Rename cpu to maxCpu and memory to maxMemory under resources, keep the same values, and upgrade again — see Upgrading From 1.0.0.
Symptom: The render fails with publicAccess.expose 'dashboard' requires dashboard.enabled: true — there is no dashboard to front.Cause: dashboard.enabled is false while publicAccess.enabled is true with the default expose: dashboard.Fix: Re-enable the dashboard, or set publicAccess.expose to api or webhooks.
Symptom: You changed publicAccess or internalAccess.type and the workload still behaves as before.Cause: Firewall changes take up to a few minutes to take effect.Fix: Wait a few minutes and test again before concluding the setting is wrong.

Important Notes

  • Create the dictionary secret before installing. A missing secret does not fail the install — the workload waits on it with no container logs to diagnose from.
  • The gateway API key must be at least 16 characters, or the API never serves and the workload never becomes ready.
  • publicAccess.enabled: true publishes a terminal-capable agent to the internet behind a basic-auth login (expose: dashboard) or a bearer token (expose: api). Use long random credentials and prefer restricting reach with internalAccess. It is off by default.
  • Anthropic keys must be workspace-scoped — any other kind fails every request.
  • An authenticated MCP server or paired chat platform acts as the person who connected it. For the pre-registered Control Plane MCP server that includes creating and deleting real infrastructure; it does nothing until you click Authenticate.
  • Single replica by design — memory is a single-writer SQLite database. Do not scale out; a restart or upgrade is a brief outage.
  • Failed model calls return HTTP 200 with the error in the body ("finish_reason": "error"). Clients must inspect the body, not the status code.
  • Rotating a secret value needs a forced redeployment, and a rotated LLM API key also needs its stale line cleared from /opt/data/.env — see Rotating a Secret Value.
  • Keep maxCpu within 4× minCpu in both the resources and browser.resources blocks — the platform rejects a wider ratio on a stateful workload.
  • browser.enabled adds a second container with its own resource floor — an ongoing cost. Leave it off unless the agent must drive a real browser.
  • Everything the agent remembers lives on the volume set and survives redeploys and upgrades. cpln helm uninstall deletes it; the final snapshot is kept for 7 days. Changing the secret and redeploying does not reset memory or config.
  • The prerequisite secret is not owned by the release — it survives cpln helm uninstall and must be deleted separately if you no longer need it.
  • Upgrading from 1.2.1 or earlier can move the public endpoint from the API to the dashboard — see Upgrading From 1.2.1 or Earlier before you upgrade.

External References

Hermes Agent Documentation

Official Hermes Agent documentation

Hermes Agent Repository

Source, issues, and release notes

Nous Research

The team behind Hermes Agent

Control Plane MCP Server

The MCP server this template pre-registers for the agent

Hermes Agent Template

View the source files, default values, and chart definition