27 KiB
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 |
|
active — SSH контейнер→хост РАБОТАЕТ (фикс uid 1000 применён 2026-09-23) |
|
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-loopmkdir /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 (НЕ менять)
- Закрытый ключ на хосте обязан быть 600 — OpenSSH 10.x (Bookworm/Trixie) отвергает 644.
- Публичный ключ на хост один раз:
ssh-copy-id kraken@<host>. - SSH config обязан лежать в
$HOME/.ssh/config($HOME=/opt/data→/opt/data/.ssh/config). - В
/etc/passwdдолжна быть запись для uid, под которым стартует контейнер. Сейчас uid=1000 без записи. См. план ниже — добавлять в Dockerfile образа, не в живой контейнер (иначе сбросится). - Не ставить 644 на закрытый ключ.
- НЕ использовать
Include ~/.ssh/configв системном/etc/ssh/ssh_config— OpenSSH не резолвит~. - Может быть несколько пар ключей: bind mount приносит хост-ключ, а агент может иметь свой в другом месте.
- Полный 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}}'"
🔁 Регрессия: что делать, если симптом вернулся
Порядок проверки — по вероятности:
- Mount пропал (чаще всего).
etc-passwdне примонтирован → uid 1000 без записи. Проверить:docker inspect hermes-kraken --format '{{range .Mounts}}{{.Destination}} {{end}}'— должен быть/etc/passwd. Если нет — правку compose потеряли, вернуть строки иcompose up -d. - Mount есть, но файл пустой/битый —
config/etc-passwdперезаписан или обрезан. Проверить хвост:tail -3 .../docker/hermes/config/etc-passwd→ должна быть строкаkraken:x:1000…. Лечится повторным созданием (команда в разделе Volumes). - Сменился uid контейнера (например после
docker compose pullнового образа с другимUSER). Тогда строка вetc-passwdне совпадает с фактическим uid. Сверитьidиgetent passwd. - Сменился базовый образ →
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 + пересоздание.
Почему отказались — три блокера, каждый проверен живьём:
- 🔴
docker compose buildнекуда применить. Compose тянет готовыйghcr.io/mallexxx/hermes-kraken:latest(image:, безbuild:). ЛокальныйDockerfileвhermes-build/её не воспроизводит (в нём нетUSER, а uid 1000 приходит именно оттуда). Переключение наbuild:= подмена версии Hermes на другую ревизию → риск регрессии. - 🔴 В ветке про root:
entrypoint.shне спасает. Вся логикаusermod/gosu/chownобёрнута вif [ "$(id -u)" = "0" ]. Контейнер стартует под 1000 → ветка пропускается целиком. - 🔴 Запись в
/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 не меняется):
- Создать копию 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- В
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- Пересоздать:
⚠️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в этой ситуации просто ничего не делает (контейнер уже есть).- Дополнительные монтирования (проверено по факту):
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старые ошибки (напр. Telegramhttpx.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 neededHERMES_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— форк, а НЕ upstreamNousResearch/hermes-agent, как утверждалось ранее). - ⚠️ Неизвестно (уточнить): собирается ли образ локально из
hermes-build/Dockerfileи пушится в GHCR, или GHCR-образ собирается CI в другом репозитории. От этого зависит, где вносить правку uid 1000.