Rodando o Hermes Agent no Kubernetes: sem Helm chart oficial, uma réplica só e um contêiner que inicia como root


Você deixou o Hermes funcionando que é uma beleza no seu laptop — memória, skills, cron jobs, tudo. Agora você está o movendo para um cluster, então escreve um Deployment perfeitamente razoável: replicas: 3, atualizações rolling, runAsNonRoot: true, dados em NFS. Os pods sobem, e aí começa o comportamento estranho. O histórico de sessão não bate. Os contêineres saem no boot. Você muda um ConfigMap e nada acontece.

O Hermes roda bem no Kubernetes — depois que você aceita que algumas coisas no seu contêiner colidem de frente com os padrões de cluster. A documentação oficial do Docker já descreve essas coisas; o que ela não faz é dizer qual frase importa num contexto de Kubernetes. Aqui vão elas na ordem em que vão realmente te morder.

Regra um: um HERMES_HOME, um writer

Comece pelo fato do qual tudo o resto depende: todo o estado mutável do Hermes vive sob HERMES_HOME — config, arquivos de memória, skills e histórico de sessão num banco SQLite local. Na imagem oficial, esse caminho é /opt/data, que a documentação do Docker chama de fonte única de verdade de todo o estado do Hermes.

A documentação então diz algo direto: nunca rode dois contêineres gateway do Hermes contra o mesmo diretório de dados ao mesmo tempo — arquivos de sessão e stores de memória não foram feitos para escrita concorrente. A mesma página explica por quê: o arquivo PID do gateway e o nome do serviço systemd são ambos delimitados por HERMES_HOME, o que é o próprio sinal da upstream de que essa é a unidade de isolamento.

Traduzindo para termos de Kubernetes:

  • replicaCount fica em 1 sempre que a persistência está ligada;
  • a estratégia de update precisa ser Recreate, nunca RollingUpdate — um rolling update roda de propósito o pod antigo e o novo ao mesmo tempo, que é exatamente a janela de dois writers que você quer evitar;
  • prefira ReadWriteOnce no PVC. RWX não é automaticamente inseguro — o invariante real é um writer ativo, não um mount — mas RWO impõe isso na camada de storage em vez de confiar no seu manifest.

Precisa de mais de um time? Isso é uma release por tenant, cada uma com o seu próprio volume, não mais réplicas por release.

Regra dois: mantenha o SQLite fora de um filesystem de rede entre VMs

Essa é esquecida com mais frequência do que contagens de réplica, porque falha em silêncio. O Hermes abre o state.db em modo WAL do SQLite por padrão, e o WAL depende de um arquivo de memória compartilhada (state.db-shm) que se mantém coerente entre todo processo que tem o banco aberto. Mounts que cruzam a fronteira de uma VM — virtiofs no macOS, 9p / drvfs no Windows — deixam writers concorrentes corromperem um banco WAL em silêncio, enquanto o arquivo principal ainda passa num integrity check.

Num cluster, o equivalente é uma storage class NFS ou CIFS. Use block storage local ao node (as classes de SSD block da nuvem) para o volume de dados; se não puder, siga a receita deste guia para converter o journal mode para DELETE e fixá-lo. Não trate “o mount funciona” como “o banco está seguro”.

Regra três: o contêiner inicia como root — o agent não

Dizer “a imagem oficial precisa rodar como root” está errado, mas é impreciso de um jeito que importa quando você está discutindo uma isenção de Pod Security Standard com o time de plataforma.

A imagem usa o s6-overlay como init system. Seu /init roda como root para poder dar chown no volume no primeiro boot, e depois desce para o usuário hermes via s6-setuidgid para o programa principal e todo serviço supervisionado. A imagem até traz um shim que detecta chamadas de root em docker exec e re-executa via s6-setuidgid hermes.

Então só o bootstrap é root; o agent não. Isso ainda basta para quebrar um security context restrito: um pod com runAsNonRoot: true ou um runAsUser diferente de zero falha antes mesmo de o agent iniciar, com algo assim:

/package/admin/s6-overlay/libexec/preinit: fatal: /run belongs to uid 0 instead of 1000,
has insecure and/or unworkable permissions, and we're lacking the privileges to fix it.
s6-overlay-suexec: fatal: child failed with exit code 100

Um detalhe que exemplos publicados erram com frequência: o usuário hermes é UID 10000, não 1000. A imagem o cria com useradd -u 10000 -m -d /opt/data hermes e não define nenhuma instrução USER — o contêiner inicia como root de propósito e derruba os privilégios sozinho. Defina fsGroup: 1000 e o seu volume acaba dono do grupo errado.

Um ponto de partida que funciona (trate a lista de capabilities como um checklist, já que as necessidades do s6 mudam entre versões da imagem):

podSecurityContext:
  runAsUser: 0
  runAsNonRoot: false
  fsGroup: 10000
  fsGroupChangePolicy: OnRootMismatch
  seccompProfile:
    type: RuntimeDefault
