Skip to main content
Esta página roda o servidor ChatCLI em container e no Kubernetes com o Helm chart do servidor. Para servidores gerenciados pelo operator (recursos Instance e o pipeline de AIOps), veja K8s Operator.
Todas as receitas abaixo definem uma credencial. Um servidor que escuta além do loopback (qualquer container com porta publicada, todo pod no Kubernetes) se recusa a subir sem token compartilhado, material JWT ou CA de cliente e, por padrão, só avisa no arquivo de log. Veja a regra da credencial.

Imagens

As duas são multi-arch (linux/amd64, linux/arm64), trazem atestados de SBOM e proveniência e são assinadas com cosign (keyless):
A imagem do servidor contém /usr/local/bin/chatcli (carimbado com a versão do release, que o GetServerInfo e o operator informam), /usr/local/bin/grpc-health-probe, ENTRYPOINT ["chatcli", "server"], EXPOSE 50051 e um HEALTHCHECK que roda grpc-health-probe -addr=:50051 em texto puro a cada 30 segundos. Argumentos depois do nome da imagem são flags do chatcli server. O Devin CLI não está na imagem, então DEVIN não é provedor do servidor.

Build local

Sem --build-arg VERSION=… o binário informa a versão dev.

Docker

Fora do Kubernetes o servidor faz bind em 127.0.0.1, o que dentro de um container significa que nada de fora o alcança. Publicar uma porta, portanto, exige CHATCLI_BIND_ADDRESS=0.0.0.0, e esse bind exige uma credencial.
Num container o servidor escreve o log no stderr em linhas JSON, então o docker logs carrega os erros de inicialização e tudo o que vem depois sem configuração extra (CHATCLI_ENV=dev troca para o console colorido de desenvolvimento). Confira e conecte (o listener é texto puro, então o cliente precisa de CHATCLI_ALLOW_INSECURE=true):
Para um teste rápido sem credencial, deixe o servidor no loopback dentro do container e use-o só de lá: omita CHATCLI_BIND_ADDRESS e as flags -p.

TLS no Docker

O HEALTHCHECK embutido testa texto puro, então com TLS ele reporta unhealthy. A imagem não tem shell e o docker run --health-cmd sempre passa por um, então desligue (--no-healthcheck) ou use uma checagem em forma exec no Compose (abaixo). O certificado do servidor precisa de localhost nos SANs para o cliente acima; veja TLS para uma receita de certificado.

Docker Compose

O docker-compose.yml do repositório faz o build da imagem a partir do código e roda endurecido (sistema de arquivos raiz somente leitura, no-new-privileges, tmpfs de 100 MB em /tmp, 2 CPUs / 1 GB). Ele faz bind em 0.0.0.0 e lê todas as variáveis de provedor do seu shell, então exporte uma credencial antes:
O compose define HOME=/home/nonroot e mantém o home inteiro num volume nomeado, chatcli-home, montado em /home/nonroot: o ~/.chatcli (sessões, o hub de conversas, memória, plugins, o arquivo de log) sobrevive a restarts, e o volume é gravável com a raiz somente leitura porque o Docker o inicializa com o dono do home da imagem. Coloque binários de plugin em ~/.chatcli/plugins dentro desse volume. O HEALTHCHECK da imagem checa em texto puro. Com TLS no servidor, troque-o num override; o Compose mescla o docker-compose.override.yml automaticamente:
Só CHATCLI_FALLBACK_PROVIDERS liga a cadeia de fallback (não existe chave separada para ligar), e ela precisa listar o provedor primário primeiro. O compose também repassa do seu shell CHATCLI_FALLBACK_MAX_RETRIES (padrão 2), CHATCLI_FALLBACK_COOLDOWN_BASE (30s) e CHATCLI_FALLBACK_COOLDOWN_MAX (5m).

Kubernetes (Helm)

