Hermes Agent Docker Compose Stack
Hermes Agent runs as one container. The image is nousresearch/hermes-agent on Docker Hub, the gateway API listens on 8642, the optional dashboard on 9119, and every piece of state lives in a single /opt/data volume backed by SQLite. There is no database container to run and no vector store to provision. One step catches almost everyone: you have to run the interactive setup wizard against the data directory once, before docker compose up -d does anything useful.
Everything below was checked against Nous Research's own Docker guide and environment-variable reference on 2026-09-09.
What changed on this page, 2026-09-09
This guide previously published a stack built on ghcr.io/nousresearch/hermes-agent, port 7860, and a PostgreSQL plus pgvector service. None of that matched the shipped product. If you copied the old compose file, that is why your pull failed.
| Was published here | Verified on 2026-09-09 |
|---|---|
ghcr.io/nousresearch/hermes-agent | nousresearch/hermes-agent on Docker Hub |
| Web UI on port 7860 | Gateway on 8642, dashboard on 9119 |
| PostgreSQL + pgvector service | SQLite with FTS5, inside /opt/data |
HERMES_LLM_* environment variables | API_SERVER_*, HERMES_DASHBOARD_*, ANTHROPIC_API_KEY, HERMES_MODEL |
| 30-second LLM timeout | HERMES_API_TIMEOUT, default 1800 seconds |
| "Hermes has no Slack or Telegram bot" | Roughly 28 messaging connectors from one gateway process |
| Measured 2.1 GB idle | Withdrawn. That figure included a Postgres container Hermes never needed. |
The old runtime-footprint table is gone rather than corrected, because it measured a stack that does not exist.
What is the correct Hermes Agent Docker image?
The image published by Nous Research is on Docker Hub:
nousresearch/hermes-agent:latest
The path a lot of people are pasting is not published:
ghcr.io/nousresearch/hermes-agent
GitHub's package page for that GHCR path returns 404, and an anonymous pull token for nousresearch/hermes-agent on ghcr.io is denied, both checked 2026-09-09. Nous Research's Docker user guide documents only the Docker Hub path.
Tags follow calendar versioning. Docker Hub listed v2026.9.7, v2026.8.31 and v2026.8.27 among 29 tags on 2026-09-09, and the matching GitHub release v2026.9.7 was published 2026-09-07. Pin a calendar tag in production so an unattended docker compose pull cannot move you a release forward.
Which ports does Hermes Agent use?
| Port | Service | Enabled by | Default bind |
|---|---|---|---|
| 8642 | OpenAI-compatible gateway API and health endpoint | API_SERVER_ENABLED | API_SERVER_HOST, default 127.0.0.1 |
| 9119 | Web dashboard | HERMES_DASHBOARD=1 | HERMES_DASHBOARD_HOST, default 0.0.0.0 |
Both defaults are documented in Nous Research's environment-variable reference. The API_SERVER_HOST default is the one that bites in Compose. Inside a container, 127.0.0.1 is that container's own loopback, so a reverse proxy in a second container gets a connection refused. Set it to 0.0.0.0 and leave the port unpublished.
The guide says port 8642 exposes a health endpoint but does not give its path. So the compose file below ships no healthcheck block. Every other guide you will find invents one; an invented path produces a container that reports unhealthy forever.
The docker-compose.yml
# hermes-stack/docker-compose.yml
# Derived from Nous Research's Docker guide, checked 2026-09-09.
# https://hermes-agent.nousresearch.com/docs/user-guide/docker
services:
hermes:
image: nousresearch/hermes-agent:v2026.9.7
container_name: hermes
restart: unless-stopped
command: gateway run
shm_size: "1g" # Playwright needs it; 64 MB default is not enough
expose:
- "8642" # reachable from caddy only, not published to the host
- "9119"
volumes:
- ./data:/opt/data # every piece of state lives here
environment:
# Dashboard
HERMES_DASHBOARD: "1"
HERMES_DASHBOARD_PORT: "9119"
HERMES_DASHBOARD_PUBLIC_URL: "https://hermes.yourdomain.com"
HERMES_DASHBOARD_BASIC_AUTH_USERNAME: ${DASH_USER}
HERMES_DASHBOARD_BASIC_AUTH_PASSWORD: ${DASH_PASS}
HERMES_DASHBOARD_BASIC_AUTH_SECRET: ${DASH_SECRET}
# OpenAI-compatible API server
API_SERVER_ENABLED: "true"
API_SERVER_HOST: "0.0.0.0" # not 127.0.0.1, or caddy cannot reach it
API_SERVER_PORT: "8642"
API_SERVER_KEY: ${API_SERVER_KEY} # minimum 8 characters
# Model provider
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
HERMES_MODEL: ${HERMES_MODEL}
# Volume ownership
HERMES_UID: "1000"
HERMES_GID: "1000"
deploy:
resources:
limits:
memory: 4G
cpus: "2.0"
networks:
- hermes_net
caddy:
image: caddy:2-alpine
container_name: hermes-caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
- hermes
networks:
- hermes_net
volumes:
caddy_data:
caddy_config:
networks:
hermes_net:
driver: bridge
The deploy.resources.limits block mirrors the limits in Nous Research's own Compose example. The rest is a direct translation of the flags in their docker run examples: the volume, command: gateway run, the two ports, and --shm-size=1g.
Why won't docker compose up work on a fresh install?
Because the data directory has to be initialised interactively first. Nous Research's guide is explicit: create the directory on the host, then start the container interactively to run the setup wizard. Their example is a bare docker run. In Compose it becomes three commands:
- Create the bind mount and give it to the container's user:
mkdir -p ./data && sudo chown -R 1000:1000 ./data
- Run the wizard once.
compose runreuses this service's image, volume and environment, and drops thegateway runcommand:docker compose run --rm hermes setup - Start the stack:
docker compose up -d
Step 2 writes config.yaml and .env into ./data. Skip it and the gateway starts against an unconfigured profile.
The default container UID and GID are 10000. Step 1 uses 1000 because the compose file above overrides them with HERMES_UID and HERMES_GID to match a typical first host user. Pick one convention and keep it; mismatched ownership is the most common first-run failure.
Where does Hermes Agent store its data?
Everything sits under /opt/data in the container, which is ./data on the host with the bind mount above. Nous Research documents the layout:
| Path | Contents |
|---|---|
.env | API keys and secrets |
config.yaml | Profile and gateway configuration |
sessions/ | Conversation history |
memories/ | Persistent memory |
skills/ | Installed and learned skills |
home/ | Per-profile HOME for tools |
logs/ | Runtime logs, including logs/container-boot.log |
State is held in SQLite with FTS5 full-text search, per the project's repository, which lists hermes_state_dbfile.py and hermes_state_fts.py as the modules that handle it. That is the whole reason this stack is one container. No Postgres, no pgvector, no migration step.
One hard rule from the vendor, worth repeating verbatim: "Never run two Hermes gateway containers against the same data directory simultaneously — session files and memory stores are not designed for concurrent write access." If you run a second profile, give it its own data directory and its own API_SERVER_PORT.
The skills/ directory is where the learning loop writes what the agent has worked out. Losing it costs more than losing the sessions.
Backups
Nous Research documents no backup command and no snapshot format, so what follows is our recommendation rather than theirs. Because the state is SQLite, a copy taken while the gateway is writing can be torn. Stop, copy, start:
#!/bin/sh
# hermes-backup.sh — run from the stack directory via host cron
cd /opt/hermes-stack || exit 1
docker compose stop hermes
tar czf "/var/backups/hermes-$(date +%F).tar.gz" ./data
docker compose start hermes
find /var/backups -name 'hermes-*.tar.gz' -mtime +14 -delete
The stop-copy-start window is a few seconds. Ship the tarball off the box afterwards; a backup on the same disk is not a backup.
How do I put the dashboard behind TLS?
Two names, two upstreams. The dashboard is a browser UI on 9119; the API is a machine endpoint on 8642.
# Caddyfile
hermes.yourdomain.com {
reverse_proxy hermes:9119
encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Frame-Options DENY
X-Content-Type-Options nosniff
}
}
api.yourdomain.com {
reverse_proxy hermes:8642
encode gzip
}
Now the part that will stop your first deploy. Hermes has a fail-closed auth gate on the dashboard. Their guide states that if no auth provider is registered and the bind is non-loopback, "the dashboard fails closed at startup with a specific error pointing at the missing env var. There is no longer an escape hatch that serves the dashboard unauthenticated on a public bind." Inside a container the bind is 0.0.0.0, which is non-loopback, so this applies to every containerised deployment.
Register exactly one provider:
| Provider | Variables |
|---|---|
| Basic | HERMES_DASHBOARD_BASIC_AUTH_USERNAME, HERMES_DASHBOARD_BASIC_AUTH_PASSWORD, HERMES_DASHBOARD_BASIC_AUTH_SECRET |
| OAuth | HERMES_DASHBOARD_OAUTH_CLIENT_ID |
| OIDC | HERMES_DASHBOARD_OIDC_ISSUER, HERMES_DASHBOARD_OIDC_CLIENT_ID |
Putting basicauth in the Caddyfile does not satisfy the gate. The check runs inside Hermes, before the request ever reaches your proxy.
Which model do I point it at?
Hermes reads provider credentials from the environment and takes a process-level model override:
| Variable | Purpose |
|---|---|
ANTHROPIC_API_KEY | Anthropic Console credentials |
OPENROUTER_API_KEY | OpenRouter, which the vendor recommends for provider flexibility |
OPENAI_API_KEY and OPENAI_BASE_URL | Any OpenAI-compatible endpoint |
HERMES_MODEL | Override the model at process level |
Use a current model ID. Anthropic's current lineup on 2026-09-09 is claude-opus-5, claude-sonnet-5, claude-fable-5-1 and claude-haiku-4-5-20251001, and from the 4.6 generation onward Anthropic's IDs are dateless snapshots (platform.claude.com model overview). A dated claude-opus-4-7-… string, which an earlier version of this page shipped, is not one of them and will fail on the first call.
For a local backend, Ollama exposes an OpenAI-compatible API on port 11434 under /v1/, with the API key required but ignored:
OPENAI_BASE_URL=http://ollama:11434/v1/
OPENAI_API_KEY=ollama
HERMES_MODEL=llama3.2
Sizing a GPU box for local inference is a separate problem from running this stack. The GPU inference guide covers tiers and VRAM.
Timeouts are generous by default, so do not go hunting for a 30-second limit. HERMES_API_TIMEOUT defaults to 1800 seconds, HERMES_STREAM_READ_TIMEOUT to 120, and HERMES_AGENT_TIMEOUT to 1800.
How much RAM does Hermes Agent need?
Nous Research's Docker guide gives 1 GB memory, 1 CPU core and 500 MB disk as the minimum, and 2–4 GB with 2 cores as the recommendation. They name browser automation with Playwright and Chromium as the memory-hungry feature.
We have not re-measured this release ourselves, and we are not going to publish a number we did not run. The figure this page carried before, 2.1 GB idle, is withdrawn: it was taken against a stack that included a PostgreSQL container Hermes has never required, so it measured the wrong thing.
Practically, the vendor's 4 GB upper recommendation plus room for the OS and Caddy puts a browser-enabled Hermes on a 8 GB box, and a Hermes without browser tools comfortably on 4 GB.
When this stack is the wrong choice
You want a chat UI for a team, not an agent. Hermes is an agent with tool access, a terminal backend and a learning loop. If what you need is a multi-user chat front end over an API key, a chat UI is less machinery. The four-way comparison walks the forks.
You need audit logging and policy enforcement. This stack has neither. HERMES_YOLO_MODE exists to bypass dangerous-command approval prompts, which tells you where the project's defaults sit. For regulated work, start from the NemoClaw policy cookbook instead of bolting guardrails onto this.
You only want messaging bots. Hermes does ship them, and generously: Telegram, Discord, Slack, WhatsApp, Signal, SMS, Email, Matrix, Microsoft Teams, Home Assistant and around twenty more, all from the one gateway process. But if a bot is the whole requirement, the OpenClaw Compose stack is a smaller thing to operate.
You do not need managed hosting. Two containers on a bind mount is not demanding work. A self-managed box and a cron job will run this fine, and it costs a fraction of a managed plan.
Read What Is Hermes Agent? if you are still deciding whether the agent is the right shape for your problem, rather than how to deploy it. As of 2026 the project is under active weekly development, MIT licensed, at 243.7k GitHub stars on 2026-09-09.
No. Hermes keeps its state in SQLite with FTS5 full-text search inside the /opt/data volume, per Nous Research's repository (modules hermes_state_dbfile.py and hermes_state_fts.py), checked 2026-09-09. There is no PostgreSQL dependency, no pgvector extension and no schema migration step. Any Hermes compose file with a postgres service in it is describing a product that does not exist.
Because that image is not published. Nous Research ships Hermes Agent on Docker Hub as nousresearch/hermes-agent. On 2026-09-09, GitHub's container package page for that GHCR path returned 404 and an anonymous pull token was denied. Replace the image line with nousresearch/hermes-agent:latest, or pin a calendar tag such as v2026.9.7.
No, as long as you keep the data directory. Nous Research's Docker guide says to pull the latest image and recreate the container: the data directory is preserved, and the container runs non-interactive config-schema migrations against the mounted config.yaml before starting the gateway. Take a tarball of ./data first anyway, and never run two gateway containers against one data directory while you cut over.
Yes, with its own data directory and its own API_SERVER_PORT. The vendor's guide is explicit that two gateway containers must not share a data directory, because session files and memory stores are not built for concurrent writes. Give the second service a separate bind mount, a distinct API_SERVER_PORT, and a separate Caddy hostname.
Common mistakes and fixes
docker compose pull fails on ghcr.io/nousresearch/hermes-agent.
That path is not published. Nous Research ships the image on Docker Hub as nousresearch/hermes-agent. GitHub's package page for ghcr.io/nousresearch/hermes-agent returned 404 and an anonymous pull token was denied when checked on 2026-09-09. Change the image line to nousresearch/hermes-agent:latest, or pin a calendar tag such as v2026.9.7.
The dashboard container exits at startup with an error naming a missing environment variable.
This is the fail-closed auth gate, not a crash. Hermes refuses to serve the dashboard on a non-loopback bind with no auth provider registered, and inside a container the bind is non-loopback by default. Register one provider: HERMES_DASHBOARD_BASIC_AUTH_USERNAME plus HERMES_DASHBOARD_BASIC_AUTH_PASSWORD and HERMES_DASHBOARD_BASIC_AUTH_SECRET, or the OAuth (HERMES_DASHBOARD_OAUTH_CLIENT_ID) or OIDC (HERMES_DASHBOARD_OIDC_ISSUER, HERMES_DASHBOARD_OIDC_CLIENT_ID) providers. Basic auth on your reverse proxy does not satisfy the gate, because the gate lives inside Hermes.
Caddy returns 502 for the API even though the hermes container is running.
API_SERVER_HOST defaults to 127.0.0.1, which inside a container means the loopback interface of that container only. Caddy sits in a different container and cannot reach it. Set API_SERVER_HOST=0.0.0.0 so the gateway binds the container's network interface, and keep the port unpublished so only Caddy can reach it.
Permission errors writing to the mounted data directory.
The container process runs as UID/GID 10000 by default. Either chown the bind mount to match (chown -R 10000:10000 ./data) or set HERMES_UID and HERMES_GID (aliases PUID and PGID) to your host user's IDs in the compose file.
Browser tools fail or hang inside the container.
Playwright needs shared memory. Docker's 64 MB default is not enough. Nous Research's Docker guide says to add --shm-size=1g; in Compose the equivalent is shm_size: "1g" on the service.