Files
obsidian-vault/family/how-to/hermes-docker-kraken.md
T

27 KiB
Executable File
Raw Blame History

title, created, updated, type, namespace, tags, status, related
title created updated type namespace tags status related
Hermes Docker on Kraken 2026-05-23 2026-09-23 tech personal
hermes
docker
kraken
infra
how-to
active — SSH контейнер→хост РАБОТАЕТ (фикс uid 1000 применён 2026-09-23)
tech/kraken-network
concepts/hermes-deployment-patterns
tech/hermes-kraken-api
family/how-to/kraken-access
personal/tech/docker-data-root-mount-race

Hermes Docker on Kraken

Key Rule

Hermes runs via docker compose from the hermes-build/ directory.

Directory Layout (on Kraken)

/srv/.../docker/
├── hermes-build/          ← build + run from here
│   ├── Dockerfile         ← clones upstream NousResearch/hermes-agent
│   ├── docker-compose.yml ← canonical compose file
│   ├── config.yaml        ← unused at build time (runtime config is in hermes/config/)
│   └── .env               ← all secrets/API keys (sourced by compose)
└── hermes/
    └── config/            ← mounted as /opt/data (HERMES_HOME) inside container

Compose File (hermes-build/docker-compose.yml)

services:
  kraken:
    image: ghcr.io/mallexxx/hermes-kraken:latest   # ← ГОТОВЫЙ образ, не локальная сборка
    container_name: hermes-kraken
    stdin_open: true
    tty: true
    restart: unless-stopped
    user: "1000:1000"                              # ← добавлено 2026-09-23 (было неявно из USER образа)
    volumes:
      - /srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/docker/hermes/config:/opt/data
      - /srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/docker/hermes/config/.ssh:/opt/data/.ssh   # ← было /root/.ssh
      - /srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/docker/hermes/config/etc-passwd:/etc/passwd:ro
      - /srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/docker/hermes/config/etc-group:/etc/group:ro
      - /home/kraken/obsidian:/vault:rw
    environment:
      - HERMES_ALLOW_ROOT_GATEWAY=1
    env_file:
      - .env
    command: gateway
    network_mode: host

✅ Обновлено 2026-09-23 — фикс применён, SSH работает. Полный разбор — в разделе «✅ SSH контейнер → хост починен 2026-09-23» ниже. Бэкап исходных файлов: /home/kraken/backups/hermes-uid1000-20260923-123803/.

⚠️ Ранее в доке был описан вариант с build: — это устарело. Сейчас compose тянет готовый образ ghcr.io/mallexxx/hermes-kraken:latest (собранный из mallexxx/hermes-agent, не upstream NousResearch). uid 1000 приходит из USER в образе; с 2026-09-23 он продублирован в compose явно.

Managing the Container

cd /srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/docker/hermes-build

docker compose up -d          # start
docker compose down           # stop
docker compose pull           # pull new image (if from registry)
docker compose build          # rebuild image from Dockerfile
docker compose restart        # restart
docker compose logs -f        # follow logs

Image

⚠️ ОБНОВЛЕНО 2026-09-23 — прежнее описание было неверным.

Фактически compose использует готовый образ ghcr.io/mallexxx/hermes-kraken:latest (image:, без build:). Локальный hermes-build/Dockerfile в репозитории есть и в нём прописан git clone --depth=1 https://github.com/mallexxx/hermes-agent.git — это форк Alex, а НЕ upstream NousResearch/hermes-agent, как утверждалось ранее. То есть «not a custom fork» — неверно.

Локальный Dockerfile (актуальное содержимое, ключевое):

FROM ghcr.io/astral-sh/uv:0.11.6-python3.13-trixie AS uv_source
FROM tianon/gosu:1.19-trixie AS gosu_source
FROM debian:13.4                      # Trixie, не Bookworm
ENV HERMES_HOME=/opt/data
RUN useradd -u 10000 -m -d /opt/data hermes    # ← единственный пользователь в образе = uid 10000
...
ENTRYPOINT [ "/usr/bin/tini", "-g", "--", "/opt/hermes/docker/entrypoint.sh" ]

В этом Dockerfile нет USER — значит uid 1000 приходит из ghcr-сборки, а файл в hermes-build/ её не воспроизводит (расхождение образ ↔ Dockerfile). Это надо развести перед правкой — иначе правка Dockerfile не попадёт в работающий контейнер.

🔴 Открытый вопрос (блокирует шаг 2 плана): где собирается ghcr.io/mallexxx/hermes-kraken:latest — CI в репозитории mallexxx/hermes-agent, или локально на Kraken с docker push? Пока не выяснено, docker compose pull может перезаписать любой локальный фикс.

