Hermes Agent — Docker
Docker пересекается с Hermes Agent двумя разными способами:
- Запуск Hermes ВНУТРИ Docker — сам агент работает внутри контейнера (этому и посвящена страница);
- Docker как терминальный бэкенд — агент работает на хосте, но каждую команду выполняет в одной постоянной Docker-песочнице. Этот контейнер живёт между вызовами инструментов, переживает
/newи субагентов всё время работы процесса Hermes (см. Конфигурация → Docker Backend).
Здесь разбираем первый вариант. Контейнер хранит все пользовательские данные (конфигурацию, API-ключи, сессии, навыки, память) в одной директории, примонтированной с хоста на /opt/data. Сам образ не хранит состояние — его можно обновить, просто скачав новую версию, и при этом не потерять ни одной настройки.
Быстрый старт
Запускаете Hermes Agent впервые? Создайте на хосте директорию для данных и запустите контейнер в интерактивном режиме, чтобы пройти мастер настройки:
Некоторые 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.
Проверка аутентификации у дашборда включается автоматически, когда одновременно верно:
- Хост привязки не петлевой (например, дефолтный
0.0.0.0внутри контейнера), и - Зарегистрирован плагин
DashboardAuthProvider.
Удовлетворить второе условие можно тремя встроенными способами:
- Логин/пароль — самый простой для self-hosted / on-prem / домашнего контейнера в доверенной сети или за VPN: задайте
HERMES_DASHBOARD_BASIC_AUTH_USERNAME+HERMES_DASHBOARD_BASIC_AUTH_PASSWORD(иHERMES_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/ и содержит:
| Путь | Содержимое |
|---|---|
.env | API-ключи и секреты |
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, один кеш Playwright | N образов / N кешей |
| Накладные расходы на память | Общий кеш интерпретатора Python, общие node_modules | Дублируются для каждого контейнера |
| Создание профиля | docker exec ... hermes profile create <name> (секунды) | Новый вызов docker run + выделение порта + bind-mount конфигурации |
| Восстановление после падения профиля | Авто-перезапуск через s6-supervise | Docker-овский --restart unless-stopped (медленнее, убивает соседние задачи) |
| Логи | Ротируемый файл по профилям через s6-log плюс журнал загрузки контейнера | docker logs <name> на каждый контейнер — без встроенной ротации |
| Резервная копия | Одна директория ~/.hermes | N директорий, которые нужно согласовывать |
Профиль по умолчанию (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-менеджером, когда не хочется держать ключи на диске.
терминальный бэкенд?
Эта страница про запуск самого 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 ГБ |
| CPU | 1 ядро | 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-overlayv3 в роли PID 1 (заменяет прежнийtini) — контролирует дашборд и шлюзы по профилям с авто-перезапуском при падении, подбирает зомби-подпроцессы и пробрасывает сигналы.
ENTRYPOINT контейнера — это /init из s6-overlay. При загрузке он:
- Запускает
/etc/cont-init.d/01-hermes-setup(=docker/stage2-hook.sh) от root: опциональный ремап UID/GID, починка владельца тома, посев.env/config.yaml/SOUL.mdпри первой загрузке, неинтерактивные миграции схемы конфигурации (если не заданоHERMES_SKIP_CONFIG_MIGRATION=1), синхронизация встроенных навыков. - Запускает
/etc/cont-init.d/02-reconcile-profiles(=hermes_cli.container_boot): обходит$HERMES_HOME/profiles/<name>/, пересоздаёт по профилям s6-слот сервиса шлюза под/run/service/gateway-<profile>/и авто-стартует только те, чьё последнее зафиксированное состояние былоrunning(см. Супервизия шлюзов по профилям). - Стартует статические s6-rc-сервисы
main-hermesиdashboard. - Делает exec CMD контейнера как основной программы (
/opt/hermes/docker/main-wrapper.sh), которая маршрутизирует аргументы, переданные пользователем вdocker run:- нет аргументов →
hermes(по умолчанию) - первый аргумент — исполняемый файл в PATH (например,
sleep,bash) → exec напрямую - всё остальное →
hermes <args>(проброс подкоманды) Контейнер завершается, когда завершается эта основная программа, с её кодом выхода.
- нет аргументов →
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 со списком вашей модели. Если не вышло, проверьте:
- Оба контейнера в одной Docker-сети (
docker network inspect hermes-net) - Inference-сервер слушает
0.0.0.0, а не127.0.0.1 - Совпадает номер порта
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 # Использование ресурсов