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

363 lines
27 KiB
Markdown
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Hermes Docker on Kraken
created: '2026-05-23'
updated: '2026-09-23'
type: tech
namespace: personal
tags: [hermes, docker, kraken, infra, how-to]
status: active — SSH контейнер→хост РАБОТАЕТ (фикс uid 1000 применён 2026-09-23)
related:
- "[[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`)
```yaml
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
```bash
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` (актуальное содержимое, ключевое):**
```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) их надо пересоздать:
> ```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` не работает.
**Архитектура (УТОЧНЕНО 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 нет записи.
Как это выглядит в диагностике:
```bash
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`) обёрнута в
```bash
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 тоже.
**Диагностика по порядку (актуальна и ПОСЛЕ фикса — для регрессий)**
```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 = регрессия (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:
> ```bash
> 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`:
> ```yaml
> 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. Пересоздать:
> ```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, 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) 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.