Обновление Hermes: либо docker compose pull && up -d (если образ из GHCR), либо docker compose build --no-cache && up -d (если собирается локально).

Volumes

Host path Container path Purpose
hermes/config/ /opt/data HERMES_HOME — config, sessions, skills, memory
hermes/config/.ssh/ /opt/data/.ssh ✅ SSH-ключи — перевешено 2026-09-23 (было /root/.ssh, при HOME=/opt/data не читалось)
hermes/config/etc-passwd /etc/passwd (ro) ✅ создан 2026-09-23 — запись kraken:x:1000 для uid контейнера
hermes/config/etc-group /etc/group (ro) ✅ создан 2026-09-23 — kraken:x:1000:
/home/kraken/obsidian /vault Obsidian vault read/write access

ℹ️ etc-passwd/etc-group — статичные снимки файлов образа + одна добавленная строка. При смене базового образа (новые uid) их надо пересоздать:

cd /srv/dev-disk-by-uuid-49e8f586-.../docker/hermes/config
sudo docker exec hermes-kraken cat /etc/passwd > /tmp/p && printf 'kraken:x:1000:1000::/opt/data:/bin/sh\n' >> /tmp/p && sudo cp /tmp/p etc-passwd

SSH из контейнера → хост (диагностика и фиксы)

Перенесено из удалённого скилла kraken-omv-maintenance / kraken-docker-ssh. Актуальные пути и фиксы для случая, когда ssh kraken-host изнутри контейнера hermes-kraken не работает.

Архитектура (УТОЧНЕНО 2026-09-23 живой проверкой)

Container (hermes-kraken)                       ← состояние ПОСЛЕ фикса 2026-09-23
  uid=1000 gid=1000        ← задан явно в compose (user: "1000:1000")
  HOME=/opt/data
  /etc/passwd — bind-mount из config/etc-passwd:
      hermes:x:10000:10000::/opt/data:/bin/sh
      kraken:x:1000:1000::/opt/data:/bin/sh     ← ✅ добавлено 2026-09-23
  config/.ssh → /opt/data/.ssh                  ← ✅ перевешено с /root/.ssh
  /opt/data/.ssh/ — authorized_keys, id_ed25519(600), config, known_hosts, kraken-backup-push

Host (Kraken RPi5)
  User kraken (uid=1000) owns /home/kraken/.ssh/
  /home/kraken/.ssh/id_ed25519 — закрытый ключ (обязательно 600)
  /home/kraken/.ssh/authorized_keys — публичный ключ
  …/docker/hermes/config/etc-passwd + etc-group — root:root, 644 (bind-mount ro)

🔴 Симптом: No user exists for uid 1000

$ sudo docker exec hermes-kraken ssh kraken-host "echo HELLO"
No user exists for uid 1000

Это НЕ про ключ. Это getpwuid(euid) вернул NULL внутри ssh. Порядок работы ssh: getpwuid(euid) → $HOME → $HOME/.ssh/config → $HOME/.ssh/id_* → коннект → аутентификация. Падает на первом шаге — до чтения ключа. Пересоздание/копирование ключа не помогает.

Почему: в образе создан только hermes с uid 10000, а контейнер запущен под uid 1000 (из USER в образе) → для 1000 в passwd нет записи.

Как это выглядит в диагностике:

sudo docker exec hermes-kraken id            # uid=1000 gid=1000
sudo docker exec hermes-kraken getent passwd 1000   # пусто, exit=2
sudo docker exec hermes-kraken getent passwd 10000  # hermes:x:10000:10000::/opt/data:/bin/sh

⚠️ Питфолл диагностики: sudo docker exec по умолчанию даёт uid 0, и getpwuid(0) тоже находит hermes (по home=/opt/data). Поэтому проверка «изнутри exec» выглядит частично рабочей, а реальный процесс под 1000 — падает. Проверять именно getent passwd 1000.

История (не повторять ошибок)

  • 2026-06-17: та же ошибка. Обошли копированием ключа в /opt/data/home/.ssh/id_ed25519 (владелец 1000, mode 600) и вызовом с явным -i. Это костыль: $HOME и место ключа разъезжаются, любой новый ssh без -i ломается снова. Правильного фикса тогда не сделали.
  • 2026-09-20: chown 1000:1000 на хосте для docker/hermes/config (был владелец 10000:10000, mode 700 → crash-loop mkdir /opt/data: Permission denied). Это правка владельца файлов, а не uid контейнера. uid контейнера остался 1000.
  • 2026-09-23: разбор повторного симптома → найден корень: отсутствие записи uid 1000 в passwd образа.

