Docker implementation of hermes-agent with support for docker terminal backend
  • Shell 58.9%
  • Dockerfile 41.1%
Find a file
Renovate Bot 95617d4df8
All checks were successful
Build Docker Image / build (pull_request) Successful in 35m11s
Build Docker Image / build (push) Successful in 26s
chore(deps): update ghcr.io/astral-sh/uv docker tag to v0.11.14
2026-05-12 18:02:10 +00:00
.forgejo/workflows perf: optimize caching of docker build 2026-05-05 10:15:31 -04:00
docker-compose.yml refactor: improve building and container init script 2026-05-05 08:59:02 -04:00
docker-wrapper.sh feat: improve docker image security and installation paths 2026-03-31 10:49:06 -04:00
Dockerfile chore(deps): update ghcr.io/astral-sh/uv docker tag to v0.11.14 2026-05-12 18:02:10 +00:00
entrypoint.sh refactor: improve building and container init script 2026-05-05 08:59:02 -04:00
env.example refactor: improve building and container init script 2026-05-05 08:59:02 -04:00
healthcheck.sh refactor: improve building and container init script 2026-05-05 08:59:02 -04:00
README.md refactor: improve building and container init script 2026-05-05 08:59:02 -04:00
renovate.json chore(deps): update dependency astral-sh/uv to v0.11.9 2026-05-05 09:08:05 -04:00
TESTING.md perf: optimize caching of docker build 2026-05-05 10:15:31 -04:00

Hermes Agent Host-Docker Image

This repository builds a Hermes gateway image for the host-Docker topology:

  • Hermes runs inside a container.
  • Hermes talks to an external Docker daemon through DOCKER_HOST.
  • Hermes asks that daemon to create sibling sandbox containers for terminal work.

The image keeps Hermes installed with uv pip install -e ".[all]", includes the existing Node and Playwright-based tooling, and preserves the Docker CLI wrapper used to optionally propagate the main container's runtime uid:gid into sandbox docker run and docker create calls.

Security Model

This image is for a trusted operator environment.

  • Access to a Docker unix socket is effectively root-equivalent on the Docker daemon host.
  • Prefer DOCKER_HOST=ssh://user@host when possible so you do not have to mount a local Docker socket into the container.
  • If you do use a unix socket, mount it read-only and keep the trust boundary explicit: Hermes can still ask the daemon to launch privileged sibling containers because that is how the Docker API works.
  • Secrets are not baked into the image. Keep them in runtime environment variables, mounted files, or files seeded into HERMES_HOME after startup.

Why HERMES_HOME Must Match The Host Path

This topology only works when HERMES_HOME is the same absolute path on both sides:

  • the Docker daemon host
  • the Hermes container

Hermes computes bind mounts for sandbox containers from HERMES_HOME, including paths under:

  • HERMES_HOME/sandboxes
  • HERMES_HOME/skills
  • files within HERMES_HOME that Hermes passes through to sibling containers

Docker resolves bind mounts on the daemon host, not inside the Hermes container. Because of that, do not remap the path.

Correct:

environment:
  HERMES_HOME: /home/micah/.hermes
volumes:
  - /home/micah/.hermes:/home/micah/.hermes

Incorrect:

environment:
  HERMES_HOME: /opt/data
volumes:
  - /home/micah/.hermes:/opt/data

Runtime Layout

The image separates persistent Hermes state from container-local writable runtime state.

  • HERMES_HOME defaults to /opt/data
  • HERMES_RUNTIME_HOME defaults to /opt/hermes-runtime
  • HOME defaults to HERMES_RUNTIME_HOME/home
  • XDG_CONFIG_HOME defaults to HERMES_RUNTIME_HOME/config
  • XDG_CACHE_HOME defaults to HERMES_RUNTIME_HOME/cache
  • XDG_STATE_HOME defaults to HERMES_RUNTIME_HOME/state

/opt/hermes is treated as application content. Runtime writes are expected to go to HERMES_HOME or HERMES_RUNTIME_HOME.

The entrypoint:

  • creates missing runtime directories with restrictive permissions
  • creates persistent Hermes directories for cron, sessions, logs, hooks, memories, skills, skins, plans, and workspace data
  • seeds .env, config.yaml, and SOUL.md into HERMES_HOME if they do not exist
  • installs those seeded files with restrictive permissions
  • syncs bundled skills into HERMES_HOME

If you want to persist runtime home or caches across restarts, mount HERMES_RUNTIME_HOME as an additional volume:

environment:
  HERMES_RUNTIME_HOME: /var/lib/hermes-runtime
volumes:
  - /var/lib/hermes-runtime:/var/lib/hermes-runtime

Root And Arbitrary UID:GID Support

The image supports:

  • running as root
  • running as an arbitrary numeric uid:gid
  • running without an /etc/passwd entry for that numeric user

That works because the image avoids depending on /root as the writable home directory and uses explicit HOME and XDG defaults that can be created by any writable runtime user.

