[2026-09-23] eagle: family/how-to/hermes-docker-kraken.md
This commit is contained in:
1 parent
2e175279a1
commit
2f42fd12e5
1 file changed
+146
-31
@@ -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.
|
||||
Reference in new issue
Block a user