Почему entrypoint.sh не спасает

Вся логика сброса прав (gosu hermes, usermod -u, chown -R hermes:hermes /opt/data) обёрнута в

if [ "$(id -u)" = "0" ]; then ... exec gosu hermes "$0" "$@" ; fi

Контейнер стартует под uid 1000, а не под root → вся ветка пропускается целиком. usermod -u "$HERMES_UID" hermes внутри неё недостижим без запуска контейнера от root. В hermes-build/.env HERMES_UID/HERMES_GID не заданы.

Fixed points (НЕ менять)

  1. Закрытый ключ на хосте обязан быть 600 — OpenSSH 10.x (Bookworm/Trixie) отвергает 644.
  2. Публичный ключ на хост один раз: ssh-copy-id kraken@<host>.
  3. SSH config обязан лежать в $HOME/.ssh/config ($HOME=/opt/data → /opt/data/.ssh/config).
  4. В /etc/passwd должна быть запись для uid, под которым стартует контейнер. Сейчас uid=1000 без записи. См. план ниже — добавлять в Dockerfile образа, не в живой контейнер (иначе сбросится).
  5. Не ставить 644 на закрытый ключ.
  6. НЕ использовать Include ~/.ssh/config в системном /etc/ssh/ssh_config — OpenSSH не резолвит ~.
  7. Может быть несколько пар ключей: bind mount приносит хост-ключ, а агент может иметь свой в другом месте.
  8. Полный mount /etc/ssh/ перезаписывает и host keys тоже.

Диагностика по порядку (актуальна и ПОСЛЕ фикса — для регрессий)

ssh kraken@<host> "echo OK && hostname"                                  # доступен ли хост
ssh kraken@<host> "sudo docker exec hermes-kraken id"                    # реальный uid
ssh kraken@<host> "sudo docker exec hermes-kraken getent passwd 1000"    # ← КЛЮЧЕВАЯ проверка; exit=2 = регрессия (mount отвалился)
ssh kraken@<host> "sudo docker exec hermes-kraken ssh -o ConnectTimeout=5 -o BatchMode=yes kraken-host 'id; echo OK'"  # приёмка
ssh kraken@<host> "sudo docker exec hermes-kraken ls -lan /opt/data/.ssh"  # ключ и права
ssh kraken@<host> "ps -o uid,cmd -p \$(sudo docker inspect -f '{{.State.Pid}}' hermes-kraken)"  # uid РЕАЛЬНОГО процесса (не exec!)
ssh kraken@<host> "cd <compose dir> && sudo docker inspect hermes-kraken --format 'User=[{{.Config.User}}] Mounts={{range .Mounts}}{{.Destination}} {{end}}'"

🔁 Регрессия: что делать, если симптом вернулся

Порядок проверки — по вероятности:

  1. Mount пропал (чаще всего). etc-passwd не примонтирован → uid 1000 без записи. Проверить: docker inspect hermes-kraken --format '{{range .Mounts}}{{.Destination}} {{end}}' — должен быть /etc/passwd. Если нет — правку compose потеряли, вернуть строки и compose up -d.
  2. Mount есть, но файл пустой/битый — config/etc-passwd перезаписан или обрезан. Проверить хвост: tail -3 .../docker/hermes/config/etc-passwd → должна быть строка kraken:x:1000…. Лечится повторным созданием (команда в разделе Volumes).
  3. Сменился uid контейнера (например после docker compose pull нового образа с другим USER). Тогда строка в etc-passwd не совпадает с фактическим uid. Сверить id и getent passwd.
  4. Сменился базовый образ → etc-passwd устарел (в нём старый набор uid). Пересоздать из docker exec … cat /etc/passwd + добавить свою строку.

⚠️ Порядок восстановления: НЕ трогать ключи и НЕ пересоздавать образ — симптом No user exists for uid N всегда про passwd, никогда про ключ.

