Erawanไทย

Hermes Agent

NousResearch built the agent. You need somewhere to run it.

Hermes Agent is open, it is good, and the hard part is not the agent. It is the box: an address, a certificate, a disk that survives a restart, and somewhere to keep four keys that must never pass through a chat window. This lesson is that half, and the code you write for it is nothing.

1. What you are actually deploying

NousResearch/hermes-agent publishes a container image. Inside it, beside the agent itself, is a LINE adapter that is a first-class platform — the same shelf as Telegram, Discord, Slack, WhatsApp, Teams, SMS and WeCom. It verifies the webhook signature, chunks a long reply into LINE's five bubbles, sends images and audio, keeps an allowlist of users and groups, and drops duplicate deliveries.

That matters because the obvious first move is to write a webhook of your own that catches the message and shells out to the agent. We did exactly that — 160 lines — and then read it against the image and deleted it. Every line was a worse copy of something already in there.

So there is no code in this lesson. Two files, and neither of them is a program.

2. The two files

The image declares no ENTRYPOINT and no CMD at all — read off the image, not from its documentation — so a Dockerfile that names neither runs nothing. And gateway is not a program: it is a subcommand of the binary, so gateway run on its own is sh: gateway: not found.

Dockerfile
FROM nousresearch/hermes-agent:v2026.9.7

# The webhook listens on 8646 at /line/webhook, and the LINE plugin switches
# itself on when both credentials are in the environment — there is no config
# file to write and no platform to select.
ENV LINE_PORT=8646

# The dashboard decides what the agent may do — terminal, code execution,
# computer use and browser are all *on* by default. That is the owner's
# decision, so it is served rather than guessed at deploy time.
ENV HERMES_DASHBOARD=1 \
    HERMES_DASHBOARD_PORT=9119

EXPOSE 8646 9119

CMD ["/opt/hermes/bin/hermes", "gateway", "run"]

The manifest is the other half. access: owner closes the app to everybody but your account; the one path LINE has to reach is opened back up at deploy time, because LINE posts to it with no session and no way to sign in.

erawan.yaml
version: 1
access: owner
components:
  web:
    path: .
    port: 8646
    public: true
    # The dashboard, on a name of its own. Same container, same HERMES_HOME —
    # two components could not share the disk this reads.
    ports:
      9119: hermes-admin
    # Sessions and memory. HERMES_HOME points at this below.
    disk: true
    # Declared and never carried: the deploy stops and you fill these in on
    # the app's own page, so no key passes through an agent.
    secrets:
      [
        LINE_CHANNEL_ACCESS_TOKEN,
        LINE_CHANNEL_SECRET,
        OPENROUTER_API_KEY,
        HERMES_DASHBOARD_BASIC_AUTH_PASSWORD,
      ]
    resources:
      requests: {memory: 512Mi, cpu: 250m}
      limits:   {memory: 2048Mi, cpu: 1000m}

3. Deploy it

erawan deploy . --name NAME --port 8646 --access owner --public-path /line/webhook --env HERMES_HOME=/data --env LINE_PUBLIC_URL=https://NAME.erawan.app --env HERMES_DASHBOARD_BASIC_AUTH_USERNAME=owner

HERMES_HOME is the one environment variable that decides whether this is a toy. It is where sessions and memory live — 138 references to it inside the image, more than any other variable — and pointed anywhere but the disk, every conversation is forgotten on the next deploy.

4. The keys do not go in the command

The deploy stops before it starts anything and prints a link. Four credentials are declared in the manifest and none of them pass through the agent doing the deploy, or through your terminal history:

Key

Where it comes from

LINE_CHANNEL_ACCESS_TOKEN

LINE Developers console

LINE_CHANNEL_SECRET

LINE Developers console

OPENROUTER_API_KEY

whichever model provider you pay

HERMES_DASHBOARD_BASIC_AUTH_PASSWORD

you choose it

You open the link, fill them in on the app's own page, and the deploy resumes. That last one is declared with the rest for a reason: Hermes refuses to bind its dashboard to a non-loopback address without an auth provider, and the dashboard is the surface that decides what the agent is allowed to do. More on how secrets are handled here.

5. Two addresses out of one container

Address

What it is

NAME.erawan.app/line/webhook

paste into LINE Developers

hermes-admin.erawan.app

the dashboard, behind its own login

Then set the webhook URL in the LINE Developers console and press Verify. Send the account a message; that is the whole of it.

6. What it costs, and what we know because we ran it

This fits Hobby — ฿99 a month. The ceiling in the manifest is one core, which is what Hobby includes; the request is 250m, so an agent that is waiting costs the machine nothing and a busy one is slowed rather than refused. Moving up a plan is what buys the second core.

We walked this on production on a real account with a real LINE channel, and it took three deploys. Every failure was ours rather than the template's, and none of them was findable without running it: a worker that could never become ready because everything was probed, including a workload that listens on nothing; a published port whose name was claimed and released in the same second, so the dashboard answered 404 with everything underneath it correct; and a manifest app that silently dropped its public paths, so the webhook answered the sign-in page instead. That last one affected every manifest app with a webhook or a payment callback, and the party who notices a bug like that is LINE, or Stripe, rather than the owner.

All three are fixed. We are telling you because a page that says the first deploy went perfectly is a page written by somebody who did not do it.

All lessons

Self-host Hermes Agent — two files, one deploy — Erawan