🤖HermesBlog
Hermes Official Tutorials · Part 168/9/2026

Tutorial 16: Docker Deployment

Tutorial 16: Docker Deployment — easy-to-understand guide based on official docs

Docker deploy: a locked suitcase

Tutorial 16: Docker Deployment

So you want to run Hermes Agent inside Docker? Great choice! This setup keeps everything clean, portable, and easy to upgrade. Let’s walk through it step by step.

Two Ways Docker Meets Hermes

Before we dive in, it’s worth knowing there are two distinct ways Docker intersects with Hermes Agent:

  1. Running Hermes IN Docker — the agent itself lives inside a container (that’s what we’ll cover here)
  2. Docker as a terminal backend — Hermes runs on your host but executes every command inside a persistent Docker sandbox container that survives across tool calls, /new, and subagents

This tutorial focuses on option 1. The beauty of this approach? All your user data (config, API keys, sessions, skills, memories) lives in a single directory mounted from your host at /opt/data. The image itself is stateless — you can pull a new version and upgrade without losing anything.

Image Channels and Runtime Ownership

Before you pull anything, it helps to know which tag you’re getting:

Image tag Meaning
latest / stable The image accepted by the stable release gate.
main The development image published from main-branch builds.
X.Y.Z A versioned stable image. Use a digest for an exact deployment pin.

The workflow builds and tests amd64 and arm64 images. Stable publication uses the tested image archives rather than rebuilding them. Only the full release promotion moves stable and latest; a main push does not advance those tags.

The image’s Python environment follows pyproject.toml and uv.lock (currently Python 3.14). Its curated extras are not the native desktop bundle’s --all-extras set. It does not include an Electron desktop app.

The TUI uses this same supervised gateway as its local backend, including when no messaging platform is configured. An interactive --tui launch starts the registered s6 gateway automatically and waits for it to become ready. Normal named profiles attach to the root gateway that serves them. Profiles explicitly configured with gateway.standalone: true use their own registered slot. Client startup does not install a service or bypass s6 with an unmanaged process.

Quick Start: First-Time Setup

If this is your first time running Hermes Agent, create a data directory on your host and start the container interactively to run the setup wizard:

mkdir -p ~/.hermes
docker run -it --rm \
  -v ~/.hermes:/opt/data \
  nousresearch/hermes-agent setup

This drops you into the setup wizard, which will prompt you for your API keys and write them to ~/.hermes/.env. You only need to do this once. It’s highly recommended to set up a chat system (like Telegram or Discord) at this point so your gateway has somewhere to send messages.

Pro tip: Inside the container, run hermes setup --portal once — the refresh token persists in your mounted ~/.hermes volume, so you won’t need to do it again. See the Nous Portal docs at https://hermes-agent.nousresearch.com/docs/integrations/nous-portal for details.

⚠️ A Word of Caution About Browser Consoles

Some VPS providers (Hetzner Cloud, and others) offer browser-based consoles that mangle special characters — : may arrive as ;, @ might get mis-rendered, and pasted API keys can get corrupted. Connect over SSH instead (ssh root@<host>) for copy-paste-safe command entry. If you must use the browser console, type commands manually and double-check every :, @, =, and / before hitting Enter.

Running in Gateway Mode

Once configured, run the container in the background as a persistent gateway (Telegram, Discord, Slack, WhatsApp, etc.):

docker run -d \
  --name hermes \
  --restart unless-stopped \
  -v ~/.hermes:/opt/data \
  -p 8642:8642 \
  nousresearch/hermes-agent gateway run

Port 8642 exposes the gateway’s OpenAI-compatible API server and health endpoint. It’s optional if you only use chat platforms, but required if you want the dashboard or external tools to reach the gateway.

Supervised by Default

Here’s something new and important: inside the official Docker image, gateway run is automatically supervised by s6-overlay. If the gateway process crashes, it’s restarted within seconds without losing the container. The dashboard (when enabled) is supervised alongside it.

The gateway run CMD process itself is a sleep infinity heartbeat that keeps the container alive while s6 manages the actual gateway process. So docker stop still shuts everything down cleanly, but docker logs shows the supervised gateway’s output.

To opt out (and get the old “container exit = gateway exit” behavior), pass --no-supervise or set HERMES_GATEWAY_NO_SUPERVISE=1. This is useful for CI smoke tests, but for production, the supervised default is strictly better.

Checking the Supervisor

Want to confirm what’s actually running? Inside the container, hermes status --full reports Manager: s6 (container supervisor). For the raw supervisor view, use:

/command/s6-svstat /run/service/gateway-<name>

One gotcha: /command/ is only on PATH for supervision-tree processes. When you call it from docker exec, pass the absolute path exactly as shown above.

Tool-Loop Hard Stops for Unattended Gateways

One important setting to know about: unattended gateway and cron sessions enable tool-loop hard stops by default through non_interactive_hard_stop_enabled. Interactive CLI, TUI, Desktop, and ACP sessions stay warning-only. If you want to opt an unattended deployment out, set this in your profile’s config.yaml:

tool_loop_guardrails:
  non_interactive_hard_stop_enabled: false

Exposing the API Server

The API server is gated on API_SERVER_ENABLED=true. To expose it beyond 127.0.0.1 inside the container, set API_SERVER_HOST=0.0.0.0 and an API_SERVER_KEY (minimum 8 characters — generate one with openssl rand -hex 32):