O chart do servidor é publicado como artefato OCI em oci://ghcr.io/diillson/charts/chatcli. A versão do chart e a da imagem do servidor são o mesmo número, e o appVersion do chart fixa a imagem.

Pré-requisitos

  • Kubernetes 1.30+, kubectl apontando para o cluster
  • Helm 3.8+ (suporte a OCI), Helm 4 incluído
  • Uma chave de provedor LLM (ou IRSA / Workload Identity para Bedrock)

Instalação

1

Instale com uma credencial

Dentro de um pod o servidor faz bind em 0.0.0.0, então server.token (ou JWT / mTLS, abaixo) é obrigatório. O chart guarda o token no próprio Secret e o entrega como CHATCLI_SERVER_TOKEN via secretKeyRef, nunca como argumento de linha de comando. Num pod o servidor também escreve o log no stderr, então o kubectl logs o mostra.
2

Espere subir

3

Conecte

Os nomes dos objetos seguem o release: um release com nome diferente de chatcli gera <release>-chatcli (Deployment, Service e Secret). O helm install imprime os comandos exatos nas notas e avisa quando não há credencial.
O que o chart cria: um Deployment (UID 1000 não root, raiz somente leitura, todas as capabilities removidas, seccomp RuntimeDefault), um Service, um ConfigMap e um Secret carregados com envFrom, um ServiceAccount e RBAC para o watcher, um PVC de 1 Gi para sessões (persistence.enabled, ligado por padrão), os 17 CRDs de AIOps mais um hook pre-install/pre-upgrade que os reaplica (crdUpgrade.enabled) e, opcionalmente, Ingress, HPA, PDB, NetworkPolicy e ServiceMonitor.

Probes

Probe grpc do kubelet não faz TLS, então o chart usa: Com server.metricsPort: 0, startup e liveness caem para uma checagem TCP na porta gRPC. Esses probes funcionam com e sem TLS.

Credenciais

security.jwtIssuer / security.jwtAudience adicionam checagem de iss / aud; security.mtlsRole define a role de quem se identifica só por certificado (viewer/readonly, user/operator, admin; padrão user). Prefira os valores *Ref: os inline vão parar no ambiente do Deployment.

TLS

Com tls.existingSecret, certFile e keyFile assumem /etc/chatcli/tls/tls.crt e /etc/chatcli/tls/tls.key; defina-os só quando os arquivos do seu Secret tiverem outros nomes. Sem Secret, defina os dois caminhos (para arquivos que você monta com extraVolumes / extraVolumeMounts).
tls.enabled: true sem tls.existingSecret e sem certFile e keyFile juntos faz o helm install / helm upgrade falhar (tls.enabled=true needs tls.existingSecret ... or both tls.certFile and tls.keyFile), em vez de subir um servidor que escutaria em texto puro.
Para mTLS com uma CA de cliente no mesmo Secret use security.tlsClientCA: /etc/chatcli/tls/ca.crt; para um Secret de CA separado, monte-o com extraVolumes / extraVolumeMounts (exemplo em Valores de segurança). Pelo kubectl port-forward o certificado precisa de localhost nos SANs.

Referência de valores

Servidor

LLM

Com secrets.existingSecret, crie o Secret com nomes de variáveis como chaves, a credencial inclusive:

Fallback de provedor

O chart coloca o llm.provider (com llm.model) na frente, a menos que você mesmo o liste, porque o servidor monta a cadeia só a partir da lista e só a instala com dois ou mais provedores funcionando. Coloque a chave de cada provedor no Secret. O fallback.enabled só decide se o chart grava CHATCLI_FALLBACK_PROVIDERS; o chart não grava nenhuma variável separada para ligar, já que o servidor não lê nenhuma.

MCP

K8s watcher

Um alvo sem kind é um Deployment; um alvo sem namespace observa o default. Alvos fora do namespace do release, ou em vários namespaces, mudam o RBAC do chart para ClusterRole automaticamente, e o mesmo vale para um watcher.namespace de alvo único diferente do namespace do release (vazio conta como default).

