- Shell 58.9%
- Dockerfile 41.1%
| .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
- creates persistent Hermes directories for cron, sessions, logs, hooks, memories, skills, skins, plans, and workspace data
- 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, mount that socket and add its owning numeric group with--group-add <socket-gid>or Composegroup_add - this deployment host uses Docker socket group
970; verify other hosts withstat -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 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
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, default0.0.0.0HERMES_DASHBOARD_PORT: dashboard bind port, default9119
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 refuv-source: provides pinneduvanduvxbinaries fromghcr.io/astral-sh/uvnode-runtime: provides Node and npmdocker-cli: provides the Docker CLI used by the host-Docker wrapperbuilder: 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 pathDOCKER_HOST: required, points at the external Docker daemonDOCKER_SOCKET_PATH: required whenDOCKER_HOSTpoints at a unix socket mounted by ComposeDOCKER_SOCKET_GID: optional helper for non-root unix-socket access, default970for this deployment hostHERMES_CONTAINER_UIDandHERMES_CONTAINER_GID: optional Compose runtime user controlsHERMES_DOCKER_PROPAGATE_RUNTIME_USER: optional wrapper toggle, default1HERMES_DASHBOARD: optional dashboard toggle; accepts1,true, oryesHERMES_DASHBOARD_HOSTandHERMES_DASHBOARD_PORT: optional dashboard bind settingsHERMES_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.