[2026-09-23] eagle: family/how-to/hermes-docker-kraken.md

This commit is contained in:
Alexey Martemyanov committed 2026-09-23 12:30:33 +06:00
1 parent 2e175279a1
commit 2f42fd12e5
1 file changed
+146 -31
+146 -31
View File
@@ -1,14 +1,17 @@
---
title: Hermes Docker on Kraken
created: '2026-05-23'
updated: '2026-06-04'
updated: '2026-09-23'
type: tech
namespace: personal
tags: [hermes, docker, kraken, infra, how-to]
status: active — план фикса uid 1000 согласован, НЕ применён
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
@@ -35,16 +38,14 @@ Hermes runs via **`docker compose`** from the `hermes-build/` directory.
```yaml
services:
kraken:
build:
context: .
dockerfile: Dockerfile
image: hermes-kraken:latest
image: ghcr.io/mallexxx/hermes-kraken:latest # ← ГОТОВЫЙ образ, не локальная сборка
container_name: hermes-kraken
stdin_open: true
tty: true
restart: unless-stopped
volumes:
- /srv/.../docker/hermes/config:/opt/data
- /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
- /home/kraken/obsidian:/vault:rw
environment:
- HERMES_ALLOW_ROOT_GATEWAY=1
@@ -54,6 +55,10 @@ 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).
>
> ⚠️ **Третья точка монтирования:** `config/.ssh → /root/.ssh`. При `HOME=/opt/data` SSH этот путь **не читает**. См. раздел SSH ниже.
## Managing the Container
```bash
@@ -89,14 +94,16 @@ docker compose up -d
> Перенесено из удалённого скилла `kraken-omv-maintenance` / `kraken-docker-ssh`. Актуальные пути и фиксы для случая, когда `ssh kraken-host` изнутри контейнера `hermes-kraken` не работает.
**Архитектура**
**Архитектура (УТОЧНЕНО 2026-09-23 живой проверкой)**
```
Container (hermes-kraken)
uid=10000 (dropped from root by entrypoint)
uid=1000 gid=1000 ← из ОБРАЗА (USER), НЕ задан в compose
HOME=/opt/data
~/.ssh/config = /opt/data/.ssh/config (bind mount from host)
/etc/ssh/ssh_config — bind mount (полный /etc/ssh/)
/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
Host (Kraken RPi5)
User kraken (uid=1000) owns /home/kraken/.ssh/
@@ -104,42 +111,150 @@ Host (Kraken RPi5)
/home/kraken/.ssh/authorized_keys — должен содержать публичный ключ
```
**🔴 Симптом: `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. В compose **не задавать** `user:`, `entrypoint:`, `HERMES_UID/GID` — entrypoint сам сбрасывает uid (image defaults).
1. Закрытый ключ на хосте **обязан быть 600** — OpenSSH 10.x (Bookworm/Trixie) отвергает 644.
2. Публичный ключ на хост один раз: `ssh-copy-id kraken@<host>`.
3. Закрытый ключ на хосте **обязан быть 600**: `ssh kraken@<host> "chmod 600 /home/kraken/.ssh/id_ed25519"` — OpenSSH 10.x (Bookworm) отвергает 644.
4. В `/etc/passwd` контейнера должен быть `kraken:x:1000` — не выставлять `user:` в compose.
5. SSH config обязан лежать в `$HOME/.ssh/config` (`$HOME=/opt/data` → `/opt/data/.ssh/config`).
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 ps --filter name=hermes-kraken --format '{{.Names}} {{.Status}}'"
# изнутри контейнера на хост (loopback алиас kraken-host)
ssh kraken@<host> "sudo docker exec hermes-kraken ssh -o ConnectTimeout=5 kraken-host 'echo HELLO'"
# verbose — какие конфиги читает SSH
ssh kraken@<host> "sudo docker exec hermes-kraken ssh -v kraken-host 'echo HELLO' 2>&1 | grep -E 'config|debug1.*/ssh|Host|connect|Permission|authenticate|bad|owner' | head -15"
ssh kraken@<host> "sudo docker exec hermes-kraken getent passwd 1000" # uid 1000 в passwd?
ssh kraken@<host> "sudo docker exec hermes-kraken ls -la /opt/data/.ssh/id_ed25519"
ssh kraken@<host> "sudo docker exec hermes-kraken cat /opt/data/.ssh/config"
ssh kraken@<host> "sudo docker exec hermes-kraken sh -c 'echo HOME=\$HOME; ls -la \"\$HOME/.ssh/config\"'"
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 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}}'"
```
**Pitfalls**
- Не менять `user:` в compose — entrypoint должен стартовать как root, чтобы сделать chown.
- Не chown'ить `/opt/data` на `1000:1000` — entrypoint делает `chown -R hermes:hermes` (uid 10000).
- 🔴 **Главный:** 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 не резолвит `~` в системных Include.
- Если `$HOME=/opt/data`, а конфиг живёт в `/opt/data/home/.ssh/` — SSH его не прочитает (путь не совпадает).
- Может быть несколько пар ключей: bind mount приносит хост-ключ, а агент может иметь свой в `/opt/data/home/.ssh/`.
- passwd сбрасывается при пересоздании контейнера.
- НЕ использовать `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 (вариант A, согласован с Alex 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-диске (минуты, риск таймаута).
**Правка 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. **Бэкап:** `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 ещё не решал.
## Notes
- `network_mode: host` — port 8642 is directly on the Kraken host, no port mapping needed
- `HERMES_ALLOW_ROOT_GATEWAY=1` — required since container runs as root
- `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.