Hermes Agent — Docker

Docker пересекается с Hermes Agent двумя разными способами:

  1. Запуск Hermes ВНУТРИ Docker — сам агент работает внутри контейнера (этому и посвящена страница);
  2. Docker как терминальный бэкенд — агент работает на хосте, но каждую команду выполняет в одной постоянной Docker-песочнице. Этот контейнер живёт между вызовами инструментов, переживает /new и субагентов всё время работы процесса Hermes (см. Конфигурация → Docker Backend).

Здесь разбираем первый вариант. Контейнер хранит все пользовательские данные (конфигурацию, API-ключи, сессии, навыки, память) в одной директории, примонтированной с хоста на /opt/data. Сам образ не хранит состояние — его можно обновить, просто скачав новую версию, и при этом не потерять ни одной настройки.

Быстрый старт

Запускаете Hermes Agent впервые? Создайте на хосте директорию для данных и запустите контейнер в интерактивном режиме, чтобы пройти мастер настройки:

осторожно
Не используйте браузерные VPS-консоли для команд установки

Некоторые VPS-провайдеры (Hetzner Cloud и ряд других) предлагают браузерную консоль для управления хостами. Такие консоли неправильно передают спецсимволы: : может прийти как ;, @ отрисоваться криво, а с не-английскими раскладками всё ещё хуже. В результате тихо портятся аргументы docker run вроде -v ~/.hermes:/opt/data, -e KEY=value, а заодно вставленные API-ключи и токены.

Подключайтесь по SSH (ssh root@<host>) — так копипаст безопасен. Если без браузерной консоли никак, набирайте команды руками, а не вставляйте, и перед нажатием Enter перепроверяйте в результате каждый :, @, = и /.

mkdir -p ~/.hermes
docker run -it --rm \
  -v ~/.hermes:/opt/data \
  nousresearch/hermes-agent setup

Команда открывает мастер настройки: он запросит ваши API-ключи и запишет их в ~/.hermes/.env. Проделать это нужно один раз. Здесь же стоит сразу подключить чат-систему, с которой будет работать шлюз, — настоятельно рекомендуем не откладывать.

совет

Внутри контейнера один раз выполните hermes setup --portal — refresh-токен сохранится в примонтированном томе ~/.hermes. См. Nous Portal.

Режим шлюза

После настройки запустите контейнер в фоне как постоянный шлюз (Telegram, Discord, Slack, WhatsApp и т. д.):

docker run -d \
  --name hermes \
  --restart unless-stopped \
  -v ~/.hermes:/opt/data \
  -p 8642:8642 \
  nousresearch/hermes-agent gateway run

Порт 8642 открывает OpenAI-совместимый API-сервер шлюза и эндпоинт состояния. Если вы пользуетесь только чат-платформами (Telegram, Discord и т. п.), порт не нужен, но для доступа к шлюзу из дашборда или внешних инструментов он обязателен.

совет
Шлюз работает под присмотром супервизора

В официальном Docker-образе gateway run автоматически контролируется s6-overlay: если процесс шлюза падает, его перезапускают за пару секунд, не теряя контейнер; дашборд (при установленном HERMES_DASHBOARD=1) контролируется рядом с ним. Сам CMD-процесс gateway run — это heartbeat sleep infinity, который держит контейнер живым, пока s6 управляет настоящим процессом шлюза. Поэтому docker stop по-прежнему всё корректно гасит, а docker logs показывает вывод контролируемого шлюза.

В docker logs вы увидите однострочную пометку, подтверждающую переход. Чтобы отказаться от такого поведения и вернуть прежнюю семантику «шлюз — главный процесс контейнера, выход шлюза = выход контейнера», передайте --no-supervise или задайте HERMES_GATEWAY_NO_SUPERVISE=1. Отказ пригодится для CI-смоук-тестов, которым нужно, чтобы контейнер завершался с кодом выхода шлюза; для боевых развёртываний вариант под присмотром однозначно лучше.

Всё это работает только для образа на основе s6. Старые образы (на tini) по-прежнему запускают gateway run как основной процесс на переднем плане.

заметка
Куда уходят логи шлюза

Полную карту маршрутов (шлюзы по профилям, дашборд, загрузочный реконсилятор, общий для контейнера docker logs) смотрите в разделе Куда уходят логи ниже.

Учтите: API-сервер включается только при API_SERVER_ENABLED=true. Чтобы открыть его за пределы 127.0.0.1 внутри контейнера, дополнительно задайте API_SERVER_HOST=0.0.0.0 и API_SERVER_KEY (минимум 8 символов — сгенерируйте через openssl rand -hex 32). Пример:

docker run -d \
  --name hermes \
  --restart unless-stopped \
  -v ~/.hermes:/opt/data \
  -p 8642:8642 \
  -e API_SERVER_ENABLED=true \
  -e API_SERVER_HOST=0.0.0.0 \
  -e API_SERVER_KEY="$(openssl rand -hex 32)" \
  -e API_SERVER_CORS_ORIGINS='*' \
  nousresearch/hermes-agent gateway run

Любой открытый порт на машине, смотрящей в интернет, — это риск для безопасности. Не делайте этого, пока не осознаёте, чем рискуете.

Запуск дашборда

Встроенный веб-дашборд работает как контролируемый s6-rc-сервис рядом со шлюзом в том же контейнере. Чтобы поднять его, задайте HERMES_DASHBOARD=1:

docker run -d \
  --name hermes \
  --restart unless-stopped \
  -v ~/.hermes:/opt/data \
  -p 8642:8642 \
  -p 9119:9119 \
  -e HERMES_DASHBOARD=1 \
  nousresearch/hermes-agent gateway run

