Docker implementation of hermes-agent with support for docker terminal backend
  • Shell 50.8%
  • Dockerfile 49.2%
Find a file
Renovate Bot cd0bcee2c0
All checks were successful
Build Docker Image / build (pull_request) Successful in 27m27s
Build Docker Image / build (push) Successful in 38s
chore(deps): update dependency nousresearch/hermes-agent to v2026.4.16
2026-04-16 21:01:58 +00:00
.forgejo/workflows ci: improve renovate 2026-03-30 15:25:03 -04:00
docker-compose.yml feat: add rootless execution and rootless docker sandboxing 2026-03-30 14:51:59 -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 dependency nousresearch/hermes-agent to v2026.4.16 2026-04-16 21:01:58 +00:00
entrypoint.sh feat: improve docker image security and installation paths 2026-03-31 10:49:06 -04:00
env.example feat: improve docker image security and installation paths 2026-03-31 10:49:06 -04:00
healthcheck.sh feat: add rootless execution and rootless docker sandboxing 2026-03-30 14:51:59 -04:00
README.md feat: improve docker image security and installation paths 2026-03-31 10:49:06 -04:00
renovate.json feat: improve docker image security and installation paths 2026-03-31 10:49:06 -04:00
TESTING.md feat: improve docker image security and installation paths 2026-03-31 10:49:06 -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
  • 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, the container user must also be able to open the socket, usually via --group-add <socket-gid> or Compose group_add

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:-0}"
    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

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-bin: downloads a pinned uv release tarball and verifies its checksum
  • builder: installs Python 3.11, Hermes extras, Node dependencies, WhatsApp bridge dependencies, and the Codex CLI
  • final stage: keeps only runtime artifacts plus the Docker CLI, Playwright Chromium, and runtime packages

Build-time sanity checks verify:

  • the Hermes venv is using Python 3.11.x
  • Node is v22.x
  • the Hermes CLI is present and runnable

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: optional helper for unix-socket mounts
  • DOCKER_SOCKET_GID: optional helper for non-root unix-socket access
  • HERMES_CONTAINER_UID and HERMES_CONTAINER_GID: optional Compose runtime user controls
  • HERMES_DOCKER_PROPAGATE_RUNTIME_USER: optional wrapper toggle, default 1
  • 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.