Skip to content

Hermes: Local → Docker Migration

Goal: Migrate Hermes Agent from a direct host installation (Python venv at ~/.hermes/hermes-agent/) to running inside an official Docker container, with the existing ~/.hermes data directory mounted as the persistent volume.

Architecture: Hermes runs as a Docker container (nousresearch/hermes-agent:latest) in the sepia compose stack. The existing ~/.hermes/ directory on the host is bind-mounted as /opt/data inside the container — no data migration needed, the container reads the same config, .env, sessions, skills, and memories. The host installation (~/.hermes/hermes-agent/ repo + venv + launcher script) is removed. The Ansible playbook that installed host-level Hermes deps is cleaned up.

Tech Stack: Docker Compose (existing sepia pattern), nousresearch/hermes-agent image, gosu-based entrypoint for UID mapping.

State Prior to Migration: - ~/.hermes/config.yaml — fully configured (OpenCode Zen provider, deepseek-v4-flash-free model, personalities, security settings, platform toolsets, etc.) - ~/.hermes/.env — API keys (OPENCODE_ZEN_API_KEY, etc.) and env overrides - ~/.hermes/sessions/, memories/, skills/, logs/ — populated - ~/.hermes/hermes-agent/ — full git checkout + Python venv (the host installation) - /home/user/.local/bin/hermes — bash wrapper that calls the venv - /ansible/sepia/setup_hermes.yml — installs nodejs, npm, ripgrep, ffmpeg, xvfb, fonts, build-essential, python3-dev, libffi-dev, uv onto the host - Host packages: ffmpeg, nodejs 20, npm, ripgrep, xvfb, fonts-noto-color-emoji, fonts-unifont, build-essential, python3-dev, libffi-dev, uv


Task 1: Backup ~/.hermes

Objective: Create a dated backup of the entire Hermes data directory before making changes.

Files: - Create: /media/backups/hermes-pre-docker-2026-05-14.tar.gz

Step 1: Create the backup

cd ~ && tar czf /media/backups/hermes-pre-docker-2026-05-14.tar.gz .hermes/

Step 2: Verify backup integrity

tar tzf /media/backups/hermes-pre-docker-2026-05-14.tar.gz | head -10
echo "---"
tar tzf /media/backups/hermes-pre-docker-2026-05-14.tar.gz | wc -l

Expected: lists files and shows a reasonable count (~hundreds of files).

Step 3: Add cleanup reminder to HEARTBEAT.md

The backup can be pruned after 2 weeks if the migration is stable.


Task 2: Determine host UID and configure UID mapping

Objective: The Docker container runs as the hermes user (UID 10000 by default). When it writes to /opt/data (the bind-mounted ~/.hermes/), files will be owned by UID 10000 unless we map to the host user's UID.

Step 1: Check host user UID

id -u user

Expected: whatever UID the user account has (usually 1000).

Step 2: Plan HERMES_UID/HERMES_GID values

If UID is e.g. 1000, we set HERMES_UID=1000 in the container so the entrypoint remaps the internal hermes user to match. This avoids permission issues when the user edits config.yaml or .env from the host side.


Task 3: Create compose.hermes.yaml

Objective: Add Hermes as a service in the existing sepia compose stack, following the modular compose.<service>.yaml convention.

Files: - Create: /opt/compose.hermes.yaml

Service specification:

services:
  hermes:
    image: nousresearch/hermes-agent:latest
    container_name: hermes
    restart: unless-stopped
    command: sleep infinity
    volumes:
      - /home/user/.hermes:/opt/data
      - /var/run/docker.sock:/var/run/docker.sock  # optional: docker-in-docker
    environment:
      - HERMES_UID=1000       # match host user UID (verify in Task 2)
      - HERMES_GID=1000       # match host user GID
    shm_size: 1g               # required for Playwright Chromium
    deploy:
      resources:
        limits:
          memory: 4G
          cpus: "2.0"

Why command: sleep infinity?

The Hermes Docker entrypoint expects either: 1. No args — runs hermes (interactive CLI) 2. A subcommand — runs hermes <subcommand> (e.g. hermes gateway run) 3. An executable on PATH — runs it directly