Дашборд под присмотром s6 — если он падает, s6-supervise перезапускает его после короткой задержки. Его stdout/stderr пересылаются в docker logs <container> (без префикса; собственный вывод шлюза теперь живёт в отдельном s6-log-файле по профилям — см. Куда уходят логи ниже, — так что потоки не перемешиваются).

Переменная окруженияОписаниеПо умолчанию
HERMES_DASHBOARDУстановите 1 (либо true / yes), чтобы включить контролируемый сервис дашборда(не задано — сервис зарегистрирован, но остаётся выключенным)
HERMES_DASHBOARD_HOSTАдрес привязки HTTP-сервера дашборда0.0.0.0
HERMES_DASHBOARD_PORTПорт HTTP-сервера дашборда9119
HERMES_DASHBOARD_INSECUREУстановите 1 (либо true / yes), чтобы привязаться без OAuth-аутентификации. Применяйте только в доверенных сетях за reverse-прокси без OAuth-контракта — дашборд раскрывает API-ключи и данные сессий(не задано — проверка действует, когда зарегистрирован DashboardAuthProvider)

Внутри контейнера дашборд по умолчанию привязывается к 0.0.0.0 — иначе опубликованный порт -p 9119:9119 был бы недоступен с хоста. Чтобы ограничить привязку петлёй контейнера (для sidecar / reverse-прокси), задайте HERMES_DASHBOARD_HOST=127.0.0.1.

Проверка аутентификации у дашборда включается автоматически, когда одновременно верно:

  1. Хост привязки не петлевой (например, дефолтный 0.0.0.0 внутри контейнера), и
  2. Зарегистрирован плагин DashboardAuthProvider.

Удовлетворить второе условие можно тремя встроенными способами:

  • Логин/пароль — самый простой для self-hosted / on-prem / домашнего контейнера в доверенной сети или за VPN: задайте HERMES_DASHBOARD_BASIC_AUTH_USERNAME + HERMES_DASHBOARD_BASIC_AUTH_PASSWORDHERMES_DASHBOARD_BASIC_AUTH_SECRET для сессий, устойчивых к перезапуску). Для прямого выхода в публичный интернет не годится.
  • OAuth (Nous Portal) — для хостинговых/публичных развёртываний: провайдер dashboard_auth/nous активируется, как только задан HERMES_DASHBOARD_OAUTH_CLIENT_ID.
  • Собственный OIDC — чтобы аутентифицироваться через ваш провайдер идентичности по стандартному OpenID Connect: провайдер dashboard_auth/self_hosted активируется при заданных HERMES_DASHBOARD_OIDC_ISSUER + HERMES_DASHBOARD_OIDC_CLIENT_ID.

Какой бы вариант вы ни выбрали, проверка перенаправляет посетителей на страницу входа прежде, чем они доберутся до любого защищённого маршрута. Все три провайдера описаны в разделе Веб-дашборд → Аутентификация.

Если провайдер не зарегистрирован, а привязка не петлевая, дашборд отказывается стартовать с конкретной ошибкой, указывающей на недостающую переменную окружения. Аварийный обход HERMES_DASHBOARD_INSECURE=1 полностью отключает проверку (один лишь хост привязки никогда не означает --insecure), но тогда дашборд обслуживается без аутентификации — лучше настройте провайдера, если только перед дашбордом у вас нет собственного слоя авторизации.

внимание

HERMES_DASHBOARD_INSECURE=1 раскрывает API-ключи Отказ от OAuth-проверки отдаёт весь API-поверхность дашборда (включая ключи моделей и данные сессий) каждому, кто дотянется до опубликованного порта. Включайте это только при собственном слое авторизации впереди или в доверенной локальной сети, которую вы полностью контролируете.

Запускать дашборд отдельным контейнером можно, если этот контейнер делит с хостом PID- и сетевое пространство (например, network_mode: host, как и делает собственный docker-compose.yml репозитория — см. его сервис dashboard). Детектору живости шлюза нужно общее PID-пространство с процессом шлюза, так что ограничение касается только дашбордов в изолированных контейнерах на bridge-сети без общего PID-пространства.

Интерактивный запуск (чат в CLI)

Чтобы открыть интерактивную сессию чата против работающей директории данных:

docker run -it --rm \
  -v ~/.hermes:/opt/data \
  nousresearch/hermes-agent

Или, если вы уже открыли терминал в работающем контейнере (скажем, через Docker Desktop), просто запустите:

/opt/hermes/.venv/bin/hermes

Постоянные тома

Том /opt/data — единственный источник правды для всего состояния Hermes. Он отображается на директорию хоста ~/.hermes/ и содержит:

ПутьСодержимое
.envAPI-ключи и секреты
config.yamlВся конфигурация Hermes
SOUL.mdЛичность/идентичность агента
sessions/История разговоров
memories/Хранилище постоянной памяти
skills/Установленные навыки
home/Отдельный HOME по профилям для подпроцессов инструментов Hermes (git, ssh, gh, npm и CLI навыков)
cron/Определения запланированных задач
hooks/Хуки событий
logs/Логи времени выполнения
skins/Кастомные скины CLI

Неизменяемое дерево установки

В хостинговых и публикуемых Docker-образах /opt/hermes — это установленное дерево приложения. Оно принадлежит root и доступно рантайм-пользователю hermes только на чтение, поэтому агентские ходы, сессии шлюза, действия дашборда и обычные команды docker exec hermes hermes ... не могут править на месте ядро исходников, встроенный .venv, node_modules или бандл TUI.

Всё изменяемое состояние Hermes живёт под /opt/data: конфигурация, .env, профили, навыки, память, сессии, логи, загрузки дашборда, плагины и прочие управляемые пользователем файлы. Образ также отключает запись .pyc во время выполнения и ленивую установку зависимостей Hermes в /opt/hermes; опциональные зависимости платформы, нужные публикуемому образу, стоит запекать в образ или ставить при новой сборке образа.

