diff --git a/family/how-to/hermes-docker-kraken.md b/family/how-to/hermes-docker-kraken.md index c710ec88..23492b1e 100644 --- a/family/how-to/hermes-docker-kraken.md +++ b/family/how-to/hermes-docker-kraken.md @@ -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@`. -3. Закрытый ключ на хосте **обязан быть 600**: `ssh kraken@ "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@ "echo OK && hostname" # доступен ли хост -ssh kraken@ "sudo docker ps --filter name=hermes-kraken --format '{{.Names}} {{.Status}}'" -# изнутри контейнера на хост (loopback алиас kraken-host) -ssh kraken@ "sudo docker exec hermes-kraken ssh -o ConnectTimeout=5 kraken-host 'echo HELLO'" -# verbose — какие конфиги читает SSH -ssh kraken@ "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@ "sudo docker exec hermes-kraken getent passwd 1000" # uid 1000 в passwd? -ssh kraken@ "sudo docker exec hermes-kraken ls -la /opt/data/.ssh/id_ed25519" -ssh kraken@ "sudo docker exec hermes-kraken cat /opt/data/.ssh/config" -ssh kraken@ "sudo docker exec hermes-kraken sh -c 'echo HOME=\$HOME; ls -la \"\$HOME/.ssh/config\"'" +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 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}}'" ``` **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 ` ещё до чтения ключа. +- ❌ **ОПРОВЕРГНУТО (было в доке как «не 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= --collect --property=Type=oneshot chown -R ...`, потом проверять + `systemctl is-active `. + +## ПЛАН: фикс 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-/`. +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.