Armazenamento, memória, pipeline e recursos

O chart monta um emptyDir em /home/chatcli/.chatcli e /tmp e define HOME=/home/chatcli, então a raiz somente leitura funciona. O servidor lê os ConfigMaps de MCP, agents, skills e bootstrap só no startup. Cada um que o chart renderiza (a partir de mcp.servers ou *.definitions) entra no hash de uma anotação checksum/<nome> do pod, então um helm upgrade que o altera recria os pods. Um ConfigMap que você mesmo gerencia (*.existingConfigMap) não pode ser hasheado pelo Helm: depois de editar um, rode kubectl -n chatcli rollout restart deploy/chatcli. agents, skills ou bootstrap ligados sem definitions nem existingConfigMap não montam nada, e o servidor encontra um diretório vazio (charts anteriores montavam um ConfigMap que não existia, deixando o pod em ContainerCreating).

Rollouts no volume de sessões

Com persistence.enabled e sem o modo de acesso ReadWriteMany, um rollout para o pod antigo antes de subir o novo: o chart renderiza RollingUpdate com maxSurge: 0 e maxUnavailable: 1. Com o padrão do API server (um pod extra, nenhum indisponível), um pod novo agendado em outro nó não conseguia anexar o volume ReadWriteOnce enquanto o antigo o segurava, e o antigo só parava quando o novo ficava pronto, então o rollout travava. Parar o pod antigo primeiro troca isso por uma curta interrupção a cada rollout. Sem persistência, ou com ReadWriteMany, nada é renderizado e vale o padrão do API server. Uma strategy explícita vence sempre; type: Recreate sem rollingUpdate é renderizado com rollingUpdate: null. Uma instalação nova, um release do Helm 3 e um release já atualizado uma vez no padrão do chart podem trocar para Recreate direto. Um release instalado pelo Helm 4 num chart anterior (até 1.211.x, que não renderizava estratégia) precisa antes de um upgrade no padrão, porque o server-side apply do Helm 4 não consegue remover o bloco rollingUpdate preenchido por padrão (o upgrade falha com spec.strategy.rollingUpdate: Forbidden), ou deste patch:
Um volume ReadWriteOnce anexa a um nó por vez, então várias réplicas nele só funcionam enquanto todas rodam nesse nó; pods agendados em outro lugar ficam em ContainerCreating. Rode uma réplica, ou use uma storage class ReadWriteMany para várias. ReadWriteOncePod admite um único pod: o render falha quando replicaCount (ou autoscaling.maxReplicas com o HPA ligado) passa de 1.

Memória no PVC de sessões

Com memory.enabled e persistência, as sessões ficam na raiz do PVC (montada em ~/.chatcli/sessions) e a memória fica no diretório memory.subPath do mesmo PVC (montado em ~/.chatcli/memory). O kubelet cria esse diretório na primeira montagem, e o podSecurityContext.fsGroup padrão o torna gravável. Charts anteriores (até 1.211.x) montavam a memória também na raiz do PVC, então os stores JSON dela ficavam entre os arquivos de sessão, onde a lista de sessões os mostrava e a expiração de sessões podia apagá-los. O upgrade de uma instalação que rodava com memória e persistência migra sozinho. Nenhuma sessão muda de lugar. O chart define CHATCLI_MEMORY_LEGACY_DIR=/home/chatcli/.chatcli/sessions (a raiz do PVC como o servidor a vê) sempre que persistência, memória e um memory.subPath estão ligados, e na primeira vez que o servidor abre a memória no layout novo ele copia de lá os arquivos da própria memória para o diretório de memória:
  • só os arquivos da memória (MEMORY.md e seus backups, memory_index.json, memory_tombstones.json, episodes.json, user_profile.json, topics.json, projects.json, usage_stats.json, graph.json, vector_index.json, memory_archive.json, compactor_state.json, as quarentenas .corrupt deles, as notas diárias YYYYMM/, weekly/, monthly/ e pending/); os arquivos de sessão ficam onde estão;
  • só cópia: nada na raiz é movido, apagado ou reescrito, e um arquivo que já existe no diretório de memória nunca é sobrescrito; cada arquivo chega de forma atômica com modo 0600;
  • uma vez: roda só enquanto o diretório de memória não tem nenhum arquivo da memória, e grava o marcador .migrated-from-legacy lá ao terminar;
  • um arquivo ilegível é pulado com um aviso; um arquivo selado com CHATCLI_ENCRYPTION_KEY é selado de novo para o caminho novo, e enquanto a chave falta ou está errada a cópia fica pendente (.migrating-from-legacy) e retoma no próximo start;
  • o log registra memory: adopted the legacy memory directory.