В хостинговых/публикуемых образах самоулучшение агента ограничено навыками, памятью, плагинами и конфигурацией под /opt/data. Установленное ядро исходников под /opt/hermes неизменяемо; правки ядра вносятся через PR в репозиторий и доезжают обновлением образа, а не живой правкой работающей установки.

Если оператору нужно починить или осмотреть файлы вне /opt/data, осознанно откройте root-шелл. Шим hermes обычно сбрасывает docker exec hermes hermes ... обратно к рантайм-пользователю; задайте HERMES_DOCKER_EXEC_AS_ROOT=1 для разового вызова от root, когда вам прямо нужна root-семантика.

CLI навыков, которые хранят учётные данные под ~, нужно инициализировать против HOME подпроцесса, а не просто против корня тома данных. Например, навык xurl держит OAuth-состояние в ~/.xurl; в официальной раскладке Docker вызовы инструментов Hermes читают это как /opt/data/home/.xurl, так что выполняйте ручную авторизацию xurl с HOME=/opt/data/home и проверяйте через HOME=/opt/data/home xurl auth status.

внимание

Никогда не запускайте два gateway-контейнера Hermes против одной директории данных одновременно — файлы сессий и хранилища памяти не рассчитаны на конкурентную запись.

Поддержка нескольких профилей

Hermes поддерживает несколько профилей — отдельные подкаталоги ~/.hermes/, которые дают запускать независимых агентов (свой SOUL, навыки, память, сессии, учётные данные) из одной установки. Внутри официального Docker-образа дерево супервизии s6 относится к каждому профилю как к полноценному контролируемому сервису, поэтому рекомендуемое развёртывание — один контейнер, в котором живут все профили.

Каждый профиль, созданный через hermes profile create <name>, получает:

  • Выделенный слот s6-сервиса по адресу /run/service/gateway-<name>/, который рантайм регистрирует динамически — без пересборки контейнера.
  • Авто-перезапуск при падении с управляемой s6-supervise задержкой.
  • Ротируемые логи по профилям в ${HERMES_HOME}/logs/gateways/<name>/current (10 архивов × 1 МБ каждый).
  • Сохранение состояния между перезапусками контейнера: загрузочный реконсилятор читает gateway_state.json из каждой директории профиля и поднимает слот только для тех профилей, чьё последнее зафиксированное состояние было running. Вниз через перезапуск уходит только тот шлюз, который вы остановили явно (hermes gateway stop); перезапуск контейнера, обновление образа или внезапный выход оставляют зафиксированное состояние running, так что при следующей загрузке шлюз стартует автоматически.

Команды управления жизненным циклом, которые вы запускали бы на хосте, изнутри контейнера работают так же:

# Создать профиль — регистрирует s6-слот gateway-<name>.
docker exec hermes hermes profile create coder

# Старт / стоп / рестарт — отправляет команду s6-svc; жизненный цикл шлюза переживает docker restart.
docker exec hermes hermes -p coder gateway start
docker exec hermes hermes -p coder gateway stop
docker exec hermes hermes -p coder gateway restart

# Статус — внутри контейнера сообщает `Manager: s6 (container supervisor)`.
docker exec hermes hermes -p coder gateway status

# Удалить профиль — заодно сносит s6-слот.
docker exec hermes hermes profile delete coder

Под капотом hermes gateway start/stop/restart внутри контейнера перехватывается и маршрутизируется на s6-svc к нужной директории сервиса; учить команды s6 напрямую не придётся. Для сырого состояния супервизора используйте /command/s6-svstat /run/service/gateway-<name> (учтите: /command/ есть в PATH только для процессов, порождённых деревом супервизии, — при вызове из docker exec указывайте абсолютный путь).

Как достучаться до нескольких профилей снаружи контейнера

Снаружи до шлюза профиля ведут две разные поверхности, и ведут себя они по-разному — не путайте их:

Hermes Desktop (и веб-дашборд). Соединение Remote Gateway в десктопном приложении говорит с бэкендом hermes dashboard (по умолчанию порт 9119, включается через HERMES_DASHBOARD=1) — а не с OpenAI API-сервером. Один бэкенд дашборда обслуживает все профили на той же машине: переключатель профилей в приложении передаёт целевой профиль с каждым запросом, а бэкенд открывает его HERMES_HOME на диске. Так что для Desktop не нужен ни второй порт, ни второе соединение на профиль — одного соединения :9119 через переключатель хватает на все.

OpenAI-совместимые API-клиенты (Open WebUI, LobeChat, /v1/...). Они говорят с API-сервером каждого профиля, а тот привязывается к порту 8642 для каждого профиля (берётся из API_SERVER_PORT / platforms.api_server.extra.port — никакого автоназначения и никакого ключа config.yaml/gateway.port нет). Если хотите, чтобы клиент достучался до конкретного второго профиля, дайте этому профилю отдельный API_SERVER_PORT в его собственном .env, иначе его шлюз тоже попробует занять 8642 и столкнётся с профилем по умолчанию:

# Создать профиль (регистрирует его s6-слот gateway-<name>)
docker exec hermes hermes profile create work

# Указать его API-серверу свободный порт (пишем в собственный .env профиля)
cat >> /opt/data/profiles/work/.env <<'EOF'
API_SERVER_ENABLED=true
API_SERVER_PORT=8643
EOF

docker exec hermes hermes -p work gateway restart

Держите API_SERVER_PORT в собственном .env каждого профиля, а не в общем для контейнера блоке environment: — глобальное значение загнало бы все профили на один порт, и они бы столкнулись. При bridge-сети опубликуйте дополнительный порт в docker-compose.yml (- "8643:8643"); при network_mode: host он и так доступен на хосте. Соединение 8642 профиля по умолчанию остаётся нетронутым.

