--- 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@`. 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 exec hermes-kraken id" # реальный uid ssh kraken@ "sudo docker exec hermes-kraken getent passwd 1000" # ← КЛЮЧЕВАЯ проверка; exit=2 = регрессия (mount отвалился) ssh kraken@ "sudo docker exec hermes-kraken ssh -o ConnectTimeout=5 -o BatchMode=yes kraken-host 'id; echo OK'" # приёмка 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}}] 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 ` ещё до чтения ключа. - ❌ **ОПРОВЕРГНУТО (было в доке как «не 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= --collect --property=Type=oneshot chown -R ...`, потом проверять `systemctl is-active `. ## ПЛАН: фикс 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.