docker run -d \
  --name hermes \
  --restart unless-stopped \
  -v ~/.hermes:/opt/data \
  -p 8642:8642 \
  -e API_SERVER_ENABLED=true \
  -e API_SERVER_HOST=0.0.0.0 \
  -e API_SERVER_KEY="$(openssl rand -hex 32)" \
  -e API_SERVER_CORS_ORIGINS='*' \
  nousresearch/hermes-agent gateway run

Security warning: Opening any port on an internet-facing machine is a risk. Don’t do it unless you understand the implications.

Running the Dashboard

The built-in web dashboard runs as a supervised s6-rc service alongside the gateway in the same container. Set HERMES_DASHBOARD=1 to bring it up:

docker run -d \
  --name hermes \
  --restart unless-stopped \
  -v ~/.hermes:/opt/data \
  -p 8642:8642 \
  -p 9119:9119 \
  -e HERMES_DASHBOARD=1 \
  nousresearch/hermes-agent gateway run

The dashboard is supervised by s6 — if it crashes, it restarts automatically after a short backoff. Dashboard output is forwarded to docker logs <container>.

Dashboard Auth Is Mandatory on Public Binds

The dashboard’s auth gate engages automatically when the bind host is non-loopback (the container default is 0.0.0.0) and a DashboardAuthProvider plugin is registered. There are three bundled options: username/password (HERMES_DASHBOARD_BASIC_AUTH_USERNAME + _PASSWORD), OAuth via Nous Portal (HERMES_DASHBOARD_OAUTH_CLIENT_ID), or self-hosted OIDC (HERMES_DASHBOARD_OIDC_ISSUER + _CLIENT_ID).

If no provider is registered and the bind is non-loopback, the dashboard fails closed at startup. The old HERMES_DASHBOARD_INSECURE=1 escape hatch is now a deprecated no-op — it logs a warning and is ignored. Configure a provider, or bind HERMES_DASHBOARD_HOST=127.0.0.1 and reach the dashboard over an SSH tunnel or Tailscale instead.

What’s Inside the Image

The image uses Debian 13.4 and includes:

  • A Python 3.14 environment synchronized from the committed uv.lock, followed by a no-dependency editable install of Hermes.
  • The curated extras all, messaging, otlp, anthropic, bedrock, azure-identity, and matrix. This is not --all-extras.
  • Node.js 26 and npm from the digest-pinned Node source image.
  • PM-pinned uv, full Chromium, FFmpeg, and ripgrep in /opt/hermes/tools.
  • System Git, OpenSSH, Docker CLI, and Chromium shared libraries.
  • Prebuilt TUI/dashboard assets and baked Photon sidecar dependencies.
  • s6-overlay for supervision and zombie-process cleanup.

Chromium is staged through PM, not npx playwright install. The build records its resolved executable in /etc/hermes/chromium-executable-path, which Hermes reads when it launches the browser. PLAYWRIGHT_BROWSERS_PATH names /opt/hermes/tools, outside the data mount. Every image, including the unsuffixed (non--desktop) tags, carries the full Chromium build rather than Playwright’s lighter headless shell: one pinned, checksummed browser serves both headless browsing and headed Bot Screen sessions.

Chromium is staged through PM, not npx playwright install. The build records its resolved executable in /etc/hermes/agent-browser-executable-path. PLAYWRIGHT_BROWSERS_PATH names /opt/hermes/tools, outside the data mount. Every image, including the unsuffixed (non--desktop) tags, carries the full set.

Where the Logs Go

A quick note on log routing: per-profile gateways, the dashboard, and the boot reconciler all send their output to docker logs <container>. So you have one place to check everything.

For an interactive container with terminal input and output (such as docker run -it ... --tui or bash), the foreground command owns the terminal. The entrypoint defaults to S6_LOGGING=2 and S6_CMD_USE_TERMINAL=1 so supervised gateway and dashboard output does not overwrite that screen. Their console output goes to s6’s rotated catch-all log at /run/uncaught-logs/current inside the container. The per-profile gateway file logs and normal Hermes logs remain available at the paths above. The catch-all directory is runtime storage, so it does not survive a container restart or removal.

Headless containers keep forwarding service output to docker logs. Explicit S6_LOGGING and S6_CMD_USE_TERMINAL settings take precedence over these interactive defaults.

A Note on SQLite and Bind Mounts

Hermes keeps sessions in a SQLite database (/opt/data/state.db) opened in WAL journal mode by default. WAL needs shared memory to stay coherent between processes, and bind mounts that cross a VM boundary — virtiofs (Docker Desktop and Podman on macOS, OrbStack) and 9p / drvfs (Docker Desktop on Windows) — don’t provide that. Concurrent writers can silently corrupt the database.

Since v2026.9.14, Hermes handles this for you: a fresh database on such a mount is created in rollback (DELETE) journal mode with a one-time warning, so there’s nothing to do. An existing WAL database is never live-downgraded — instead every process logs a one-time error and hermes doctor flags it. To fix it, either stop all Hermes processes and run a one-time offline conversion, or move your data to a named Docker volume (-v hermes-data:/opt/data), which lives on the VM’s own filesystem and supports WAL normally.

Wrapping Up

That’s it! You now have Hermes Agent running in Docker with persistent data, automatic restarts, and optional dashboard support. The supervised architecture means your gateway stays up even if the process crashes — which is exactly what you want for a production deployment.

Happy building! 🚀

📖 Official Docs

This article is based on the official Hermes Agent documentation:Official docs › user-guide/docker