Почему один контейнер с множеством профилей, а не множество контейнеров

До перехода на s6 паттерн «один контейнер на профиль» был вынужденным: внутри контейнера не было супервизора, способного управлять несколькими шлюзами. С s6 в роли PID 1 это больше не нужно, и раскладка с одним контейнером проще почти по всем статьям:

Один контейнер, много профилейОдин контейнер на профиль
Накладные расходы на дискОдин образ, один встроенный venv, один кеш PlaywrightN образов / N кешей
Накладные расходы на памятьОбщий кеш интерпретатора Python, общие node_modulesДублируются для каждого контейнера
Создание профиляdocker exec ... hermes profile create <name> (секунды)Новый вызов docker run + выделение порта + bind-mount конфигурации
Восстановление после падения профиляАвто-перезапуск через s6-superviseDocker-овский --restart unless-stopped (медленнее, убивает соседние задачи)
ЛогиРотируемый файл по профилям через s6-log плюс журнал загрузки контейнераdocker logs <name> на каждый контейнер — без встроенной ротации
Резервная копияОдна директория ~/.hermesN директорий, которые нужно согласовывать

Профиль по умолчанию (default) всегда регистрируется при первой загрузке, так что свежий контейнер из коробки несёт один контролируемый шлюз. Дополнительные профили добавляются в рантайме без пересборки.

Когда отдельный контейнер всё-таки нужен

Профиль-в-контейнере — поведение по умолчанию. Отдельный контейнер на профиль запускайте только при конкретной причине:

  • Изоляция ресурсов по нагрузке — например, разогнавшаяся сессия браузерного инструмента в профиле A не должна вызвать OOM у профиля B. Контейнеры дают --memory / --cpus на каждый профиль.
  • Независимое закрепление образов — разные теги апстрим-образа под разные нагрузки.
  • Сегментация сети — отдельные Docker-сети на профиль (скажем, один смотрит на клиентов, другой внутренний).
  • Комплаенс / радиус поражения — отдельные учётные данные никогда не делят дерево процессов на уровне ОС.

В таких случаях объявляйте по одному сервису на профиль с отдельными container_name, volumes и ports:

services:
  hermes-work:
    image: nousresearch/hermes-agent:latest
    container_name: hermes-work
    restart: unless-stopped
    command: gateway run
    ports:
      - "8642:8642"
    volumes:
      - ~/.hermes-work:/opt/data

  hermes-personal:
    image: nousresearch/hermes-agent:latest
    container_name: hermes-personal
    restart: unless-stopped
    command: gateway run
    ports:
      - "8643:8642"
    volumes:
      - ~/.hermes-personal:/opt/data

Предупреждение из раздела Постоянные тома по-прежнему в силе: никогда не направляйте два контейнера на одну директорию ~/.hermes одновременно. Супервизор s6 внутри каждого контейнера управляет своим набором профилей; разделение тома данных между контейнерами портит файлы сессий и хранилища памяти.

Куда уходят логи

У s6-контейнера четыре отдельных поверхности логов, и «почему мой шлюз ничего не показывает в docker logs» — частая неожиданность. Шпаргалка:

ИсточникКуда попадаетКак читать
Шлюз по профилю (hermes gateway run и шлюзы по профилям под s6)Дублируется в два места: docker logs <container> (в реальном времени, без лишнего префикса) и ${HERMES_HOME}/logs/gateways/<profile>/current (ротация, метки времени ISO-8601, 10 архивов × 1 МБ каждый)docker logs -f hermes или tail -F ~/.hermes/logs/gateways/default/current на хосте
Дашборд (при HERMES_DASHBOARD=1)docker logs <container> (без префикса)docker logs -f hermes — вперемешку со строками шлюза
Загрузочный реконсилятор (фиксирует, какие шлюзы профилей восстановлены при каждом старте контейнера)${HERMES_HOME}/logs/container-boot.log (журнал только на дозапись)tail -F ~/.hermes/logs/container-boot.log
Общие логи Hermes (agent.log, errors.log)${HERMES_HOME}/logs/ (с учётом профиля)docker exec hermes hermes logs --follow [--level WARNING] [--session <id>]

Два практических следствия:

  • Файловая копия в logs/gateways/<profile>/current — это то, что переживает перезапуски контейнера. docker logs хранит вывод только за время жизни текущего контейнера (и стирается при docker rm); ротируемые файлы остаются на bind-mount-томе.
  • Строка аудита реконсилятора имеет форму <iso-timestamp> profile=<name> prior_state=<state> action=<registered|started>, так что быстрый grep profile=coder ~/.hermes/logs/container-boot.log покажет, когда профиль восстанавливали в последний раз и запустил ли его s6 автоматически.

Проброс переменных окружения

API-ключи читаются из /opt/data/.env внутри контейнера. Переменные окружения можно передать и напрямую:

docker run -it --rm \
  -v ~/.hermes:/opt/data \
  -e ANTHROPIC_API_KEY="sk-ant-..." \
  -e OPENAI_API_KEY="sk-..." \
  nousresearch/hermes-agent

Прямые флаги -e перекрывают значения из .env. Удобно для CI/CD или интеграций с secrets-менеджером, когда не хочется держать ключи на диске.

заметка
Ищете Docker как

