From 4f1b26fb45496ae9f47cc9e89cc01cea0aa2f15a Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Wed, 23 Sep 2026 12:45:45 +0600 Subject: [PATCH] [2026-09-23] eagle: family/how-to/hermes-docker-kraken.md personal/projects/personal-os/hermes-deployment-patterns.md --- family/how-to/hermes-docker-kraken.md | 162 +++++++++++------- .../personal-os/hermes-deployment-patterns.md | 21 ++- 2 files changed, 113 insertions(+), 70 deletions(-) diff --git a/family/how-to/hermes-docker-kraken.md b/family/how-to/hermes-docker-kraken.md index 72c5598e..ccc1a46c 100644 --- a/family/how-to/hermes-docker-kraken.md +++ b/family/how-to/hermes-docker-kraken.md @@ -5,7 +5,7 @@ updated: '2026-09-23' type: tech namespace: personal tags: [hermes, docker, kraken, infra, how-to] -status: active — план фикса uid 1000 согласован, НЕ применён +status: active — SSH контейнер→хост РАБОТАЕТ (фикс uid 1000 применён 2026-09-23) related: - "[[tech/kraken-network]]" - "[[concepts/hermes-deployment-patterns]]" @@ -43,9 +43,12 @@ services: 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:/root/.ssh + - /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 @@ -55,9 +58,13 @@ services: network_mode: host ``` -> ⚠️ **Обновлено 2026-09-23.** Ранее в доке был описан вариант с `build:` — **это устарело**. Сейчас compose тянет **готовый образ** `ghcr.io/mallexxx/hermes-kraken:latest` (собранный из `mallexxx/hermes-agent`, не upstream NousResearch). В compose **нет** строки `user:` — uid **1000 приходит из образа** (проверено: `tini` стартует под uid 1000). +> ✅ **Обновлено 2026-09-23 — фикс применён, SSH работает.** Полный разбор — в разделе +> «✅ SSH контейнер → хост починен 2026-09-23» ниже. Бэкап исходных файлов: +> `/home/kraken/backups/hermes-uid1000-20260923-123803/`. > -> ⚠️ **Третья точка монтирования:** `config/.ssh → /root/.ssh`. При `HOME=/opt/data` SSH этот путь **не читает**. См. раздел SSH ниже. +> ⚠️ Ранее в доке был описан вариант с `build:` — **это устарело**. Сейчас compose тянет **готовый образ** +> `ghcr.io/mallexxx/hermes-kraken:latest` (собранный из `mallexxx/hermes-agent`, не upstream NousResearch). +> uid **1000** приходит из `USER` в образе; с 2026-09-23 он продублирован в compose явно. ## Managing the Container @@ -107,9 +114,18 @@ ENTRYPOINT [ "/usr/bin/tini", "-g", "--", "/opt/hermes/docker/entrypoint.sh" ] | Host path | Container path | Purpose | |-----------|----------------|---------| | `hermes/config/` | `/opt/data` | `HERMES_HOME` — config, sessions, skills, memory | -| `hermes/config/.ssh/` | `/root/.ssh` | ⚠️ SSH-ключи; при `HOME=/opt/data` **не читается** SSH | +| `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) их надо пересоздать: +> ```bash +> 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` не работает. @@ -117,18 +133,20 @@ ENTRYPOINT [ "/usr/bin/tini", "-g", "--", "/opt/hermes/docker/entrypoint.sh" ] **Архитектура (УТОЧНЕНО 2026-09-23 живой проверкой)** ``` -Container (hermes-kraken) - uid=1000 gid=1000 ← из ОБРАЗА (USER), НЕ задан в compose +Container (hermes-kraken) ← состояние ПОСЛЕ фикса 2026-09-23 + uid=1000 gid=1000 ← задан явно в compose (user: "1000:1000") HOME=/opt/data - /etc/passwd содержит ТОЛЬКО: hermes:x:10000:10000::/opt/data:/bin/sh - ← записи для uid 1000 НЕТ ← КОРЕНЬ ПРОБЛЕМЫ - config/.ssh смонтирован в /root/.ssh ← при HOME=/opt/data SSH его НЕ читает - /opt/data/.ssh/ — есть authorized_keys, id_ed25519(600), known_hosts — но НЕТ config + /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 — должен содержать публичный ключ + /home/kraken/.ssh/authorized_keys — публичный ключ + …/docker/hermes/config/etc-passwd + etc-group — root:root, 644 (bind-mount ro) ``` **🔴 Симптом: `No user exists for uid 1000`** @@ -187,19 +205,36 @@ if [ "$(id -u)" = "0" ]; then ... exec gosu hermes "$0" "$@" ; fi 7. Может быть несколько пар ключей: bind mount приносит хост-ключ, а агент может иметь свой в другом месте. 8. Полный mount `/etc/ssh/` перезаписывает и host keys тоже. -**Диагностика по порядку** +**Диагностика по порядку (актуальна и ПОСЛЕ фикса — для регрессий)** ```bash ssh kraken@ "echo OK && hostname" # доступен ли хост ssh kraken@ "sudo docker exec hermes-kraken id" # реальный uid -ssh kraken@ "sudo docker exec hermes-kraken getent passwd 1000" # ← КЛЮЧЕВАЯ проверка, exit=2 = нет записи -ssh kraken@ "sudo docker exec hermes-kraken getent passwd 10000" -ssh kraken@ "sudo docker exec hermes-kraken ssh -o ConnectTimeout=5 -o BatchMode=yes kraken-host 'echo HELLO'" # ожидаем 'No user exists for uid 1000' +ssh kraken@ "sudo docker exec hermes-kraken getent passwd 1000" # ← КЛЮЧЕВАЯ проверка; exit=2 = регрессия (mount отвалился) +ssh kraken@ "sudo docker exec hermes-kraken ssh -o ConnectTimeout=5 -o BatchMode=yes kraken-host 'id; echo OK'" # приёмка ssh kraken@ "sudo docker exec hermes-kraken ls -lan /opt/data/.ssh" # ключ и права ssh kraken@ "ps -o uid,cmd -p \$(sudo docker inspect -f '{{.State.Pid}}' hermes-kraken)" # uid РЕАЛЬНОГО процесса (не exec!) -ssh kraken@ "cd && sudo docker inspect hermes-kraken --format 'User=[{{.Config.User}}] Entrypoint={{.Config.Entrypoint}}'" +ssh kraken@ "cd && 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`. Иначе @@ -220,52 +255,31 @@ ssh kraken@ "cd && sudo docker inspect hermes-kraken --forma `sudo systemd-run --unit= --collect --property=Type=oneshot chown -R ...`, потом проверять `systemctl is-active `. -## ПЛАН: фикс uid 1000 (вариант A, согласован с Alex 2026-09-23) +## ПЛАН: фикс uid 1000 — ⛔ ОТМЕНЁН 2026-09-23 (заменён рабочим решением) -> **Статус: СОГЛАСОВАН, НЕ ПРИМЕНЁН.** Alex выбрал вариант **A** — завести пользователя uid 1000 в образе. +> **Статус: НЕ ПРИМЕНЁН И НЕ БУДЕТ ПРИМЕНЁН.** Оставлено для истории — почему отказались. -**Выбор:** A — заводим `kraken` с uid 1000 **в образе**; `user:` в compose не добавляем (uid 1000 уже -идёт из `USER` в образе). Альтернатива **B** (убрать uid 1000, уйти на `hermes:10000`) отклонена, т.к. -потребовала бы `chown -R` всего `/opt/data` на USB-диске (минуты, риск таймаута). +**Что планировалось (вариант A):** завести `kraken` с uid 1000 **в образе** — правка `Dockerfile` +(`RUN useradd -u 1000 … && USER kraken`), затем `docker compose build --no-cache` + пересоздание. -**Правка Dockerfile** — после `RUN useradd -u 10000 -m -d /opt/data hermes` добавить: -```dockerfile -RUN useradd -u 1000 -g 1000 -m -d /opt/data -s /bin/sh kraken \ - && chown -R kraken:kraken /opt/data -USER kraken -``` -⚠️ Дополнительно проверить `USER` в базовом образе `ghcr.io/mallexxx/hermes-kraken:latest`: uid 1000 уже -там, но **записи в passwd нет** — именно поэтому одно `useradd` (без согласования с `USER`) может -не совпасть. После правки uid процесса и uid в passwd должны совпадать. +**Почему отказались — три блокера, каждый проверен живьём:** -**Вторая часть фикса (SSH-конфиг).** `config/.ssh` монтируется в `/root/.ssh`, а процесс под 1000 с -`HOME=/opt/data` → SSH его не читает, и `~/.ssh/config` отсутствует. Варианты (выбрать при применении): -- либо перевесить mount на `/opt/data/.ssh`, -- либо создать `config/.ssh/config` на хосте (тогда он приедет в `/root/.ssh`, но читаться не будет — - можно дополнительно симлинкнуть или выставить `HOME`), -- либо задать `-F /root/.ssh/config` явно. -**Требует решения Alex при применении.** +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 не менялась).** +Полный разбор и команды — в следующем разделе. -1. **Бэкап:** `Dockerfile`, `docker-compose.yml`, `.env`, `/etc/passwd` контейнера (`docker exec … cat /etc/passwd`) - → `/home/kraken/backups/hermes-uid-fix-20260923-/`. -2. **Правка `Dockerfile`** (см. выше) — через локальную копию, залить обратно (не sed на удалённом хосте). -3. **Пересборка + пересоздание:** `docker compose build` затем `up -d --force-recreate`. - ⚠️ Это **рестарт Hermes на Kraken** — нужен апрув Alex. - ⚠️ Образ сейчас тянется из **ghcr.io/mallexxx/hermes-kraken:latest** — уточнить, собирается ли он - локально (`Dockerfile` в `hermes-build/`) или заливается в GHCR из другого места. Если GHCR — правку - надо вносить в источнике сборки образа, иначе `docker compose pull` перезапишет фикс. -4. **Проверка (приёмка — Alex запускает сам):** - ```bash - sudo docker exec hermes-kraken id - sudo docker exec hermes-kraken getent passwd 1000 # должен вернуть kraken:x:1000:1000 - sudo docker exec hermes-kraken ssh kraken-host 'id; echo OK' # ожидаем uid=1000(kraken) + OK - ``` -5. **Обновить эту доку** по факту применённого фикса (убрать «НЕ ПРИМЕНЁН», записать реальные пути). - -**Открытый вопрос при применении:** второй вариант compose (с `config/.ssh → /root/.ssh`) — оставить -mount как есть и починить `$HOME`, или перевесить на `/opt/data/.ssh`? Alex ещё не решал. +**Урок:** при выборе «править Dockerfile vs обойти» сначала проверять, из чего реально запущен +контейнер (`docker inspect --format '{{.Config.Image}}'` + есть ли `build:` в compose). Правка +Dockerfile, который не участвует в сборке, — нулевой эффект. ## ✅ SSH контейнер → хост починен 2026-09-23 (passwd через bind-mount) @@ -298,22 +312,42 @@ mount как есть и починить `$HOME`, или перевесить > - …/docker/hermes/config/etc-group:/etc/group:ro > - …/docker/hermes/config/.ssh:/opt/data/.ssh # было /root/.ssh — мимо $HOME > ``` -> 3. Пересоздать: `docker compose up -d` (⚠️ **не** `--force-recreate`) — контейнер создан вручную, compose конфликтует по имени. +> 3. Пересоздать: +> ```bash +> 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):** +> **Приёмка (проверено 2026-09-23, Alex подтвердил: «ssh работает»):** > ```bash > 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) ... OK +> # → 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`. -> - `docker compose up -d --force-recreate` падает с `Conflict. The container name "/hermes-kraken" is already in use` — контейнер создан вне compose-проекта. Использовать `docker rm -f hermes-kraken` + `compose up -d`, либо просто `compose up -d`. -> - Правка `entrypoint.sh` (добавить `printf … >> /etc/passwd`) **не работает** — файл root-owned, запись пропускается на `[ -w /etc/passwd ]`. -> - `useradd` в контейнере тоже невозможен по той же причине. +> - `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 diff --git a/personal/projects/personal-os/hermes-deployment-patterns.md b/personal/projects/personal-os/hermes-deployment-patterns.md index 86ee02ab..4a6110b0 100644 --- a/personal/projects/personal-os/hermes-deployment-patterns.md +++ b/personal/projects/personal-os/hermes-deployment-patterns.md @@ -41,13 +41,22 @@ See [[claude-python-cli-proxy]] for full details and token-refresh procedure. ## Pattern 2: Kraken Docker (RPi5) -**Where:** Kraken RPi5, `docker run` (not compose) -**Transport:** Zulip (same instance or separate) -**Model backend:** Gemini or OpenRouter via Hermes gateway -**Autostart:** `--restart unless-stopped` on the container +**Where:** Kraken RPi5, `docker compose` из `docker/hermes-build/` +(⚠️ ранее здесь было «`docker run`, not compose» — **это устарело**, проверено 2026-09-23) +**Transport:** Zulip (same instance or separate) +**Model backend:** Gemini or OpenRouter via Hermes gateway +**Autostart:** `restart: unless-stopped` в compose -**Why docker run, not compose:** explicit restart behavior; avoids accidental -`docker compose down` wipes. Config volume: `~/.hermes` on Kraken host. +**Образ:** `ghcr.io/mallexxx/hermes-kraken:latest` (`image:`, **без** `build:`). uid процесса — **1000** +(из `USER` в образе, продублирован в compose как `user: "1000:1000"`). +Config volume: `…/docker/hermes/config` → `/opt/data` (`HERMES_HOME`). + +⚠️ **Урок (2026-09-23), переносимый на любой Docker-деплой Hermes:** uid, под которым стартует +контейнер, ОБЯЗАН иметь запись в `/etc/passwd`. Иначе `ssh`/`git` внутри контейнера падают с +`No user exists for uid ` **до** чтения ключа. Записать запись изнутри нельзя (`/etc/passwd` +root-owned) → либо bind-mount готового passwd, либо `useradd` в **Dockerfile**, если образ +действительно собирается локально. Проверять `docker inspect --format '{{.Config.Image}}'` + +наличие `build:` в compose **до** правки Dockerfile. Подробности: [[tech/hermes-docker-kraken]]. See [[tech/hermes-docker-kraken]].