containers:
  - name: hermes
    securityContext:
      allowPrivilegeEscalation: true
      readOnlyRootFilesystem: false
      runAsNonRoot: false
      capabilities:
        drop: ["ALL"]
        add: ["CHOWN", "SETUID", "SETGID"]

Isso é bem mais estreito que um pod privilegiado: largue tudo e readicione só o que o bootstrap precisa. Se o seu cluster impõe o Pod Security Standard restricted, essa carga precisa de uma isenção de namespace — você não deixou passar nenhuma configuração, a imagem simplesmente não foi feita para iniciar non-root.

Regra quatro: quando a plataforma embrulha o PID 1, a supervisão do s6 fica muda

O entrypoint da imagem é um dispatcher, e o suexec do s6-overlay exige ser PID 1. Quando uma plataforma embrulha o entrypoint sob o seu próprio init, o caminho documentado pela upstream é que o dispatcher roda o bootstrap de stage two diretamente e executa o wrapper principal sem s6. A documentação cita docker run --init e “alguns setups de Nomad/Kubernetes” como os casos que caem nisso.

Duas consequências importam:

  • Se algo na sua stack injeta um processo init — um pod com shareProcessNamespace: true, certos injectors de sidecar, um runtime configurado para adicionar um — você perde a supervisão do s6 em silêncio. O agent continua rodando; só não está mais sendo reiniciado pelo s6.
  • Isso não é necessariamente ruim. Num cluster, você geralmente quer que a restart policy do Kubernetes seja o supervisor, e não um process manager dentro do contêiner. Mas o modelo de falha muda, então saiba em qual caminho você está. Se você quer o comportamento sem s6 de propósito, peça por ele com --no-supervise ou HERMES_GATEWAY_NO_SUPERVISE=1 em vez de contar com um acidente no tratamento do PID 1. Os dois são no-ops fora da imagem s6.

Regra cinco: mudanças de config não têm endpoint HTTP de reload

Essa é a chata para GitOps. O Hermes te dá /reload-mcp (recarrega servidores MCP a partir do config.yaml) e /reload-skills (reescaneia o diretório de skills) dentro de uma conversa, mas o servidor de API expõe nenhum endpoint HTTP equivalente. A solicitação de recurso da comunidade exatamente para isso, a issue #52417, continua aberta com prioridade baixa.

Então quando o seu pipeline atualiza um ConfigMap, não existe hook de reconciliação nativo. As suas opções realistas são acionar o caminho de reload em runtime (rodar o slash command numa sessão) ou reiniciar a carga — e reiniciar te traz de volta à restrição de Recreate, não RollingUpdate.

E sim: não existe Helm chart oficial

A documentação da upstream cobre install.sh, Docker, Docker Compose e pacotes Nix. Kubernetes simplesmente não é um alvo de deploy prometido, e a solicitação de recurso por uma história oficial de empacotamento — Dockerfile, Helm chart, RBAC e smoke tests de uma vez — é a issue #85474, aberta em agosto de 2026 e ainda aberta.

Existem tentativas da comunidade, como ultraworkers/hermes-agent-helm-chart (cerca de 140 stars, último push em abril), além de alguns operators pessoais. Nenhuma delas é mantida pela Nous Research, e as versões de chart avançam independentemente do Hermes. Se você usar uma, leia os templates de ponta a ponta, sobrescreva o security context você mesmo e confira contra as cinco regras acima — qualquer uma delas faltando em silêncio é um risco de corrupção de dados esperando por um rollout ruim.

Checklist de um deployment mínimo

  • Uma release, um PVC, ReadWriteOnce, montado em /opt/data;
  • replicas: 1 e strategy.type: Recreate;
  • o volume de dados em block storage local ao node, nunca NFS;
  • aceite root no bootstrap, use fsGroup: 10000, largue todas as capabilities e readicione apenas CHOWN, SETUID, SETGID;
  • decida de propósito se você quer a supervisão do s6 e diga isso com --no-supervise ou HERMES_GATEWAY_NO_SUPERVISE=1;
  • trate mudanças de config como restart-ou-slash-command, nunca como hot reload via HTTP;
  • dimensione os recursos a partir da orientação oficial do Docker e deixe folga para automação de browser (Playwright/Chromium), que é a coisa que mais consome recursos no Hermes.

A versão curta

O Hermes roda de forma estável no Kubernetes desde que você o trate como um serviço stateful single-writer, e não como um app stateless doze-fatores. Escreva essas restrições nos seus manifests e na documentação do seu time — writer único, storage local ao node, root no bootstrap, sem reload HTTP — e o resto é como qualquer outra carga stateful.

Se você ainda está num único host Docker, compartilhar um contêiner entre perfis dá menos trabalho. E se você ainda não instalou, comece pelo guia de instalação.