- Shell 50.8%
- Dockerfile 49.2%
| .forgejo/workflows | ||
| docker-compose.yml | ||
| docker-wrapper.sh | ||
| Dockerfile | ||
| entrypoint.sh | ||
| env.example | ||
| healthcheck.sh | ||
| README.md | ||
| renovate.json | ||
| TESTING.md | ||
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@hostwhen 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_HOMEafter 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/sandboxesHERMES_HOME/skills- files within
HERMES_HOMEthat 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_HOMEdefaults to/opt/dataHERMES_RUNTIME_HOMEdefaults to/opt/hermes-runtimeHOMEdefaults toHERMES_RUNTIME_HOME/homeXDG_CONFIG_HOMEdefaults toHERMES_RUNTIME_HOME/configXDG_CACHE_HOMEdefaults toHERMES_RUNTIME_HOME/cacheXDG_STATE_HOMEdefaults toHERMES_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, andSOUL.mdintoHERMES_HOMEif 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/passwdentry 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_HOMEpath must already be writable by thatuid:gid - if
DOCKER_HOSTis a unix socket, the container user must also be able to open the socket, usually via--group-add <socket-gid>or Composegroup_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 mounts0: 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
runandcreate - it does nothing if Hermes already passed
--user - it ignores malformed
HERMES_DOCKER_RUNTIME_UIDorHERMES_DOCKER_RUNTIME_GIDvalues
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 refuv-bin: downloads a pinneduvrelease tarball and verifies its checksumbuilder: 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 pathDOCKER_HOST: required, points at the external Docker daemonDOCKER_SOCKET_PATH: optional helper for unix-socket mountsDOCKER_SOCKET_GID: optional helper for non-root unix-socket accessHERMES_CONTAINER_UIDandHERMES_CONTAINER_GID: optional Compose runtime user controlsHERMES_DOCKER_PROPAGATE_RUNTIME_USER: optional wrapper toggle, default1HERMES_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.