memory.subPath: "" mantém o layout compartilhado antigo (sem cópia, sem CHATCLI_MEMORY_LEGACY_DIR). Um upgrade só com --reuse-values não traz a chave memory.subPath e também mantém o layout antigo; --reset-then-reuse-values pega o novo padrão.

Segurança

Service, ingress, network policy

Escala e monitoramento

O schema de valores rejeita chaves desconhecidas, então um valor com erro de digitação falha a instalação em vez de ser ignorado.

Expondo o servidor

Com className: nginx o chart define como padrão backend-protocol: GRPC (texto puro até o pod) e ssl-redirect: "true". Uma chave que você define em ingress.annotations substitui o padrão em vez de ser renderizada duas vezes, então para TLS de ponta a ponta pelo nginx (com tls.* no servidor) adicione nginx.ingress.kubernetes.io/backend-protocol: GRPCS.

Logs, health e métricas

Num pod o servidor escreve toda entrada de log no stderr, em linhas JSON, que é o que o kubectl logs e o seu coletor de logs leem; o arquivo rotativo em /home/chatcli/.chatcli/app.log (um emptyDir, sem shell para ler) é uma cópia que morre com o pod. CHATCLI_LOG_STDERR=false no extraEnv desliga a cópia no stderr. Com a raiz somente leitura padrão, /home/chatcli/.chatcli é um emptyDir limitado a 200Mi, e um volume acima do limite faz o pod ser despejado. Os padrões de rotação do próprio servidor (100 MB, 3 backups: até 400 MB) não cabem, então o chart define uma rotação que cabe, no máximo 80 MB de log: Enquanto esse emptyDir está em uso, o render falha quando maxSizeMB × (maxBackups + 1) passa de 100 MB. Um campo definido como null não renderiza variável (vale o padrão do servidor, que conta como tal na checagem). Uma entrada de mesmo nome no extraEnv vence o valor do chart, e uma para o tamanho ou os backups pula a checagem.
  • Uma mudança no Secret ou ConfigMap gerenciado pelo chart recria os pods (anotações de checksum), incluindo os ConfigMaps de MCP, agents, skills e bootstrap. Uma mudança no secrets.existingSecret ou num *.existingConfigMap não: rode kubectl -n chatcli rollout restart deploy/chatcli.
  • Com persistência num volume sem ReadWriteMany, cada rollout para o pod antigo antes de subir o novo: espere uma curta interrupção (veja Rollouts no volume de sessões).
  • Se o chart do servidor e o chart chatcli-operator estiverem instalados, mantenha os dois na mesma versão: cada um reaplica a própria cópia dos CRDs.
  • O helm uninstall apaga o PVC de sessões; faça backup antes. Os CRDs ficam; apagá-los apaga todos os recursos daqueles tipos.

Solução de problemas

Veja também a tabela de problemas do servidor.

Próximos passos

Modo Servidor

Flags, auth, limites, operação

Conexão Remota

Conecte ao servidor

K8s Watcher

Monitore workloads