Documentação oficial desta página · v0.20.6
Hermes no Docker
Existem dois jeitos diferentes de o Docker cruzar com o Hermes Agent:
- Rodar o Hermes DENTRO do Docker — o próprio agente roda dentro de um contêiner (o foco principal desta página)
- Docker como backend de terminal — o agente roda na sua máquina, mas executa cada comando dentro de um único contêiner-sandbox Docker persistente, que sobrevive entre chamadas de ferramenta,
/newe subagentes durante toda a vida do processo do Hermes (veja Configuração → Backend Docker)
Esta página cobre a opção 1. O contêiner guarda todos os dados do usuário (configuração, chaves de API, sessões, skills, memórias) em um único diretório montado a partir do host em /opt/data. A imagem em si não tem estado e pode ser atualizada baixando uma versão nova, sem perder nenhuma configuração.
Início rápido
Se esta é a primeira vez que você roda o Hermes Agent, crie um diretório de dados no host e suba o contêiner em modo interativo para rodar o assistente de configuração:
Alguns provedores de VPS (Hetzner Cloud, entre outros) oferecem um console pelo
navegador para administrar as máquinas. Esses consoles transmitem caracteres
especiais de forma errada — : pode chegar como ;, @ pode ser renderizado
errado, e layouts de teclado fora do inglês se saem pior ainda — o que corrompe
silenciosamente argumentos do docker run como -v ~/.hermes:/opt/data,
-e KEY=value, e chaves de API / tokens colados.
Conecte por SSH em vez disso (ssh root@<host>), para digitar e colar
comandos com segurança. Se você tiver mesmo que usar o console do navegador,
digite os comandos na mão em vez de colar, e confira cada :, @, = e /
no resultado antes de apertar Enter.
mkdir -p ~/.hermes
docker run -it --rm \
-v ~/.hermes:/opt/data \
nousresearch/hermes-agent setup
Isso abre o assistente de configuração, que vai pedir suas chaves de API e gravá-las em ~/.hermes/.env. Você só precisa fazer isso uma vez. É altamente recomendável configurar já nesse momento um sistema de chat para o gateway usar.
Dentro do contêiner, rode hermes setup --portal uma vez — o token de refresh fica salvo no volume montado ~/.hermes. Veja Nous Portal.
Rodar em modo gateway
Depois de configurado, rode o contêiner em segundo plano como um gateway persistente (Telegram, Discord, Slack, WhatsApp etc.):
docker run -d \
--name hermes \
--restart unless-stopped \
-v ~/.hermes:/opt/data \
-p 8642:8642 \
nousresearch/hermes-agent gateway run
A porta 8642 expõe o servidor de API compatível com OpenAI do gateway e o endpoint de saúde. Ela é opcional se você só usa plataformas de chat (Telegram, Discord etc.), mas obrigatória se você quiser que o painel ou ferramentas externas alcancem o gateway.
Dentro da imagem Docker oficial, o gateway run é supervisionado automaticamente pelo s6-overlay: se o processo do gateway quebrar, ele é reiniciado em poucos segundos sem derrubar o contêiner, e o painel (quando HERMES_DASHBOARD=1 está definido) é supervisionado junto. O processo do CMD gateway run em si é um sleep infinity que serve de batimento e mantém o contêiner vivo enquanto o s6 gerencia o processo real do gateway — então docker stop continua encerrando tudo de forma limpa, mas docker logs mostra a saída do gateway supervisionado.
Você vai ver uma linha de rastro no docker logs confirmando a mudança. Para optar por não usar isso — e voltar à semântica histórica de "o gateway é o processo principal do contêiner, gateway sai = contêiner sai" — passe --no-supervise ou defina HERMES_GATEWAY_NO_SUPERVISE=1. A saída é útil em testes de fumaça de CI que querem que o contêiner encerre com o código de status do gateway; para implantações em produção, o padrão supervisionado é melhor em todos os aspectos.
Esse comportamento vale só para a imagem baseada em s6. Imagens mais antigas (baseadas em tini) ainda rodam gateway run como processo principal em primeiro plano.
Veja a seção Onde ficam os logs mais abaixo para o mapa completo de roteamento (gateways por perfil, painel, reconciliador de boot, docker logs do contêiner inteiro).
A opção tool_loop_guardrails.hard_stop_enabled vem como false por padrão, o que faz sentido em sessões interativas de CLI e TUI, onde uma pessoa vê os avisos repetidos de chamada de ferramenta. Em implantações de gateway ou servidor sem ninguém olhando, só os avisos podem não parar um agente preso em um laço repetido de chamadas de ferramenta. Quem operar e quiser comportamento de disjuntor deve habilitar explicitamente as paradas forçadas no config.yaml do perfil:
tool_loop_guardrails:
hard_stop_enabled: true
hard_stop_after:
exact_failure: 5
idempotent_no_progress: 5
Atenção: o servidor de API só sobe com API_SERVER_ENABLED=true. Para expô-lo além do 127.0.0.1 dentro do contêiner, defina também API_SERVER_HOST=0.0.0.0 e uma API_SERVER_KEY (mínimo de 8 caracteres — gere uma com openssl rand -hex 32). Exemplo:
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
Abrir qualquer porta em uma máquina voltada para a internet é um risco de segurança. Não faça isso a menos que você entenda os riscos.
Rodar o painel
O painel web embutido roda como um serviço s6-rc supervisionado ao lado do gateway, no mesmo contêiner. Defina HERMES_DASHBOARD=1 para subi-lo:
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
O painel é supervisionado pelo s6 — se ele quebrar, o s6-supervise reinicia automaticamente depois de um curto intervalo. A saída padrão e de erro do painel é encaminhada para docker logs <contêiner> (sem prefixo; a saída do próprio gateway agora vive em um arquivo de log do s6 por perfil — veja Onde ficam os logs abaixo — para os dois fluxos não se atropelarem).
| Variável de ambiente | Descrição | Padrão |
|---|---|---|
HERMES_DASHBOARD | Defina como 1 (ou true / yes) para habilitar o serviço supervisionado do painel | (não definida — o serviço fica registrado, mas parado) |
HERMES_DASHBOARD_HOST | Endereço de bind do servidor HTTP do painel | 0.0.0.0 |
HERMES_DASHBOARD_PORT | Porta do servidor HTTP do painel | 9119 |
HERMES_DASHBOARD_INSECURE | Obsoleta / sem efeito. Antigamente pulava a barreira de autenticação; desde o endurecimento de junho de 2026 ela não desliga mais a autenticação. Um bind fora do loopback sempre exige um provedor de autenticação | (ignorada — configure um provedor no lugar) |
O painel dentro do contêiner faz bind em 0.0.0.0 por padrão — sem isso, a porta publicada -p 9119:9119 não seria alcançável a partir do host. Para restringir o bind ao loopback do contêiner (em montagens com sidecar / proxy reverso), defina HERMES_DASHBOARD_HOST=127.0.0.1.
A barreira de autenticação do painel entra em ação automaticamente quando as duas condições abaixo são verdadeiras:
- O host de bind está fora do loopback (por exemplo, o padrão
0.0.0.0dentro do contêiner), e - Um plugin
DashboardAuthProviderestá registrado.
Há três formas embutidas de satisfazer a segunda condição:
- Usuário/senha — a mais simples para um contêiner auto-hospedado / on-premise / de homelab em rede confiável ou atrás de uma VPN: defina
HERMES_DASHBOARD_BASIC_AUTH_USERNAME+HERMES_DASHBOARD_BASIC_AUTH_PASSWORD(eHERMES_DASHBOARD_BASIC_AUTH_SECRETpara sessões que sobrevivem a reinícios). Não serve para exposição direta na internet pública. - OAuth (Nous Portal) — para implantações hospedadas/públicas: o provedor
dashboard_auth/nousé ativado sempre queHERMES_DASHBOARD_OAUTH_CLIENT_IDestá definido. - OIDC auto-hospedado — para autenticar contra o seu próprio provedor de identidade via OpenID Connect padrão: o provedor
dashboard_auth/self_hostedé ativado quandoHERMES_DASHBOARD_OIDC_ISSUER+HERMES_DASHBOARD_OIDC_CLIENT_IDestão definidos.
Qualquer que seja a escolha, a barreira redireciona quem chama para uma página de login antes de dar acesso a qualquer rota protegida. Veja Painel Web → Autenticação para os três provedores.
Quando um proxy reverso como Traefik ou nginx roda em outro contêiner, o
endereço dele na rede bridge não é confiável por padrão. Defina a URL pública
do painel e confie só no IP exato daquele proxy, ou em um CIDR limitado para
uma rede dedicada de proxy, no config.yaml montado:
dashboard:
public_url: "https://painel.exemplo.com"
trusted_proxies:
- "172.20.0.5"
# Ou, se o endereço do proxy for dinâmico em uma rede dedicada:
# - "172.20.0.0/24"
Isso deixa o X-Forwarded-Proto: https do proxy controlar os cookies OAuth
seguros, mantendo como não confiáveis os cabeçalhos de encaminhamento de
outros pares. Não use *, 0.0.0.0/0 nem ::/0; o Hermes rejeita essas
entradas sem limite.
Se nenhum provedor estiver registrado e o bind for fora do loopback, o painel falha fechado na inicialização, com um erro específico apontando a variável de ambiente que falta. Não existe mais uma válvula de escape que sirva o painel sem autenticação em um bind público: HERMES_DASHBOARD_INSECURE=1 agora é obsoleta e sem efeito (registra um aviso e é ignorada). Configure um provedor, ou faça bind em HERMES_DASHBOARD_HOST=127.0.0.1 e alcance o painel por um túnel SSH / Tailscale.
--insecure foi removidoUm painel público sem autenticação foi a porta de entrada da campanha de persistência via configuração de MCP em junho de 2026: scanners da internet alcançaram painéis expostos (e servidores de API OpenAI) e levaram o agente a plantar um backdoor de chave SSH. A barreira de autenticação agora é obrigatória em todo bind fora do loopback. Para uma máquina de LAN confiável / homelab, o provedor embutido de usuário/senha (HERMES_DASHBOARD_BASIC_AUTH_USERNAME + _PASSWORD) é o jeito sem infraestrutura de satisfazê-la.
Rodar o painel como um contêiner separado é suportado quando esse contêiner compartilha o namespace de PID e de rede do host (por exemplo, network_mode: host, como faz o próprio docker-compose.yml do repositório — veja o serviço dashboard dele). A detecção de gateway vivo precisa de um namespace de PID compartilhado com o processo do gateway, então a limitação só se aplica a painéis rodando em contêineres isolados em rede bridge, sem namespace de PID compartilhado.
Rodar de forma interativa (chat pela CLI)
Para abrir uma sessão de chat interativa contra um diretório de dados existente:
docker run -it --rm \
-v ~/.hermes:/opt/data \
nousresearch/hermes-agent
Ou, se você já abriu um terminal dentro do contêiner em execução (pelo Docker Desktop, por exemplo), basta rodar:
/opt/hermes/.venv/bin/hermes
Volumes persistentes
O volume /opt/data é a fonte única de verdade de todo o estado do Hermes. Ele mapeia o diretório ~/.hermes/ do seu host e contém:
| Caminho | Conteúdo |
|---|---|
.env | Chaves de API e segredos |
config.yaml | Toda a configuração do Hermes |
SOUL.md | Personalidade/identidade do agente |
sessions/ | Histórico de conversas |
memories/ | Armazenamento de memória persistente |
skills/ | Skills instaladas |
home/ | HOME por perfil para os subprocessos de ferramenta do Hermes (git, ssh, gh, npm e CLIs de skills) |
cron/ | Definições de tarefas agendadas |
hooks/ | Hooks de eventos |
logs/ | Logs de execução |
skins/ | Skins personalizadas da CLI |
Árvore de instalação imutável
Nas imagens Docker hospedadas e publicadas, /opt/hermes é a árvore da aplicação instalada. Ela pertence ao root e é somente leitura para o usuário de execução hermes, então turnos do agente, sessões de gateway, ações do painel e comandos normais docker exec hermes hermes ... não conseguem editar o código-fonte principal, o .venv embutido, o node_modules nem o bundle da TUI no lugar.
Todo estado mutável do Hermes fica sob /opt/data: configuração, .env, perfis, skills, memórias, sessões, logs, uploads do painel, plugins e outros arquivos gerenciados pelo usuário. A imagem também desabilita a escrita de .pyc em tempo de execução e as instalações preguiçosas de dependências do Hermes dentro de /opt/hermes; dependências opcionais de plataforma necessárias na imagem publicada devem ser embutidas na imagem ou instaladas em uma nova construção de imagem.
Em imagens hospedadas/publicadas, o autoaperfeiçoamento do agente fica limitado a skills, memória, plugins e configuração sob /opt/data. O código-fonte principal instalado sob /opt/hermes é imutável; mudanças no núcleo são feitas por PRs no repositório e entregues atualizando a imagem, não editando ao vivo a instalação em execução.
Se quem opera precisar reparar ou inspecionar arquivos fora de /opt/data, use um shell root de propósito. O shim do hermes normalmente rebaixa docker exec hermes hermes ... de volta para o usuário de execução; defina HERMES_DOCKER_EXEC_AS_ROOT=1 para uma invocação root pontual, quando você precisar mesmo da semântica de root.
CLIs de skills que guardam credenciais sob ~ precisam ser inicializadas contra o HOME do subprocesso, não só contra a raiz do volume de dados. Por exemplo, a skill xurl guarda o estado de OAuth em ~/.xurl; no layout Docker oficial, as chamadas de ferramenta do Hermes leem isso como /opt/data/home/.xurl, então rode a autenticação manual do xurl com HOME=/opt/data/home e confira com HOME=/opt/data/home xurl auth status.
Nunca rode dois contêineres de gateway do Hermes contra o mesmo diretório de dados ao mesmo tempo — arquivos de sessão e armazenamentos de memória não foram feitos para escrita concorrente.
Suporte a vários perfis
O Hermes suporta vários perfis — subdiretórios separados de ~/.hermes/ que deixam você rodar agentes independentes (SOUL, skills, memória, sessões e credenciais diferentes) a partir de uma única instalação. Dentro da imagem Docker oficial, a árvore de supervisão do s6 trata cada perfil como um serviço supervisionado de primeira classe, então a implantação recomendada é um contêiner hospedando todos os perfis.
Cada perfil criado com hermes profile create <nome> ganha:
- Um slot dedicado de serviço s6 em
/run/service/gateway-<nome>/, registrado dinamicamente pelo runtime — sem precisar reconstruir o contêiner. - Reinício automático em caso de queda, com intervalo gerenciado pelo
s6-supervise. - Logs rotacionados por perfil em
${HERMES_HOME}/logs/gateways/<nome>/current(10 arquivos × 1 MB cada). - Persistência de estado entre reinícios do contêiner: o reconciliador de boot lê o
gateway_state.jsonde cada diretório de perfil e sobe de volta apenas os slots cujos últimos estados registrados eramrunning. Só um gateway que você parou explicitamente (hermes gateway stop) continua parado depois de um reinício — um reinício de contêiner, atualização de imagem ou saída inesperada deixa o estado registrado comorunning, então o gateway sobe sozinho no próximo boot.
Os comandos de ciclo de vida que você rodaria no host funcionam do mesmo jeito de dentro do contêiner:
# Cria um perfil — registra o slot s6 gateway-<nome>.
docker exec hermes hermes profile create coder
# Iniciar / parar / reiniciar — dispara o s6-svc; o ciclo de vida do gateway sobrevive ao 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
# Status — informa `Manager: s6 (container supervisor)` dentro do contêiner.
docker exec hermes hermes -p coder gateway status
# Remove um perfil — derruba o slot s6 junto.
docker exec hermes hermes profile delete coder
Por baixo dos panos, hermes gateway start/stop/restart dentro do contêiner é interceptado e roteado para o s6-svc no diretório de serviço certo; você não precisa aprender os comandos do s6 diretamente. Para o estado bruto do supervisor, use /command/s6-svstat /run/service/gateway-<nome> (atenção: /command/ só está no PATH de processos criados pela árvore de supervisão — ao chamar de um docker exec, passe o caminho absoluto).
Alcançar mais de um perfil de fora do contêiner
Duas superfícies diferentes alcançam o gateway de um perfil a partir de fora, e elas se comportam de formas distintas — não confunda as duas:
Hermes Desktop (e o painel web). A conexão Remote Gateway do app Desktop fala com um backend hermes dashboard (padrão porta 9119, habilitada por HERMES_DASHBOARD=1) — não com o servidor de API OpenAI. Um único backend de painel serve todos os perfis que moram junto: o seletor de perfis do app envia o perfil de destino em cada requisição e o backend abre o HERMES_HOME daquele perfil no disco. Ou seja, você não precisa de uma segunda porta — nem de uma segunda conexão — por perfil no Desktop; uma conexão em :9119 cobre todos pelo seletor.
Clientes de API compatíveis com OpenAI (Open WebUI, LobeChat, /v1/...). Esses falam com o servidor de API de cada perfil, que faz bind na porta 8642 para todos os perfis (resolvida a partir de API_SERVER_PORT / platforms.api_server.extra.port — não há alocação automática nem chave config.yaml/gateway.port). Se você quiser que um cliente alcance um segundo perfil específico, dê a esse perfil um API_SERVER_PORT distinto no .env dele, senão o gateway dele também tenta fazer bind na 8642 e conflita com o perfil padrão:
# Cria o perfil (registra o slot s6 gateway-<nome> dele)
docker exec hermes hermes profile create work
# Aponta o servidor de API dele para uma porta livre (escreva no .env do próprio perfil)
cat >> /opt/data/profiles/work/.env <<'EOF'
API_SERVER_ENABLED=true
API_SERVER_PORT=8643
EOF
docker exec hermes hermes -p work gateway restart
Mantenha o API_SERVER_PORT no .env de cada perfil, nunca no bloco environment: do contêiner inteiro — um valor global forçaria todos os perfis para a mesma porta e eles colidiriam. Com rede bridge, publique a porta extra no docker-compose.yml (- "8643:8643"); com network_mode: host ela já fica alcançável no host. A conexão 8642 do perfil padrão continua intacta.
Por que um contêiner com vários perfis, e não vários contêineres
Antes da migração para o s6, "um contêiner por perfil" era o padrão recomendado, porque não havia supervisor dentro do contêiner para gerenciar vários gateways. Com o s6 como PID 1, isso não é mais necessário, e o layout de contêiner único é mais simples em quase todas as dimensões:
| Um contêiner, vários perfis | Um contêiner por perfil | |
|---|---|---|
| Custo de disco | Uma imagem, um venv embutido, um cache do Playwright | N imagens / N caches |
| Custo de memória | Cache do interpretador Python compartilhado, node_modules compartilhado | Duplicado por contêiner |
| Criação de perfil | docker exec ... hermes profile create <nome> (segundos) | Nova invocação de docker run + alocação de porta + configuração de bind-mount |
| Recuperação de queda por perfil | Reinício automático do s6-supervise | --restart unless-stopped do Docker (mais lento, mata o trabalho dos vizinhos) |
| Logs | Arquivo rotacionado por perfil via s6-log, mais log de auditoria do boot do contêiner | docker logs <nome> por contêiner — sem rotação embutida |
| Backup | Um diretório ~/.hermes | N diretórios para coordenar |
O perfil padrão (default) é sempre registrado no primeiro boot, então um contêiner novo já vem com um gateway supervisionado de fábrica. Perfis adicionais são acréscimos puramente de tempo de execução.
Quando você REALMENTE quer um contêiner separado
Perfil dentro do contêiner é o padrão. Rode um contêiner separado por perfil só quando você tiver um motivo específico:
- Isolamento de recursos por carga de trabalho — por exemplo, uma sessão descontrolada da ferramenta de navegador no perfil A não deveria conseguir estourar a memória do perfil B. Contêineres dão
--memory/--cpuspor perfil. - Fixar imagens de forma independente — tags de imagem diferentes por carga de trabalho.
- Segmentação de rede — redes Docker distintas por perfil (por exemplo, uma voltada ao cliente, outra interna).
- Conformidade / raio de dano — credenciais distintas nunca compartilham uma árvore de processos no nível do sistema operacional.
Nesses casos, declare um serviço por perfil, com container_name, volumes e ports distintos:
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
O aviso de Volumes persistentes continua valendo: nunca aponte dois contêineres para o mesmo diretório ~/.hermes ao mesmo tempo. O supervisor s6 dentro de cada contêiner gerencia o próprio conjunto de perfis; compartilhar um volume de dados entre contêineres corrompe arquivos de sessão e armazenamentos de memória.
Onde ficam os logs
O contêiner com s6 tem quatro superfícies distintas de log, e "por que meu gateway não aparece no docker logs" é uma surpresa comum. Cola:
| Origem | Onde cai | Como ler |
|---|---|---|
Gateway por perfil (hermes gateway run e gateways por perfil sob o s6) | Duplicado em dois lugares: docker logs <contêiner> (tempo real, sem prefixo extra) e ${HERMES_HOME}/logs/gateways/<perfil>/current (rotacionado, com carimbo ISO-8601, 10 arquivos × 1 MB cada) | docker logs -f hermes ou tail -F ~/.hermes/logs/gateways/default/current no host |
Painel (quando HERMES_DASHBOARD=1) | docker logs <contêiner> (sem prefixo) | docker logs -f hermes — intercalado com as linhas do gateway |
| Reconciliador de boot (registra quais gateways de perfil foram restaurados a cada início de contêiner) | ${HERMES_HOME}/logs/container-boot.log (log de auditoria só de acréscimo) | tail -F ~/.hermes/logs/container-boot.log |
Logs genéricos do Hermes (agent.log, errors.log) | ${HERMES_HOME}/logs/ (ciente do perfil) | docker exec hermes hermes logs --follow [--level WARNING] [--session <id>] |
Duas consequências práticas que vale conhecer:
- A cópia em arquivo em
logs/gateways/<perfil>/currenté a que sobrevive a reinícios do contêiner. Odocker logssó guarda a saída da vida útil do contêiner atual (e é apagado numdocker rm); os arquivos rotacionados persistem no volume montado. - A linha de auditoria do reconciliador de boot tem o formato
<iso-timestamp> profile=<nome> prior_state=<estado> action=<registered|started>, então um rápidogrep profile=coder ~/.hermes/logs/container-boot.logrevela quando um dado perfil foi restaurado pela última vez e se o s6 o iniciou sozinho.
Encaminhamento de variáveis de ambiente
As chaves de API são lidas de /opt/data/.env dentro do contêiner. Você também pode passar variáveis de ambiente diretamente:
docker run -it --rm \
-v ~/.hermes:/opt/data \
-e ANTHROPIC_API_KEY="sk-ant-..." \
-e OPENAI_API_KEY="sk-..." \
nousresearch/hermes-agent
Flags -e diretas sobrescrevem os valores do .env. Isso é útil em CI/CD ou integrações com gerenciadores de segredos, quando você não quer chaves em disco.
Esta página trata de rodar o próprio Hermes dentro do Docker. Se você quer que o Hermes execute as chamadas de terminal / execute_code do agente dentro de um contêiner-sandbox Docker (um contêiner de vida longa compartilhado entre processos do Hermes — veja a issue #20561), isso é outro bloco de configuração — terminal.backend: docker mais 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 e terminal.docker_orphan_reaper. Veja Configuração → Backend Docker para o conjunto completo, incluindo as regras de ciclo de vida do contêiner.
Exemplo com Docker Compose
Para uma implantação persistente com gateway e painel, um docker-compose.yaml é conveniente:
services:
hermes:
image: nousresearch/hermes-agent:latest
container_name: hermes
restart: unless-stopped
command: gateway run
ports:
- "8642:8642" # API do gateway
- "9119:9119" # painel (só é alcançado quando HERMES_DASHBOARD=1)
volumes:
- ~/.hermes:/opt/data
environment:
- HERMES_DASHBOARD=1
# Descomente para encaminhar variáveis específicas em vez de usar o arquivo .env:
# - 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"
Suba com docker compose up -d e veja os logs com docker compose logs -f. A saída padrão do gateway supervisionado também é duplicada em ${HERMES_HOME}/logs/gateways/<perfil>/current no volume — veja Onde ficam os logs para o mapa completo de roteamento.
Opcional: ponte de áudio para desktop Linux
O modo de voz no Docker precisa de duas coisas separadas para funcionar: o Hermes tem que poder sondar dispositivos de áudio dentro do contêiner, e o contêiner tem que conseguir alcançar o servidor de áudio do host. A montagem abaixo cobre o encanamento de áudio do host para desktops Linux que expõem um socket compatível com PulseAudio, incluindo muitas instalações com PipeWire.
Isto é um contorno para desktop Linux, não um recurso geral do Docker Desktop. É útil quando você já tem o áudio do host funcionando e quer o modo de voz da CLI dentro do contêiner do Hermes. Se o Hermes ainda reportar Running inside Docker container -- no audio devices, use uma build que inclua suporte a sondagem de áudio no Docker para PULSE_SERVER / PIPEWIRE_REMOTE.
Primeiro, crie um arquivo de configuração do ALSA ao lado do seu arquivo do Compose:
pcm.!default {
type pulse
hint {
show on
description "Default ALSA Output (PulseAudio)"
}
}
pcm.pulse {
type pulse
}
ctl.!default {
type pulse
}
Depois construa uma pequena imagem derivada com o plugin PulseAudio do ALSA instalado:
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/*
Use essa imagem no Compose e repasse o socket e o cookie do PulseAudio do usuário do host:
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
Suba com o UID/GID do seu host, para que o processo do contêiner consiga acessar o socket de áudio daquele usuário:
export HERMES_UID="$(id -u)"
export HERMES_GID="$(id -g)"
docker compose up -d --build
Para verificar o que o PortAudio enxerga dentro do contêiner:
docker exec hermes /opt/hermes/.venv/bin/python -c "import sounddevice as sd; print(sd.query_devices())"
Limites de recursos
O contêiner do Hermes precisa de recursos moderados. Mínimos recomendados:
| Recurso | Mínimo | Recomendado |
|---|---|---|
| Memória | 1 GB | 2–4 GB |
| CPU | 1 núcleo | 2 núcleos |
| Disco (volume de dados) | 500 MB | 2+ GB (cresce com sessões/skills) |
A automação de navegador (Playwright/Chromium) é o recurso que mais consome memória. Se você não precisa das ferramentas de navegador, 1 GB basta. Com as ferramentas de navegador ativas, reserve pelo menos 2 GB.
Defina os limites no Docker:
docker run -d \
--name hermes \
--restart unless-stopped \
--memory=4g --cpus=2 \
-v ~/.hermes:/opt/data \
nousresearch/hermes-agent gateway run
O que o Dockerfile faz
A imagem oficial é baseada em debian:13.4 e inclui:
- Python 3.13 com as dependências sincronizadas a partir do arquivo de lock via
uv sync --frozen --no-install-projectpara os extras embutidos (all,messaging, identidade Anthropic/Bedrock/Azure, Hindsight, Matrix), seguido de uma instalação editável do próprio Hermes sem dependências. - Node.js 26 + npm (para automação de navegador, ponte do WhatsApp, bundles de TUI/Desktop e ferramentas de build do workspace)
- Playwright com Chromium (
npx playwright install --with-deps chromium --only-shell) - ripgrep, ffmpeg, git e
xz-utilscomo utilitários de sistema docker-cli— para que agentes rodando dentro do contêiner possam dirigir o daemon Docker do host (faça bind-mount de/var/run/docker.sockpara habilitar) emdocker build,docker run, inspeção de contêineres etc.openssh-client— habilita o backend de terminal SSH de dentro do contêiner. O backend SSH chama o bináriosshdo sistema; sem ele, falhava silenciosamente em instalações conteinerizadas.- A ponte do WhatsApp (
scripts/whatsapp-bridge/) s6-overlayv3 como PID 1 (substitui o antigotini) — supervisiona o painel e os gateways por perfil com reinício automático em caso de queda, recolhe subprocessos zumbis e encaminha sinais.
A imagem trata /opt/hermes como uma árvore de instalação imutável em tempo de execução. Extras opcionais de Python, workspaces Node e assets da TUI que precisem estar disponíveis dentro do Docker têm que ser embutidos durante a construção da imagem; instalações preguiçosas em tempo de execução estão desabilitadas, para que gateways supervisionados e comandos docker exec hermes … não tentem gravar artefatos de dependência de volta na árvore de código somente leitura.
O ENTRYPOINT do contêiner é um pequeno despachante (docker/entrypoint-dispatch.sh). Quando o contêiner é dono do PID 1 (Docker / Podman normais), ele faz exec do /init do s6-overlay e você ganha a árvore de supervisão completa descrita abaixo. Quando uma plataforma envolve o entrypoint da imagem sob o próprio init de PID 1 (Fly.io Machines, docker run --init, algumas montagens de Nomad/Kubernetes), o /init abortaria com s6-overlay-suexec: fatal: can only run as pid 1 — então o despachante roda o bootstrap do stage2 diretamente e faz exec do wrapper principal sem o s6. Nesse caminho alternativo, o comando pedido ainda roda, mas os serviços supervisionados (painel, gateways por perfil) ficam indisponíveis.
No caminho de PID 1, o /init:
- Roda
/etc/cont-init.d/01-hermes-setup(=docker/stage2-hook.sh) como root: remapeamento opcional de UID/GID, corrige a posse do volume, semeia.env/config.yaml/SOUL.mdno primeiro boot, roda migrações não interativas do esquema de configuração a menos queHERMES_SKIP_CONFIG_MIGRATION=1, sincroniza as skills embutidas. - Roda
/etc/cont-init.d/02-reconcile-profiles(=hermes_cli.container_boot): percorre$HERMES_HOME/profiles/<nome>/, recria o slot de serviço s6 do gateway por perfil em/run/service/gateway-<perfil>/e sobe automaticamente só aqueles cujo último estado registrado erarunning(veja Supervisão de gateway por perfil). - Inicia os serviços s6-rc estáticos
main-hermesedashboard. - Faz exec do CMD do contêiner como programa principal (
/opt/hermes/docker/main-wrapper.sh), que roteia os argumentos que o usuário passou para odocker run:- sem argumentos →
hermes(o padrão) - o primeiro argumento é um executável no PATH (por exemplo,
sleep,bash) → faz exec dele diretamente - qualquer outra coisa →
hermes <args>(repasse de subcomando) O contêiner sai quando esse programa principal sai, com o código de saída dele.
- sem argumentos →
O ENTRYPOINT do contêiner agora é o despachante entrypoint-dispatch.sh (que delega ao /init do s6-overlay sob PID 1), e não /usr/bin/tini. Os cinco padrões documentados de invocação do docker run (sem argumentos, chat -q "…", sleep infinity, bash, --tui) se comportam de forma idêntica à imagem baseada em tini. Se você tem um wrapper a jusante que dependia do comportamento de sinais específico do tini ou de uma invocação /usr/bin/tini -- fixa no código, fixe a tag de imagem anterior.
Não sobrescreva o entrypoint da imagem a menos que você mantenha /init (ou, de forma equivalente, o shim legado docker/entrypoint.sh que encaminha para o hook do stage2) na cadeia de comandos. O /init do s6-overlay roda como root para conseguir dar chown no volume no primeiro boot, e depois rebaixa para o usuário hermes via s6-setuidgid em cada serviço supervisionado E no programa principal. Iniciar hermes gateway run como root dentro da imagem oficial é recusado por padrão, porque isso pode deixar arquivos pertencentes ao root em /opt/data e quebrar inícios posteriores do painel ou do gateway. Defina HERMES_ALLOW_ROOT_GATEWAY=1 só quando você aceitar esse risco de propósito.
O docker exec cai automaticamente para o usuário hermes
docker exec hermes <cmd> roda como root dentro do contêiner por padrão, mas a imagem traz um shim fino em /opt/hermes/bin/hermes (o primeiro no PATH) que detecta chamadas como root e refaz o exec de forma transparente por s6-setuidgid hermes. Assim, docker exec hermes login, docker exec hermes profile create …, docker exec hermes setup etc. gravam arquivos pertencentes ao UID 10000 — ou seja, legíveis pelo gateway supervisionado — sem precisar de nenhuma flag --user extra. Chamadas que não são root (os próprios processos supervisionados, docker exec --user hermes, subagentes de kanban dentro do contêiner) caem num atalho que faz exec do binário do venv diretamente, então não há custo nos caminhos quentes.
Se você precisar especificamente de um docker exec que mantenha a semântica de root (sessões de diagnóstico, inspeção de estado só de root, arquivos fora de /opt/data que por acaso pertençam ao root), desative por invocação:
docker exec -e HERMES_DOCKER_EXEC_AS_ROOT=1 hermes <cmd>
O shim aceita 1 / true / yes (sem diferenciar maiúsculas). Qualquer outra coisa — inclusive erros de digitação como =0 — cai no rebaixamento, então não dá para desativar sem perceber. Se o s6-setuidgid não estiver disponível (builds personalizadas que removeram o s6-overlay), o shim se recusa a rodar como root e sai com 126, expondo o modelo de privilégios quebrado em alto e bom som, em vez de regredir para a armadilha histórica em que docker exec hermes login gravava o auth.json como root:root e quebrava a autenticação do gateway supervisionado em toda mensagem de plataforma de chat.
Supervisão de gateway por perfil
Cada perfil criado com hermes profile create <nome> ganha automaticamente um serviço de gateway supervisionado pelo s6, registrado em /run/service/gateway-<nome>/, com reinício automático que preserva o estado entre reinícios do contêiner. Veja Suporte a vários perfis acima para o fluxo do ponto de vista do usuário e os comandos de ciclo de vida.
Vantagens da supervisão em relação à imagem pré-s6:
- Quedas do gateway são reiniciadas automaticamente pelo
s6-supervisedepois de um intervalo de cerca de 1s. - O painel, quando habilitado com
HERMES_DASHBOARD=1, é supervisionado na mesma árvore e recebe o mesmo tratamento de reinício automático. docker restart, atualizações de imagem (docker compose up -d --force-recreate) e saídas inesperadas preservam os gateways em execução: o reconciliador do cont-init lê$HERMES_HOME/profiles/<nome>/gateway_state.jsone sobe o slot de volta se o último estado registrado erarunning. Só umhermes gateway stopexplícito registrastoppede mantém o gateway parado depois do reinício; o SIGTERM do contêiner/s6 enviado em um reinício ou atualização é tratado como "ainda rodando" e o gateway sobe sozinho.- Os logs de gateway por perfil persistem em
$HERMES_HOME/logs/gateways/<perfil>/current(rotacionados pelos6-log), e as ações do reconciliador são acrescentadas a$HERMES_HOME/logs/container-boot.loga cada boot. Veja Onde ficam os logs para o mapa completo de roteamento.
hermes status dentro do contêiner informa Manager: s6 (container supervisor). Use /command/s6-svstat /run/service/gateway-<nome> para a visão bruta do supervisor (atenção: /command/ só está no PATH dos processos da árvore de supervisão; passe o caminho absoluto ao chamar de um docker exec).
Atualizar
Baixe a imagem mais recente e recrie o contêiner. Seu diretório de dados é
preservado, e o contêiner roda migrações não interativas do esquema de
configuração contra o $HERMES_HOME/config.yaml montado antes de iniciar o
gateway. Quando uma migração é necessária, o Hermes primeiro grava backups com
carimbo de tempo ao lado do config.yaml e do .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
Ou com Docker Compose:
docker compose pull
docker compose up -d
Defina HERMES_SKIP_CONFIG_MIGRATION=1 só se você precisar inspecionar ou
migrar a configuração persistida na mão antes de deixar a nova imagem
reescrevê-la.
Skills e arquivos de credenciais
Ao usar o Docker como ambiente de execução (não os métodos acima, mas quando o agente roda comandos dentro de um sandbox Docker — veja Configuração → Backend Docker), o Hermes reaproveita um único contêiner de vida longa para todas as chamadas de ferramenta e faz bind-mount automático do diretório de skills (~/.hermes/skills/) e de quaisquer arquivos de credenciais declarados pelas skills nesse contêiner, como volumes somente leitura. Scripts, modelos e referências das skills ficam disponíveis dentro do sandbox sem configuração manual, e como o contêiner persiste durante toda a vida do processo do Hermes, qualquer dependência que você instale ou arquivo que você escreva continua lá na próxima chamada de ferramenta.
A mesma sincronização acontece nos backends SSH e Modal — skills e arquivos de credenciais são enviados via rsync ou pela API de mount do Modal antes de cada comando.
Instalar mais ferramentas no contêiner
A imagem oficial vem com um conjunto selecionado de utilitários (veja O que o Dockerfile faz), mas nem toda ferramenta que um agente possa querer vem pré-instalada. Há cinco abordagens recomendadas, em ordem crescente de esforço e durabilidade.
Ferramentas de npm ou Python — use npx ou uvx
Para qualquer ferramenta publicada no npm ou no PyPI, instrua o Hermes a rodá-la via npx (npm) ou uvx (Python) e a lembrar esse comando na memória persistente dele. Se a ferramenta precisa de arquivo de configuração ou credenciais, instrua-o a colocá-los sob /opt/data (por exemplo, /opt/data/<ferramenta>/config.yaml).
As dependências são baixadas sob demanda e ficam em cache durante a vida do contêiner. A configuração escrita sob /opt/data sobrevive a reinícios do contêiner, porque vive no diretório do host montado. O cache de pacotes em si é refeito depois de um docker rm, mas npx e uvx rebaixam tudo de forma transparente na próxima vez que a ferramenta rodar.
Outras ferramentas (pacotes apt, binários) — instale e lembre
Para qualquer coisa fora do npm ou do PyPI — pacotes apt, binários pré-compilados, runtimes de linguagem que não estão na imagem — instrua o Hermes sobre como instalar (por exemplo, apt-get update && apt-get install -y <pacote>) e diga a ele para lembrar o comando de instalação. A ferramenta persiste pelo resto da vida do contêiner, e o Hermes vai rodar o comando de instalação de novo depois de um reinício de contêiner, na próxima vez que precisar da ferramenta.
Isso serve bem para ferramentas rápidas de instalar e usadas de vez em quando. Para ferramentas usadas o tempo todo, prefira a abordagem seguinte.
Instalações duráveis — construa uma imagem derivada
Quando uma ferramenta tem que estar disponível na hora, a cada início de contêiner, sem atraso de reinstalação, construa uma imagem nova que herde de nousresearch/hermes-agent e instale a ferramenta em uma camada:
FROM nousresearch/hermes-agent:latest
USER root
RUN apt-get update \
&& apt-get install -y --no-install-recommends <seu-pacote> \
&& rm -rf /var/lib/apt/lists/*
USER hermes
Construa e use no lugar da imagem oficial:
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
O script de entrypoint e a semântica de /opt/data são herdados sem mudanças, então o resto desta página continua valendo. Lembre de reconstruir a imagem quando baixar uma versão mais nova do nousresearch/hermes-agent de origem.
Ferramentas complexas ou pilhas com vários serviços — rode um contêiner sidecar
Para ferramentas que trazem o próprio serviço (um banco de dados, um servidor web, uma fila, uma fazenda de navegadores headless) ou que são pesadas demais para viver dentro do contêiner do Hermes, rode-as como um contêiner separado em uma rede Docker compartilhada. O Hermes alcança o sidecar pelo nome do contêiner, do mesmo jeito que alcança um servidor de inferência local (veja Conectar a servidores de inferência locais).
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
De dentro do contêiner do Hermes, o sidecar é alcançável em http://my-tool:<porta> (ou no protocolo que ele servir). Esse padrão mantém independentes o ciclo de vida, os limites de recursos e o ritmo de atualização de cada serviço, e evita inchar a imagem do Hermes com dependências de que só uma ferramenta precisa.
Ferramentas amplamente úteis — abra uma issue ou pull request
Se uma ferramenta provavelmente é útil para a maioria dos usuários do Hermes Agent, considere contribuir com ela lá em cima em vez de carregá-la em uma imagem derivada privada. Abra uma issue ou pull request no repositório do hermes-agent descrevendo a ferramenta e o caso de uso. Ferramentas que entram na imagem oficial beneficiam todo mundo e evitam o custo de manutenção de um fork.
Conectar a servidores de inferência locais (vLLM, Ollama etc.)
Quando você roda o Hermes no Docker e o seu servidor de inferência (vLLM, Ollama, text-generation-inference etc.) também roda no host ou em outro contêiner, a rede exige atenção extra.
Docker Compose (recomendado)
Coloque os dois serviços na mesma rede Docker. Essa é a abordagem mais confiável:
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
Depois, no seu ~/.hermes/config.yaml, use o nome do contêiner como hostname:
model:
provider: custom
model: my-model
base_url: http://vllm:8000/v1
api_key: "none"
- Use o nome do contêiner (
vllm) como hostname — nãolocalhostnem127.0.0.1, que se referem ao próprio contêiner do Hermes. - O valor de
modeltem que bater com o--served-model-nameque você passou ao vLLM. - Defina
api_keycomo qualquer string não vazia (o vLLM exige o cabeçalho, mas não o valida por padrão). - Não coloque barra no final da
base_url.
Docker run avulso (sem Compose)
Se o seu servidor de inferência roda direto no host (não no Docker), use host.docker.internal no macOS/Windows, ou --network host no 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 (rede do host):
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, a flag -p é ignorada — todas as portas do contêiner ficam expostas diretamente no host.Verificar a conectividade
De dentro do contêiner do Hermes, confirme que o servidor de inferência está alcançável:
docker exec hermes curl -s http://vllm:8000/v1/models
Você deve ver uma resposta JSON listando o modelo servido. Se falhar, confira:
- Os dois contêineres estão na mesma rede Docker (
docker network inspect hermes-net) - O servidor de inferência está escutando em
0.0.0.0, não em127.0.0.1 - O número da porta bate
Ollama
O Ollama funciona do mesmo jeito. Se o Ollama roda no host, use host.docker.internal:11434 (macOS/Windows) ou 127.0.0.1:11434 (Linux com --network host). Se o Ollama roda no próprio contêiner na mesma rede Docker:
model:
provider: custom
model: llama3
base_url: http://ollama:11434/v1
api_key: "none"
Resolução de problemas
O contêiner sai na hora
Confira os logs: docker logs hermes. Causas comuns:
- Arquivo
.envausente ou inválido — rode de forma interativa primeiro para concluir a configuração - Conflitos de porta, se estiver rodando com portas expostas
Erros de "Permission denied"
O hook de stage2 do contêiner rebaixa privilégios para o usuário não root hermes (UID 10000) via s6-setuidgid dentro de cada serviço supervisionado. Se o seu ~/.hermes/ no host pertence a outro UID, defina HERMES_UID/HERMES_GID — ou os apelidos PUID/PGID, para paridade com as imagens da LinuxServer.io e de NAS — para bater com o seu usuário do host, ou garanta que o diretório de dados tenha permissão de escrita:
chmod -R 755 ~/.hermes
Em um NAS (UGOS, Synology, unRAID), o diretório de dados normalmente é um bind mount pertencente a um UID do host que o contêiner não consegue mudar com chown. Defina PUID/PGID (ou HERMES_UID/HERMES_GID) com esse usuário do host, para que o runtime rode como dono do mount em vez de 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> também cai automaticamente para o UID 10000 — veja O docker exec cai automaticamente para o usuário hermes para os detalhes e a forma de desativar por invocação.
"Permission denied" em todo docker exec (diretório de instalação travado em 0700)
Imagens construídas antes do fim de agosto de 2026 tinham um bug: escrever um arquivo de credenciais diretamente sob /opt/hermes restringia aquele diretório a 0700, trancando o usuário hermes (UID 10000) para fora da árvore de instalação. A partir daí, todo docker exec novo falha com Permission denied.
Baixar uma imagem mais nova e recriar o contêiner resolve isso de vez (o diretório de instalação vem como 0755 e as versões atuais não o restringem mais). Se você precisa recuperar um contêiner em execução no lugar, sem recriá-lo:
docker exec -u root hermes chmod 0755 /opt/hermes
Ferramentas de navegador não funcionam
O Playwright precisa de memória compartilhada. Adicione --shm-size=1g ao seu comando de docker run:
docker run -d \
--name hermes \
--shm-size=1g \
-v ~/.hermes:/opt/data \
nousresearch/hermes-agent gateway run
O gateway não reconecta depois de problemas de rede
A flag --restart unless-stopped resolve a maioria das falhas transitórias. Se o gateway travou, reinicie o contêiner:
docker restart hermes
Verificar a saúde do contêiner
docker logs --tail 50 hermes # Logs recentes
docker run -it --rm nousresearch/hermes-agent:latest version # Confere a versão
docker stats hermes # Uso de recursos