терминальный бэкенд? Эта страница про запуск самого Hermes внутри Docker. Если же вам нужно, чтобы Hermes выполнял агентские вызовы terminal / execute_code внутри Docker-песочницы (один долгоживущий контейнер, общий для процессов Hermes — см. issue #20561), это отдельный блок конфигурации: terminal.backend: docker плюс terminal.docker_image, terminal.docker_volumes, terminal.docker_forward_env, terminal.docker_env, terminal.docker_run_as_host_user, terminal.docker_extra_args, terminal.docker_persist_across_processes и terminal.docker_orphan_reaper. Полный набор, включая правила жизненного цикла контейнера, описан в Конфигурация → Docker Backend.

Пример Docker Compose

Для постоянного развёртывания со шлюзом и дашбордом удобен docker-compose.yaml:

services:
  hermes:
    image: nousresearch/hermes-agent:latest
    container_name: hermes
    restart: unless-stopped
    command: gateway run
    ports:
      - "8642:8642"   # gateway API
      - "9119:9119"   # dashboard (only reached when HERMES_DASHBOARD=1)
    volumes:
      - ~/.hermes:/opt/data
    environment:
      - HERMES_DASHBOARD=1
      # Uncomment to forward specific env vars instead of using .env file:
      # - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
      # - OPENAI_API_KEY=${OPENAI_API_KEY}
      # - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
    deploy:
      resources:
        limits:
          memory: 4G
          cpus: "2.0"

Запустите через docker compose up -d, а логи смотрите через docker compose logs -f. stdout контролируемого шлюза также дублируется в ${HERMES_HOME}/logs/gateways/<profile>/current на томе — полную карту маршрутов см. в Куда уходят логи.

Опционально: аудиомост для Linux-десктопа

Чтобы голосовой режим заработал в Docker, нужны две отдельные вещи: Hermes должно быть разрешено опрашивать аудиоустройства внутри контейнера, и сам контейнер должен дотягиваться до аудиосервера хоста. Настройка ниже закрывает хостовую часть звуковой обвязки для Linux-десктопов, отдающих PulseAudio-совместимый сокет, — в том числе для многих конфигураций PipeWire.

осторожно

Это обходной приём для Linux-десктопа, а не общая возможность Docker Desktop. Он пригодится, когда хостовое аудио у вас уже работает и хочется голосовой режим CLI внутри контейнера Hermes. Если Hermes всё равно сообщает Running inside Docker container -- no audio devices, используйте сборку с поддержкой Docker-опроса аудио для PULSE_SERVER / PIPEWIRE_REMOTE.

Сначала создайте ALSA-конфиг рядом с файлом Compose:

pcm.!default {
    type pulse
    hint {
        show on
        description "Default ALSA Output (PulseAudio)"
    }
}

pcm.pulse {
    type pulse
}

ctl.!default {
    type pulse
}

Затем соберите небольшой производный образ с установленным ALSA-плагином PulseAudio:

FROM nousresearch/hermes-agent:latest

USER root
RUN apt-get update \
    && apt-get install -y --no-install-recommends libasound2-plugins \
    && rm -rf /var/lib/apt/lists/*

Используйте этот образ в Compose и пробросьте PulseAudio-сокет и cookie хостового пользователя:

services:
  hermes:
    build:
      context: .
      dockerfile: Dockerfile.audio
    image: hermes-agent-audio
    container_name: hermes
    restart: unless-stopped
    command: gateway run
    volumes:
      - ~/.hermes:/opt/data
      - /run/user/${HERMES_UID}/pulse:/run/user/${HERMES_UID}/pulse
      - ~/.config/pulse/cookie:/tmp/pulse-cookie:ro
      - ./asound.conf:/etc/asound.conf:ro
    environment:
      - HERMES_UID=${HERMES_UID}
      - HERMES_GID=${HERMES_GID}
      - XDG_RUNTIME_DIR=/run/user/${HERMES_UID}
      - PULSE_SERVER=unix:/run/user/${HERMES_UID}/pulse/native
      - PULSE_COOKIE=/tmp/pulse-cookie

Запускайте с UID/GID хоста, чтобы процесс контейнера получил доступ к пер-пользовательскому аудиосокету:

export HERMES_UID="$(id -u)"
export HERMES_GID="$(id -g)"
docker compose up -d --build

Проверить, что именно видит PortAudio внутри контейнера:

docker exec hermes /opt/hermes/.venv/bin/python -c "import sounddevice as sd; print(sd.query_devices())"

Лимиты ресурсов

Контейнеру Hermes нужны умеренные ресурсы. Рекомендуемые минимумы:

РесурсМинимумРекомендуется
Память1 ГБ2–4 ГБ
CPU1 ядро2 ядра
Диск (том данных)500 МБ2+ ГБ (растёт с сессиями/навыками)

Самая прожорливая по памяти возможность — автоматизация браузера (Playwright/Chromium). Без браузерных инструментов хватит 1 ГБ. Когда они активны, выделяйте минимум 2 ГБ.

Задайте лимиты в Docker:

docker run -d \
  --name hermes \
  --restart unless-stopped \
  --memory=4g --cpus=2 \
  -v ~/.hermes:/opt/data \
  nousresearch/hermes-agent gateway run

Что делает Dockerfile

Официальный образ построен на debian:13.4 и включает:

  • Python 3 со всеми зависимостями Hermes (uv pip install -e ".[all]")
  • Node.js + npm (для автоматизации браузера и моста WhatsApp)
  • Playwright с Chromium (npx playwright install --with-deps chromium --only-shell)
  • ripgrep, ffmpeg, git и xz-utils как системные утилиты
  • docker-cli — чтобы агенты внутри контейнера могли управлять Docker-демоном хоста (для подключения примонтируйте /var/run/docker.sock): docker build, docker run, инспекция контейнеров и т. д.
  • openssh-client — включает SSH-терминальный бэкенд изнутри контейнера. SSH-бэкенд вызывает системный бинарник ssh; без него в контейнерных установках он тихо отказывал.
  • Мост WhatsApp (scripts/whatsapp-bridge/)
  • s6-overlay v3 в роли PID 1 (заменяет прежний tini) — контролирует дашборд и шлюзы по профилям с авто-перезапуском при падении, подбирает зомби-подпроцессы и пробрасывает сигналы.

ENTRYPOINT контейнера — это /init из s6-overlay. При загрузке он:

  1. Запускает /etc/cont-init.d/01-hermes-setup (= docker/stage2-hook.sh) от root: опциональный ремап UID/GID, починка владельца тома, посев .env / config.yaml / SOUL.md при первой загрузке, неинтерактивные миграции схемы конфигурации (если не задано HERMES_SKIP_CONFIG_MIGRATION=1), синхронизация встроенных навыков.
  2. Запускает /etc/cont-init.d/02-reconcile-profiles (= hermes_cli.container_boot): обходит $HERMES_HOME/profiles/<name>/, пересоздаёт по профилям s6-слот сервиса шлюза под /run/service/gateway-<profile>/ и авто-стартует только те, чьё последнее зафиксированное состояние было running (см. Супервизия шлюзов по профилям).
  3. Стартует статические s6-rc-сервисы main-hermes и dashboard.
  4. Делает exec CMD контейнера как основной программы (/opt/hermes/docker/main-wrapper.sh), которая маршрутизирует аргументы, переданные пользователем в docker run:
    • нет аргументов → hermes (по умолчанию)
    • первый аргумент — исполняемый файл в PATH (например, sleep, bash) → exec напрямую
    • всё остальное → hermes <args> (проброс подкоманды) Контейнер завершается, когда завершается эта основная программа, с её кодом выхода.
внимание
Ломающее изменение по сравнению с pre-s6-образами

ENTRYPOINT контейнера теперь /init (s6-overlay), а не /usr/bin/tini. Все пять задокументированных паттернов вызова docker run (без аргументов, chat -q "…", sleep infinity, bash, --tui) ведут себя идентично образу на tini. Если у вас есть нижестоящая обёртка, завязанная на специфичное для tini поведение сигналов или на жёстко прописанный вызов /usr/bin/tini --, закрепитесь на предыдущем теге образа.

внимание
Модель привилегий

Не переопределяйте entrypoint образа, если не оставляете /init (или эквивалентно — легаси-шим docker/entrypoint.sh, который форвардит на stage2-хук) в цепочке команд. /init из s6-overlay работает от root, чтобы при первой загрузке сделать chown тома, а затем сбрасывает привилегии до пользователя hermes через s6-setuidgid для каждого контролируемого сервиса И для основной программы. Запуск hermes gateway run от root внутри официального образа по умолчанию запрещён, потому что это может оставить в /opt/data файлы, принадлежащие root, и сломать последующие старты дашборда или шлюза. Задавайте HERMES_ALLOW_ROOT_GATEWAY=1 только когда осознанно принимаете этот риск.

docker exec автоматически сбрасывает привилегии до пользователя hermes

docker exec hermes <cmd> по умолчанию работает от root внутри контейнера, но образ несёт тонкий шим в /opt/hermes/bin/hermes (самый ранний в PATH), который распознаёт вызовы от root и прозрачно перевыполняет их через s6-setuidgid hermes. Поэтому docker exec hermes login, docker exec hermes profile create …, docker exec hermes setup и т. п. пишут файлы, принадлежащие UID 10000, — то есть читаемые контролируемым шлюзом, — и никакого дополнительного флага --user не требуется. Не-root-вызовы (сами контролируемые процессы, docker exec --user hermes, kanban-субагенты внутри контейнера) попадают на короткое замыкание, exec-ающее бинарник venv напрямую, так что на горячих путях нет накладных расходов.

Если вам нужен именно такой docker exec, что сохраняет root-семантику (диагностические сессии, инспекция root-only-состояния, файлы вне /opt/data, которыми владеет root), откажитесь от сброса для конкретного вызова:

docker exec -e HERMES_DOCKER_EXEC_AS_ROOT=1 hermes <cmd>

Шим принимает 1 / true / yes (без учёта регистра). Всё остальное — включая опечатки вроде =0 — проваливается на сброс, так что тихий отказ невозможен. Если s6-setuidgid недоступен (кастомные сборки, из которых вырезали s6-overlay), шим отказывается работать от root и завершается с кодом 126, громко вскрывая сломанную модель привилегий вместо отката к историческому подвоху, когда docker exec hermes login писал auth.json как root:root и ломал авторизацию контролируемого шлюза на каждом сообщении любой чат-платформы.

Супервизия шлюзов по профилям

Каждый профиль, созданный через hermes profile create <name>, автоматически получает s6-контролируемый сервис шлюза, зарегистрированный по адресу /run/service/gateway-<name>/, с авто-перезапуском, сохраняющим состояние между перезапусками контейнера. Пользовательский сценарий и команды управления жизненным циклом см. в разделе Поддержка нескольких профилей выше.

Преимущества супервизии перед pre-s6-образом:

  • Падения шлюза авто-перезапускаются s6-supervise после задержки ~1 с.
  • Дашборд, включённый через HERMES_DASHBOARD=1, контролируется тем же деревом супервизии и получает такой же авто-перезапуск.
  • docker restart, обновления образа (docker compose up -d --force-recreate) и внезапные выходы сохраняют работающие шлюзы: cont-init-реконсилятор читает $HERMES_HOME/profiles/<name>/gateway_state.json и поднимает слот, если последнее зафиксированное состояние было running. Только явный hermes gateway stop записывает stopped и держит шлюз выключенным через перезапуск; SIGTERM от контейнера/s6 при перезапуске или обновлении трактуется как «всё ещё работает» и приводит к авто-старту.
  • Логи шлюзов по профилям сохраняются под $HERMES_HOME/logs/gateways/<profile>/current (ротация через s6-log), а действия реконсилятора дозаписываются в $HERMES_HOME/logs/container-boot.log при каждой загрузке. Полную карту маршрутов см. в Куда уходят логи.

hermes status внутри контейнера сообщает Manager: s6 (container supervisor). Для сырого взгляда супервизора используйте /command/s6-svstat /run/service/gateway-<name> (учтите: /command/ есть в PATH только для процессов дерева супервизии; при вызове из docker exec указывайте абсолютный путь).

Обновление

Скачайте свежий образ и пересоздайте контейнер. Директория данных сохраняется, а контейнер перед стартом шлюза прогоняет неинтерактивные миграции схемы конфигурации против примонтированного $HERMES_HOME/config.yaml. Когда миграция нужна, Hermes сначала пишет рядом с config.yaml и .env резервные копии с метками времени.

docker pull nousresearch/hermes-agent:latest
docker rm -f hermes
docker run -d \
  --name hermes \
  --restart unless-stopped \
  -v ~/.hermes:/opt/data \
  nousresearch/hermes-agent gateway run

Или через Docker Compose:

docker compose pull
docker compose up -d

Задавайте HERMES_SKIP_CONFIG_MIGRATION=1 только если хотите вручную осмотреть или мигрировать сохранённую конфигурацию прежде, чем новый образ перепишет её.

Навыки и файлы учётных данных

Когда Docker выступает средой выполнения (не методы выше, а случай, когда агент выполняет команды внутри Docker-песочницы — см. Конфигурация → Docker Backend), Hermes переиспользует один долгоживущий контейнер для всех вызовов инструментов и автоматически bind-mount-ит директорию навыков (~/.hermes/skills/) и любые объявленные навыками файлы учётных данных в этот контейнер как read-only-тома. Скрипты, шаблоны и справочники навыков доступны внутри песочницы без ручной настройки, а поскольку контейнер живёт всё время работы процесса Hermes, любые установленные зависимости или записанные файлы остаются на месте к следующему вызову инструмента.

Та же синхронизация происходит для бэкендов SSH и Modal — навыки и файлы учётных данных загружаются через rsync или Modal mount API перед каждой командой.

Установка дополнительных инструментов в контейнер

Официальный образ несёт подобранный набор утилит (см. Что делает Dockerfile), но не каждый инструмент, который может понадобиться агенту, предустановлен. Есть пять рекомендованных подходов — по возрастанию усилий и долговечности.

npm- или Python-инструменты — используйте npx или uvx

Для любого инструмента, опубликованного в npm или PyPI, попросите Hermes запускать его через npx (npm) или uvx (Python) и запомнить эту команду в постоянной памяти. Если инструменту нужен конфиг или учётные данные, попросите сложить их под /opt/data (например, /opt/data/<tool>/config.yaml).

Зависимости подтягиваются по требованию и кешируются на время жизни контейнера. Конфигурация, записанная под /opt/data, переживает перезапуски контейнера, потому что лежит на bind-mount-директории хоста. Сам кеш пакетов пересобирается после docker rm, но npx и uvx прозрачно перекачивают его при следующем запуске инструмента.

Прочие инструменты (apt-пакеты, бинарники) — установите и запомните

Для всего за пределами npm или PyPI — apt-пакетов, готовых бинарников, языковых рантаймов, которых ещё нет в образе, — объясните Hermes, как это поставить (например, apt-get update && apt-get install -y <package>), и попросите запомнить команду установки. Инструмент будет жить до конца жизни контейнера, а Hermes перезапустит команду установки после перезапуска контейнера, когда инструмент снова понадобится.

Это хорошо подходит для инструментов, которые быстро ставятся и используются нечасто. Для тех, что нужны постоянно, лучше следующий подход.

Долговечная установка — соберите производный образ

Когда инструмент должен быть доступен сразу при каждом старте контейнера без задержки на переустановку, соберите новый образ, наследующий от nousresearch/hermes-agent и ставящий инструмент в отдельный слой:

FROM nousresearch/hermes-agent:latest

USER root
RUN apt-get update \
    && apt-get install -y --no-install-recommends <your-package> \
    && rm -rf /var/lib/apt/lists/*
USER hermes

Соберите его и используйте вместо официального образа:

docker build -t my-hermes:latest .
docker run -d \
  --name hermes \
  --restart unless-stopped \
  -v ~/.hermes:/opt/data \
  -p 8642:8642 \
  my-hermes:latest gateway run

Скрипт entrypoint и семантика /opt/data наследуются без изменений, так что остальное на этой странице по-прежнему применимо. Не забывайте пересобирать образ при скачивании более свежего апстрим-nousresearch/hermes-agent.

Сложные инструменты или мультисервисные стеки — запустите sidecar-контейнер

Для инструментов, которые тянут собственный сервис (базу данных, веб-сервер, очередь, ферму headless-браузеров) или слишком тяжелы, чтобы жить внутри контейнера Hermes, запускайте их отдельным контейнером в общей Docker-сети. Hermes дотягивается до sidecar по имени контейнера — так же, как до локального inference-сервера (см. Подключение к локальным inference-серверам).

services:
  hermes:
    image: nousresearch/hermes-agent:latest
    container_name: hermes
    restart: unless-stopped
    command: gateway run
    ports:
      - "8642:8642"
    volumes:
      - ~/.hermes:/opt/data
    networks:
      - hermes-net

  my-tool:
    image: example/my-tool:latest
    container_name: my-tool
    restart: unless-stopped
    networks:
      - hermes-net

networks:
  hermes-net:
    driver: bridge

Изнутри контейнера Hermes sidecar доступен по http://my-tool:<port> (или по тому протоколу, что он отдаёт). Такой паттерн оставляет жизненный цикл, лимиты ресурсов и темп обновлений каждого сервиса независимыми и не раздувает образ Hermes зависимостями, нужными лишь одному инструменту.

Широко полезные инструменты — откройте issue или pull request

Если инструмент, скорее всего, пригодится большинству пользователей Hermes Agent, подумайте о том, чтобы внести его в апстрим, а не таскать в приватном производном образе. Откройте issue или pull request в репозитории hermes-agent с описанием инструмента и сценария его применения. Инструменты, попавшие в официальный образ, приносят пользу каждому и избавляют от расходов на поддержку нижестоящего форка.

Подключение к локальным inference-серверам (vLLM, Ollama и т. д.)

Когда Hermes работает в Docker, а ваш inference-сервер (vLLM, Ollama, text-generation-inference и т. д.) тоже работает на хосте или в другом контейнере, сети требуют дополнительного внимания.

Docker Compose (рекомендуется)

Поместите оба сервиса в одну Docker-сеть. Это самый надёжный подход:

services:
  vllm:
    image: vllm/vllm-openai:latest
    container_name: vllm
    command: >
      --model Qwen/Qwen2.5-7B-Instruct
      --served-model-name my-model
      --host 0.0.0.0
      --port 8000
    ports:
      - "8000:8000"
    networks:
      - hermes-net
    deploy:
      resources:
        reservations:
          devices:
            - capabilities: [gpu]

  hermes:
    image: nousresearch/hermes-agent:latest
    container_name: hermes
    restart: unless-stopped
    command: gateway run
    ports:
      - "8642:8642"
    volumes:
      - ~/.hermes:/opt/data
    networks:
      - hermes-net

networks:
  hermes-net:
    driver: bridge

Затем в ~/.hermes/config.yaml укажите в качестве хоста имя контейнера:

model:
  provider: custom
  model: my-model
  base_url: http://vllm:8000/v1
  api_key: "none"
совет
Ключевые моменты
  • В качестве хоста используйте имя контейнера (vllm), а не localhost или 127.0.0.1 — они указывают на сам контейнер Hermes.
  • Значение model должно совпадать с --served-model-name, переданным vLLM.
  • Задайте api_key любой непустой строкой (vLLM требует заголовок, но по умолчанию его не проверяет).
  • Не добавляйте завершающий слеш в base_url.

Отдельный docker run (без Compose)

Если inference-сервер работает прямо на хосте (не в Docker), используйте host.docker.internal на macOS/Windows или --network host на Linux:

macOS / Windows:

docker run -d \
  --name hermes \
  -v ~/.hermes:/opt/data \
  -p 8642:8642 \
  nousresearch/hermes-agent gateway run
# config.yaml
model:
  provider: custom
  model: my-model
  base_url: http://host.docker.internal:8000/v1
  api_key: "none"

Linux (host networking):

docker run -d \
  --name hermes \
  --network host \
  -v ~/.hermes:/opt/data \
  nousresearch/hermes-agent gateway run
# config.yaml
model:
  provider: custom
  model: my-model
  base_url: http://127.0.0.1:8000/v1
  api_key: "none"
внимание
С

--network host флаг -p игнорируется — все порты контейнера напрямую выставлены на хосте.

Проверка связности

Изнутри контейнера Hermes убедитесь, что inference-сервер доступен:

docker exec hermes curl -s http://vllm:8000/v1/models

В ответ должен прийти JSON со списком вашей модели. Если не вышло, проверьте:

  1. Оба контейнера в одной Docker-сети (docker network inspect hermes-net)
  2. Inference-сервер слушает 0.0.0.0, а не 127.0.0.1
  3. Совпадает номер порта

Ollama

Ollama работает так же. Если Ollama запущена на хосте, используйте host.docker.internal:11434 (macOS/Windows) или 127.0.0.1:11434 (Linux с --network host). Если Ollama работает в своём контейнере в той же Docker-сети:

model:
  provider: custom
  model: llama3
  base_url: http://ollama:11434/v1
  api_key: "none"

Устранение неполадок

Контейнер сразу завершается

Проверьте логи: docker logs hermes. Частые причины:

  • Отсутствует или повреждён файл .env — сначала запустите интерактивно и завершите настройку
  • Конфликты портов, если запускаете с открытыми портами

Ошибки «Permission denied»

stage2-хук контейнера сбрасывает привилегии до не-root-пользователя hermes (UID 10000) через s6-setuidgid внутри каждого контролируемого сервиса. Если ваша хостовая ~/.hermes/ принадлежит другому UID, задайте HERMES_UID/HERMES_GID — или их алиасы PUID/PGID, для совместимости с образами LinuxServer.io и NAS — под вашего хостового пользователя либо убедитесь, что директория данных доступна на запись:

chmod -R 755 ~/.hermes

На NAS (UGOS, Synology, unRAID) директория данных обычно представляет собой bind mount, принадлежащий хостовому UID, которому контейнер не может сделать chown. Задайте PUID/PGID (или HERMES_UID/HERMES_GID) под этого хостового пользователя, чтобы рантайм работал от владельца монтирования, а не от UID 10000:

docker run -d \
  --name hermes \
  -e PUID=1000 -e PGID=10 \
  -v /volume1/docker/hermes:/opt/data \
  nousresearch/hermes-agent gateway run

docker exec hermes <cmd> тоже автоматически сбрасывает привилегии до UID 10000 — подробности и отказ для конкретного вызова см. в docker exec автоматически сбрасывает привилегии до пользователя hermes.

Браузерные инструменты не работают

Playwright нужна разделяемая память. Добавьте --shm-size=1g в команду docker run:

docker run -d \
  --name hermes \
  --shm-size=1g \
  -v ~/.hermes:/opt/data \
  nousresearch/hermes-agent gateway run

Шлюз не переподключается после сетевых сбоев

Флаг --restart unless-stopped справляется с большинством временных сбоев. Если шлюз завис, перезапустите контейнер:

docker restart hermes

Проверка состояния контейнера

docker logs --tail 50 hermes          # Последние логи
docker run -it --rm nousresearch/hermes-agent:latest version     # Проверить версию
docker stats hermes                    # Использование ресурсов
ESC