If you run non-root:

  • the host-side HERMES_HOME path must already be writable by that uid:gid
  • if DOCKER_HOST is a unix socket, mount that socket and add its owning numeric group with --group-add <socket-gid> or Compose group_add
  • this deployment host uses Docker socket group 970; verify other hosts with stat -c '%g' /var/run/docker.sock

Example Compose snippet:

services:
  hermes-gateway:
    image: hermes-agent:host-docker
    user: "${HERMES_CONTAINER_UID:-0}:${HERMES_CONTAINER_GID:-0}"
    group_add:
      - "${DOCKER_SOCKET_GID:-970}"
    environment:
      HERMES_HOME: ${HERMES_HOME}
      DOCKER_HOST: ${DOCKER_HOST}
    volumes:
      - ${HERMES_HOME}:${HERMES_HOME}
      - ${DOCKER_SOCKET_PATH}:${DOCKER_SOCKET_PATH}:ro

Docker Wrapper Behavior

/usr/local/bin/docker is a wrapper around the real Docker CLI at /usr/local/bin/docker-real.

When HERMES_DOCKER_PROPAGATE_RUNTIME_USER=1 and Hermes did not already pass --user, the wrapper injects:

--user ${HERMES_DOCKER_RUNTIME_UID}:${HERMES_DOCKER_RUNTIME_GID}

for docker run and docker create.

This is useful when sandbox containers need to write files that stay aligned with the same host ownership as the main Hermes container. It is not automatically the least-privilege choice in every environment.

Tradeoff:

  • 1: better host file ownership compatibility for sandbox bind mounts
  • 0: fewer implicit privileges carried into sibling containers, but sandbox writes may land as root or another user depending on Hermes' explicit Docker arguments

The wrapper is conservative:

  • it only affects run and create
  • it does nothing if Hermes already passed --user
  • it ignores malformed HERMES_DOCKER_RUNTIME_UID or HERMES_DOCKER_RUNTIME_GID values

Process And Health Model

The image starts through tini so PID 1 can forward signals and reap child processes created by Hermes, MCP servers, git, npm, and other tools. The healthcheck accounts for that by checking for a running Hermes process or the foreground process supervised by tini; it also verifies that HERMES_HOME is writable and the configured Docker daemon is reachable. If Docker is unreachable, the healthcheck prints the effective user/groups, DOCKER_HOST, and Unix socket ownership details to make group or mount mistakes visible.

Dashboard

Set HERMES_DASHBOARD=1, true, or yes to start hermes dashboard as a background process before the foreground command runs.

  • HERMES_DASHBOARD_HOST: dashboard bind host, default 0.0.0.0
  • HERMES_DASHBOARD_PORT: dashboard bind port, default 9119

When the dashboard binds outside localhost, the entrypoint passes --insecure because Hermes requires that explicit opt-in for externally reachable dashboard servers. Dashboard logs stream to container stdout with a [dashboard] prefix.

Build

Default build:

docker build -t hermes-agent:host-docker .

Override the Hermes source ref:

docker build \
  --build-arg HERMES_REF=main \
  -t hermes-agent:host-docker .

The Dockerfile uses a multi-stage build:

  • source: clones Hermes at the pinned ref
  • uv-source: provides pinned uv and uvx binaries from ghcr.io/astral-sh/uv
  • node-runtime: provides Node and npm
  • docker-cli: provides the Docker CLI used by the host-Docker wrapper
  • builder: installs Python, Hermes extras, Node dependencies, WhatsApp bridge dependencies, and built dashboard/TUI assets
  • final stage: keeps only runtime artifacts plus the Docker CLI, Playwright Chromium, and runtime packages

The build sets npm_config_install_links=false to keep npm workspace/file dependency behavior aligned with the upstream Hermes image and avoid runtime install churn from lockfile drift.

Environment Variables

See env.example for the full set. The most important ones are:

  • HERMES_HOME: required, absolute host path, mounted into the container at that exact path
  • DOCKER_HOST: required, points at the external Docker daemon
  • DOCKER_SOCKET_PATH: required when DOCKER_HOST points at a unix socket mounted by Compose
  • DOCKER_SOCKET_GID: optional helper for non-root unix-socket access, default 970 for this deployment host
  • HERMES_CONTAINER_UID and HERMES_CONTAINER_GID: optional Compose runtime user controls
  • HERMES_DOCKER_PROPAGATE_RUNTIME_USER: optional wrapper toggle, default 1
  • HERMES_DASHBOARD: optional dashboard toggle; accepts 1, true, or yes
  • HERMES_DASHBOARD_HOST and HERMES_DASHBOARD_PORT: optional dashboard bind settings
  • HERMES_RUNTIME_HOME, HOME, XDG_CONFIG_HOME, XDG_CACHE_HOME, XDG_STATE_HOME: optional runtime-writable path overrides

Testing

The acceptance-test flow for this repository is documented in TESTING.md.