By using sleep infinity, we keep the container alive without starting the gateway or CLI (we'll exec into it for interactive use). When the user wants gateway mode later, they can change this to command: gateway run.

Why /var/run/docker.sock?

The official image includes docker-cli. Mounting the Docker socket lets agents inside the container drive the host's Docker daemon (run containers, build images, etc.). This is optional — remove the mount if you don't want that capability.

Step 1: Create the compose file

Write /opt/compose.hermes.yaml with the content above.

Step 2: Verify compose syntax

cd /opt && docker compose config 2>&1 | head -5

Expected: no errors, output shows the configured services.


Task 4: Wire Hermes into the main compose.yaml

Objective: Add the Hermes compose module to the main include list.

Files: - Modify: /opt/compose.yaml — add - compose.hermes.yaml to the include list

Step 1: Edit compose.yaml

Insert - compose.hermes.yaml into the include list (e.g. before # Docs or after # Other).

Step 2: Verify the full config resolves

cd /opt && docker compose config --services 2>&1

Expected: hermes appears in the service list alongside existing services (caddy, homeassistant, etc.).

Step 3: Commit

cd /opt
git add compose.yaml compose.hermes.yaml
git -c user.name="Hermes" -c user.email="hermes@local" commit -m "feat: add Hermes Agent as Docker service"

Task 5: Pull the image and validate the container starts

Objective: Verify the image exists, the container starts, and Hermes is functional.

Step 1: Pull the image

cd /opt && docker compose pull hermes

Expected: pulls nousresearch/hermes-agent:latest from Docker Hub.

Step 2: Start the container

cd /opt && docker compose up -d hermes

Step 3: Verify it's running

docker compose ps hermes
docker logs hermes | tail -20

Expected: container status "Up", logs show "Dropping root privileges" and the entrypoint bootstrap messages (directory creation, etc.).

Note on paths inside the container: The Hermes entrypoint sources the Python venv at startup (source /opt/hermes/.venv/bin/activate), but docker exec starts a fresh process that does NOT inherit the venv's PATH. Always use the full path /opt/hermes/.venv/bin/hermes (or use an interactive shell that sources the venv).

Step 4: Run Hermes interactively to verify config is loaded

docker exec -it hermes /opt/hermes/.venv/bin/hermes doctor

Expected: shows config path, provider, model, and health status all OK.

Step 5: Send a test query

docker exec hermes /opt/hermes/.venv/bin/hermes chat -q "What version of Hermes are you?"

Expected: prints version and model info.


Task 6: Add the Caddy route for the Hermes dashboard (optional)

Objective: If the dashboard will be used later, add a Caddy site config so it's reachable at hermes.uitgeest.veenboer.xyz.

Files: - Create: /opt/caddy/sites/hermes.caddy

Step 1: Create the Caddy site config

hermes.{$SUBDOMAIN}.{$DOMAIN} {
    reverse_proxy hermes:9119
}

Step 2: Restart Caddy

docker compose restart caddy

Step 3: Update docs

Add the port and domain to: - /opt/docs/REFERENCE/network.md (host port, container port, service name) - /opt/docs/REFERENCE/caddy.md (route name, subdomain, target)


Task 7: Clean up the Ansible playbook

Objective: Remove/update the Hermes-specific Ansible playbook. The system deps it installed (nodejs, ripgrep, ffmpeg, xvfb, fonts, build-essential, etc.) were only there to support the local Hermes installation. With Hermes containerized, these deps live inside the image.

Context: Some of these packages may also be used by other things on the host: - nodejs/npm — Hermes LSP originally needed this, but LSP runs inside the container now - ripgrep — general-purpose CLI, may be useful to keep - ffmpeg — general-purpose CLI, may be useful to keep - build-essential, python3-dev, libffi-dev — dev toolchain, may be needed for other Python work - xvfb, fonts-noto-color-emoji, fonts-unifont — exclusively for Playwright browser rendering - uv — Python package manager, only used by Hermes

Step 1: Remove the Ansible playbook

rm /ansible/sepia/setup_hermes.yml

Step 2: Remove exclusively-Hermes host packages (safe to purge)

sudo apt-get remove -y xvfb fonts-noto-color-emoji fonts-unifont
sudo apt-get remove --auto-remove -y uv  # installed via curl, check if package-managed

Check if uv was installed via apt or via the script:

which uv && dpkg -S $(which uv) 2>/dev/null || echo "uv was installed via script, not apt"

If uv was installed via the Ansible curl | sh method, it's at ~/.local/bin/uv and can be removed manually:

rm /home/user/.local/bin/uv /home/user/.local/bin/uvx

Step 3: Leave generally-useful packages (ask user first)

The following packages are generally useful on any Linux workstation and may be used by other things. Rather than removing them, note they're no longer required by Hermes:

  • nodejs, npm — general JS runtime
  • ripgrep — fast grep
  • ffmpeg — media processing
  • build-essential, python3-dev, libffi-dev — development toolchain

Step 4: Commit

cd /ansible
git add -A
git -c user.name="Hermes" -c user.email="hermes@local" commit -m "chore: remove Hermes host deps playbook (migrated to Docker)"

Task 8: Remove the local Hermes installation

Objective: Clean up the host-side Hermes files that are no longer needed — the git checkout, Python venv, launcher wrapper, and any leftover artifacts.

Step 1: Remove the launcher script

rm /home/user/.local/bin/hermes

Step 2: Remove the cloned Hermes repo and its venv

rm -rf /home/user/.hermes/hermes-agent

This removes: - The full git checkout (~200MB) - The Python venv at .venv/ or venv/ - Compiled bytecode caches (__pycache__/) - The .git directory

Step 3: Remove LSP artifacts (if not used elsewhere)

rm -rf /home/user/.hermes/lsp/

Step 4: Verify hermes is no longer accessible from the host

which hermes

Expected: hermes not found — the only way to run Hermes is via docker exec -it hermes /opt/hermes/.venv/bin/hermes.

Step 5: Commit cleanup (to the separate hermes-agent repo if it was tracked, otherwise just the ansible repo)

cd /ansible
git -c user.name="Hermes" -c user.email="hermes@local" commit --allow-empty -m "chore: remove local Hermes installation (migrated to Docker)"

Task 9: Add HEARTBEAT.md entry

Objective: Document the new Hermes deployment approach and add a maintenance check.

Files: - Modify: /opt/docs/HEARTBEAT.md

Step 1: Add a Hermes Docker migration note

Add entry to HEARTBEAT.md:

## 2026-05-14 — Hermes migrated to Docker

Hermes Agent now runs inside a Docker container (`compose.hermes.yaml`) with `~/.hermes` mounted as `/opt/data`.

- Interactive CLI: `docker exec -it hermes /opt/hermes/.venv/bin/hermes`
- Quick query: `docker exec hermes /opt/hermes/.venv/bin/hermes chat -q "<query>"`
- Gateway mode: update `compose.hermes.yaml` command to `gateway run`
- Dashboard: set `HERMES_DASHBOARD=1` in the compose environment

The host installation at `~/.hermes/hermes-agent/` has been removed.
Host packages no longer required: xvfb, fonts-noto-color-emoji, fonts-unifont (uv was also removed).
General packages left in place (not Hermexclusive): nodejs, npm, ripgrep, ffmpeg, build-essential, python3-dev, libffi-dev.

- Backup at /media/backups/hermes-pre-docker-2026-05-14.tar.gz — prune after 2026-05-28

Task 10: Update MEMORY.md and AGENTS.md

Objective: Document the new deployment pattern in the knowledge base.

Files: - Modify: /opt/docs/MEMORY.md - Modify: /opt/AGENTS.md (or just verify it's current)

Step 1: Add Hermes Docker deployment to MEMORY.md

Add a section under the appropriate area (new heading or under the existing Docker section):

### Hermes Agent

Hermes Agent runs as a Docker container (`compose.hermes.yaml`) using `nousresearch/hermes-agent:latest`.
Data at `~/.hermes/` is mounted as `/opt/data` inside the container.
Interactive use: `docker exec -it hermes /opt/hermes/.venv/bin/hermes`.

Step 2: Update AGENTS.md

Verify AGENTS.md correctly reflects the compose structure. Add a note about the UID remapping if needed.

Step 3: Commit

cd /opt
git add docs/
git -c user.name="Hermes" -c user.email="hermes@local" commit -m "docs: add Hermes Docker migration notes"

Rollback Plan

If the Docker container doesn't work as expected:

  1. Restore the compose.yaml: git checkout -- compose.yaml (remove the include entry)
  2. Restart from backup: tar xzf /media/backups/hermes-pre-docker-2026-05-14.tar.gz -C ~/
  3. Reinstall launcher: ln -s ~/.hermes/hermes-agent/venv/bin/hermes ~/.local/bin/hermes
  4. Set terminal.backend: local in config to point at the host installation again

The backup is preserved in /media/backups/ for 2 weeks.