[2026-09-23] eagle: family/how-to/hermes-docker-kraken.md personal/projects/personal-os/hermes-deployment-patterns.md

This commit is contained in:
Alexey Martemyanov committed 2026-09-23 12:45:45 +06:00
1 parent a804c4a7cf
commit 4f1b26fb45
2 files changed
+113 -70

No files matched your search

+98 -64
View File
@@ -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@<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 = нет записи
ssh kraken@<host> "sudo docker exec hermes-kraken getent passwd 10000"
ssh kraken@<host> "sudo docker exec hermes-kraken ssh -o ConnectTimeout=5 -o BatchMode=yes kraken-host 'echo HELLO'" # ожидаем 'No user exists for uid 1000'
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}}] Entrypoint={{.Config.Entrypoint}}'"
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`. Иначе
@@ -220,52 +255,31 @@ ssh kraken@<host> "cd <compose dir> && sudo docker inspect hermes-kraken --forma
`sudo systemd-run --unit=<name> --collect --property=Type=oneshot chown -R ...`, потом проверять
`systemctl is-active <name>`.
## ПЛАН: фикс 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-<HHMMSS>/`.
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
@@ -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 <N>` **до** чтения ключа. Записать запись изнутри нельзя (`/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]].