Pitfalls

  • 🔴 Главный: uid, под которым стартует контейнер, ОБЯЗАН иметь запись в /etc/passwd. Иначе любой ssh внутри контейнера падает с No user exists for uid <N> ещё до чтения ключа.
  • ❌ ОПРОВЕРГНУТО (было в доке как «не chown'ить на 1000:1000»): формулировка неверна. Правка владельца /opt/data — это про права файлов, не про uid. Реальный инвариант: uid процесса == владелец /opt/data. При фиксе 2026-09-20 оба и привели к 1000. Держать их согласованными.
  • ❌ ОПРОВЕРГНУТО (было «в compose не задавать user:; entrypoint сам сбросит uid»): entrypoint сбрасывает права только если стартовал от root (if [ "$(id -u)" = "0" ]). Контейнер стартует под 1000 → ветка пропускается. Утверждение верно лишь для root-старта.
  • ❌ ОПРОВЕРГНУТО (было «passwd сбрасывается при пересоздании контейнера»): passwd из образа, пересоздание контейнера его не трогает. Он сбрасывается при пересборке образа — поэтому и надо править Dockerfile, а не живой контейнер.
  • Не ставить 644 на закрытый ключ.
  • НЕ использовать Include ~/.ssh/config в системном /etc/ssh/ssh_config — OpenSSH не резолвит ~.
  • Полный mount /etc/ssh/ перезаписывает и host keys тоже.
  • chown -R по большому дереву на медленном USB-HDD не влезает в SSH-таймаут → запускать через sudo systemd-run --unit=<name> --collect --property=Type=oneshot chown -R ..., потом проверять systemctl is-active <name>.

ПЛАН: фикс uid 1000 — ⛔ ОТМЕНЁН 2026-09-23 (заменён рабочим решением)

Статус: НЕ ПРИМЕНЁН И НЕ БУДЕТ ПРИМЕНЁН. Оставлено для истории — почему отказались.

Что планировалось (вариант A): завести kraken с uid 1000 в образе — правка Dockerfile (RUN useradd -u 1000 … && USER kraken), затем docker compose build --no-cache + пересоздание.

Почему отказались — три блокера, каждый проверен живьём:

  1. 🔴 docker compose build некуда применить. Compose тянет готовый ghcr.io/mallexxx/hermes-kraken:latest (image:, без build:). Локальный Dockerfile в hermes-build/ её не воспроизводит (в нём нет USER, а uid 1000 приходит именно оттуда). Переключение на build: = подмена версии Hermes на другую ревизию → риск регрессии.
  2. 🔴 В ветке про root: entrypoint.sh не спасает. Вся логика usermod/gosu/chown обёрнута в if [ "$(id -u)" = "0" ]. Контейнер стартует под 1000 → ветка пропускается целиком.
  3. 🔴 Запись в /etc/passwd изнутри невозможна. Файл root:root, процесс uid 1000 → touch /etc/passwd = Permission denied. Ни useradd, ни printf >> /etc/passwd из entrypoint.sh не сработают (блок молча пропускался на [ -w /etc/passwd ]).

Что сделали вместо — bind-mount passwd/group (без пересборки, версия Hermes не менялась). Полный разбор и команды — в следующем разделе.

Урок: при выборе «править Dockerfile vs обойти» сначала проверять, из чего реально запущен контейнер (docker inspect --format '{{.Config.Image}}' + есть ли build: в compose). Правка Dockerfile, который не участвует в сборке, — нулевой эффект.

✅ SSH контейнер → хост починен 2026-09-23 (passwd через bind-mount)

Симптом: docker exec hermes-kraken ssh kraken-host "…" → No user exists for uid 1000. Старые доки называли это «passwd сбрасывается» — на деле причина глубже, см. ниже.

Проверенный диагноз (сырьё):

  • Контейнер стартует под uid 1000, заданным USER в образе (ps -o uid,pid,cmd → PID 1 = tini под uid 1000). В compose строки user: НЕ было.
  • /etc/passwd образа создаёт только hermes:x:10000 (RUN useradd -u 10000 -m -d /opt/data hermes в Dockerfile). Записи для uid 1000 нет.
  • ssh падает на getpwuid(euid) до чтения ~/.ssh/config и ключа → пересоздание ключа не помогает.
  • /etc/passwd принадлежит root:root, процесс uid 1000 → touch /etc/passwd = Permission denied. Записать запись изнутри контейнера невозможно. Оригинальный entrypoint.sh поэтому и оборачивал всё в if [ "$(id -u)" = "0" ], а gosu hermes / usermod / chown для uid 1000 не выполняются вообще.
  • .ssh монтировался в /root/.ssh (недоступен под uid 1000, HOME=/opt/data) — ключи до SSH не доезжали.

Фикс (2 файла, пересборки образа НЕ требует → версия Hermes не меняется):

  1. Создать копию passwd/group с записью для uid 1000:
    cd /srv/dev-disk-by-uuid-49e8f586-.../docker/hermes/config
    sudo docker exec hermes-kraken cat /etc/passwd > /tmp/passwd.new
    printf 'kraken:x:1000:1000::/opt/data:/bin/sh\n' >> /tmp/passwd.new
    sudo docker exec hermes-kraken cat /etc/group  > /tmp/group.new
    printf 'kraken:x:1000:\n' >> /tmp/group.new
    sudo cp /tmp/passwd.new etc-passwd && sudo cp /tmp/group.new etc-group
    sudo chown root:root etc-passwd etc-group && sudo chmod 644 etc-passwd etc-group
    
  2. В hermes-build/docker-compose.yml:
    user: "1000:1000"                                    # явно, раньше приходило из USER образа
    volumes:
      - …/docker/hermes/config/etc-passwd:/etc/passwd:ro
      - …/docker/hermes/config/etc-group:/etc/group:ro
      - …/docker/hermes/config/.ssh:/opt/data/.ssh       # было /root/.ssh — мимо $HOME
    
  3. Пересоздать:
    cd /srv/dev-disk-by-uuid-49e8f586-.../docker/hermes-build
    sudo docker rm -f hermes-kraken && sudo docker compose up -d
    # Порядок подтверждён рабочим 2026-09-23.
    
    ⚠️ docker compose up -d --force-recreate падает с Conflict. The container name "/hermes-kraken" is already in use — контейнер создан вне compose-проекта, поэтому compose не находит его как «свой». Поэтому и нужен явный docker rm -f. ⚠️ docker compose up -d без rm -f в этой ситуации просто ничего не делает (контейнер уже есть).
  4. Дополнительные монтирования (проверено по факту): config/.ssh при этом указывать на /opt/data/.ssh, а не /root/.ssh.

Приёмка (проверено 2026-09-23, Alex подтвердил: «ssh работает»):

ssh kraken 'sudo docker exec hermes-kraken sh -c "getent passwd 1000; ssh kraken-host \"id; echo OK\""'
# → kraken:x:1000:1000::/opt/data:/bin/sh
# → uid=1000(kraken) gid=1000(kraken) ... kraken ... HELLO_OK

Результат: SSH из контейнера hermes-kraken → хост Kraken работает без -i и без явных опций. Ключи, права и конфиг трогать не потребовалось — только passwd/group и точка монтирования .ssh.

Что НЕ потребовалось (важно для будущих сессий):

  • Пересборка образа (docker compose build) — НЕ делалась, версия Hermes не менялась.
  • Правка Dockerfile — НЕ делалась (см. отменённый план выше).
  • Пересоздание/копирование ключей — НЕ делалось и не поможет (симптом не про ключ).

Питфоллы:

  • chown/cp в hermes-build/ и docker/hermes/config/ требуют sudo — каталог принадлежит root, обычный cp даёт Permission denied. Это ловилось живьём при заливке compose.
  • Правка entrypoint.sh (добавить printf … >> /etc/passwd) не работает — файл root-owned, запись молча пропускается на [ -w /etc/passwd ]. Блок остался в файле внутри контейнера как безвредный no-op; при пересоздании контейнера он и так исчезнет (файл берётся из образа). Копия исходника: backups/hermes-uid1000-20260923-123803/entrypoint.sh.
  • useradd в контейнере тоже невозможен по той же причине (root-owned /etc/passwd).
  • etc-passwd — статичный снимок passwd образа. При смене базового образа (новые uid) файл надо пересоздать.
  • /etc/group тоже понадобился — без него id показывает gid=1000 без имени группы.
  • Не путать «контейнер поднялся, но внутри своя ошибка» с гонкой docker↔mount (см. personal/tech/docker-data-root-mount-race.md).
  • 🔴 Логи читать только из ТЕКУЩЕГО контейнера. После docker rm -f старые ошибки (напр. Telegram httpx.ConnectError) остаются в буфере прошлого контейнера. Я процитировал их как актуальные — Alex опроверг: Telegram работал. Проверять docker logs hermes-kraken 2>&1 | grep -ic <что> на новом контейнере.

Notes

  • network_mode: host — port 8642 is directly on the Kraken host, no port mapping needed
  • HERMES_ALLOW_ROOT_GATEWAY=1 — ⚠️ исправлено 2026-09-23: контейнер НЕ работает под root, он стартует под uid 1000 (tini под 1000, проверено). Переменная остаётся в окружении как историческая настройка; фактический root-старт возможен только если явно запустить под uid 0.
  • All secrets in hermes-build/.env (not committed)
  • Образ: ghcr.io/mallexxx/hermes-kraken:latest (собран из mallexxx/hermes-agent — форк, а НЕ upstream NousResearch/hermes-agent, как утверждалось ранее).
  • ⚠️ Неизвестно (уточнить): собирается ли образ локально из hermes-build/Dockerfile и пушится в GHCR, или GHCR-образ собирается CI в другом репозитории. От этого зависит, где вносить правку uid 1000.