> ## Documentation Index
> Fetch the complete documentation index at: https://chatcli.edilsonfreitas.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Deploy com Docker e Kubernetes

> Rode o servidor ChatCLI com Docker, Docker Compose ou o Helm chart: imagens, comandos que funcionam, credenciais, TLS, probes, logs, upgrades e solução de problemas.

Esta página roda o [servidor ChatCLI](/pt/server/server-mode) 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](/pt/kubernetes/k8s-operator).

<Warning>
  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](/pt/server/server-mode#endereço-de-bind-e-a-regra-da-credencial).
</Warning>

## Imagens

| Imagem | Tags | Runtime |
| - | - | - |
| `ghcr.io/diillson/chatcli` | `<versão>` (sem `v`, ex.: `1.214.0`) e `latest` | `gcr.io/distroless/static-debian12:nonroot`, UID 65532, sem shell |
| `ghcr.io/diillson/chatcli-operator` | `<versão>` e `latest` | Alpine (o `SourceRepository` precisa de git e, com `authType: ssh`, de `openssh-client`), UID 65532 |

```bash theme={"system"}
docker pull ghcr.io/diillson/chatcli:1.214.0
docker pull ghcr.io/diillson/chatcli-operator:1.214.0
```

As duas são multi-arch (`linux/amd64`, `linux/arm64`), trazem atestados de SBOM e proveniência e são assinadas com cosign (keyless):

```bash theme={"system"}
cosign verify ghcr.io/diillson/chatcli:1.214.0 \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp 'https://github.com/diillson/chatcli/'
```

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

```bash theme={"system"}
# Imagem do servidor (a partir da raiz do repositório)
docker build -t chatcli:dev --build-arg VERSION=dev .

# Imagem do operator: também da raiz (o operator/go.mod substitui o módulo raiz por ../)
docker build -f operator/Dockerfile -t chatcli-operator:dev .
```

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.

```bash theme={"system"}
export CHATCLI_SERVER_TOKEN="$(openssl rand -hex 32)"
export ANTHROPIC_API_KEY=sk-ant-xxx

docker run -d --name chatcli \
  -p 50051:50051 -p 9090:9090 \
  -e CHATCLI_BIND_ADDRESS=0.0.0.0 \
  -e CHATCLI_SERVER_TOKEN \
  -e LLM_PROVIDER=CLAUDEAI \
  -e ANTHROPIC_API_KEY \
  -v chatcli-home:/home/nonroot \
  ghcr.io/diillson/chatcli:1.214.0
```

| Configuração | Por quê |
| - | - |
| `CHATCLI_BIND_ADDRESS=0.0.0.0` | escutar na interface do container para a porta publicada funcionar |
| `CHATCLI_SERVER_TOKEN` | a credencial que esse bind exige (ou `CHATCLI_JWT_SECRET` / `CHATCLI_JWT_PUBLIC_KEY`) |
| `-v chatcli-home:/home/nonroot` | manter o `~/.chatcli` (sessões, hub de conversas, memória, plugins, arquivo de log) entre restarts; o home da imagem é `/home/nonroot`, e um volume nomeado ali herda o dono do diretório |
| `-p 9090:9090` | opcional: `/metrics` e `/healthz` |

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`):

```bash theme={"system"}
docker logs chatcli | grep listening          # 🚀 ChatCLI server listening on 0.0.0.0:50051
docker inspect --format '{{.State.Health.Status}}' chatcli   # healthy
curl -s localhost:9090/healthz                 # ok

CHATCLI_ALLOW_INSECURE=true chatcli connect localhost:50051 --token "$CHATCLI_SERVER_TOKEN"
```

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

```bash theme={"system"}
# tls/ contém server.crt, server.key e ca.crt, legíveis pelo UID 65532
docker run -d --name chatcli -p 50051:50051 \
  -e CHATCLI_BIND_ADDRESS=0.0.0.0 -e CHATCLI_SERVER_TOKEN \
  -e LLM_PROVIDER=CLAUDEAI -e ANTHROPIC_API_KEY \
  -e CHATCLI_SERVER_TLS_CERT=/etc/chatcli/tls/server.crt \
  -e CHATCLI_SERVER_TLS_KEY=/etc/chatcli/tls/server.key \
  -v "$PWD/tls:/etc/chatcli/tls:ro" \
  -v chatcli-home:/home/nonroot \
  --no-healthcheck \
  ghcr.io/diillson/chatcli:1.214.0

chatcli connect localhost:50051 --tls --ca-cert tls/ca.crt --token "$CHATCLI_SERVER_TOKEN"
```

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](/pt/server/server-mode#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:

```bash theme={"system"}
git clone https://github.com/diillson/chatcli.git && cd chatcli
export CHATCLI_SERVER_TOKEN="$(openssl rand -hex 32)"
export LLM_PROVIDER=CLAUDEAI ANTHROPIC_API_KEY=sk-ant-xxx
docker compose up -d --build

CHATCLI_ALLOW_INSECURE=true chatcli connect localhost:50051 --token "$CHATCLI_SERVER_TOKEN"
```

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:

```yaml theme={"system"}
# docker-compose.override.yml
services:
  chatcli-server:
    # Forma exec, sem shell:
    healthcheck:
      test: ["CMD", "/usr/local/bin/grpc-health-probe", "-addr=:50051", "-tls",
             "-tls-ca-cert", "/etc/chatcli/tls/ca.crt", "-tls-server-name", "localhost"]
```

Só `CHATCLI_FALLBACK_PROVIDERS` liga a [cadeia de fallback](/pt/server/server-mode#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

<Steps>
  <Step title="Instale com uma credencial">
    ```bash theme={"system"}
    helm install chatcli oci://ghcr.io/diillson/charts/chatcli \
      --version 1.214.0 \
      --namespace chatcli --create-namespace \
      --set llm.provider=CLAUDEAI \
      --set secrets.anthropicApiKey="$ANTHROPIC_API_KEY" \
      --set server.token="$(openssl rand -hex 32)"
    ```

    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.
  </Step>

  <Step title="Espere subir">
    ```bash theme={"system"}
    kubectl -n chatcli rollout status deploy/chatcli
    kubectl -n chatcli logs deploy/chatcli | grep listening
    # 🚀 ChatCLI server listening on 0.0.0.0:50051
    ```
  </Step>

  <Step title="Conecte">
    ```bash theme={"system"}
    export CHATCLI_REMOTE_TOKEN="$(kubectl -n chatcli get secret chatcli \
      -o jsonpath='{.data.CHATCLI_SERVER_TOKEN}' | base64 -d)"

    kubectl -n chatcli port-forward svc/chatcli 50051:50051 &
    CHATCLI_ALLOW_INSECURE=true chatcli connect localhost:50051 --token "$CHATCLI_REMOTE_TOKEN"
    ```

    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.
  </Step>
</Steps>

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:

| Probe | Checagem | Tempo |
| - | - | - |
| startup | `GET /healthz` na porta `metrics` | a cada 5 s, até 30 falhas |
| liveness | `GET /healthz` na porta `metrics` | a cada 20 s, 3 falhas |
| readiness | conexão TCP na porta `grpc` | a cada 10 s, 3 falhas |

Com `server.metricsPort: 0`, startup e liveness caem para uma checagem TCP na porta gRPC. Esses probes funcionam com e sem TLS.

### Credenciais

| Opção | Valores | Observações |
| - | - | - |
| Token compartilhado | `server.token`, ou `CHATCLI_SERVER_TOKEN` em `secrets.existingSecret` | role `admin` para todo chamador |
| JWTs HS256 | `security.jwtSecretRef: {name, key}` (ou inline `security.jwtSecret`) | tokens precisam de `exp`; veja [JWT](/pt/server/server-mode#autenticação-do-servidor) |
| JWTs RS256 | `security.jwtPublicKeyRef: {name, key}` (ou inline `security.jwtPublicKey`) | várias chaves PEM podem ser listadas, para rotação |
| TLS mútuo | `tls.*` mais `security.tlsClientCA` (caminho dentro do pod) | toda conexão precisa de certificado de cliente |
| Nenhuma | `security.bindAddress: "127.0.0.1"` | acessível só de dentro do pod |

`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.

```bash theme={"system"}
kubectl -n chatcli create secret generic chatcli-jwt --from-literal=secret="$(openssl rand -hex 32)"
helm upgrade chatcli oci://ghcr.io/diillson/charts/chatcli --version 1.214.0 -n chatcli \
  --reset-then-reuse-values --set security.jwtSecretRef.name=chatcli-jwt --set security.jwtSecretRef.key=secret
```

### TLS

```yaml theme={"system"}
# values-tls.yaml
tls:
  enabled: true
  existingSecret: chatcli-tls              # Secret kubernetes.io/tls, montado em /etc/chatcli/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`).

<Warning>
  `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.
</Warning>

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](#segurança)). Pelo `kubectl port-forward` o certificado precisa de `localhost` nos SANs.

### Referência de valores

#### Servidor

| Valor | Padrão | Descrição |
| - | - | - |
| `replicaCount` | `1` | Réplicas (ignorado com `autoscaling.enabled`) |
| `strategy` | `{}` | Estratégia de atualização do Deployment, renderizada como escrita; vazia = escolhida pela persistência (veja [Rollouts no volume de sessões](#rollouts-no-volume-de-sessões)) |
| `image.repository` / `image.tag` / `image.pullPolicy` | `ghcr.io/diillson/chatcli` / `appVersion` do chart / `IfNotPresent` | Imagem do servidor |
| `imagePullSecrets` | `[]` | Pull secrets |
| `server.port` | `50051` | Porta gRPC |
| `server.metricsPort` | `9090` | `/metrics` e `/healthz`; `0` desliga os dois |
| `server.token` | `""` | Token compartilhado (obrigatório sem JWT ou mTLS) |
| `server.grpcReflection` | `false` | Define `CHATCLI_GRPC_REFLECTION=true`, que registra o reflection gRPC do servidor; as chamadas de reflection continuam exigindo a credencial do servidor. Deixe desligado em produção |
| `extraEnv` | `[]` | Variáveis extras (por exemplo `LOG_LEVEL=debug`); uma entrada para uma variável `CHATCLI_LOG_*` substitui o valor de `logging` do chart |
| `logging.maxSizeMB` / `maxBackups` / `maxAgeDays` / `compress` | `20` / `3` / `28` / `true` | Rotação do arquivo de log → `CHATCLI_LOG_MAX_SIZE_MB`, `CHATCLI_LOG_MAX_BACKUPS`, `CHATCLI_LOG_MAX_AGE_DAYS`, `CHATCLI_LOG_COMPRESS` (veja [Logs, health e métricas](#logs-health-e-métricas)) |
| `extraVolumes` / `extraVolumeMounts` | `[]` | Volumes extras para o container do servidor |

#### LLM

| Valor | Padrão | Descrição |
| - | - | - |
| `llm.provider` | `""` (primeiro provedor com credenciais) | `OPENAI`, `OPENAI_ASSISTANT`, `CLAUDEAI`, `BEDROCK`, `GOOGLEAI`, `XAI`, `ZAI`, `MINIMAX`, `MOONSHOT`, `STACKSPOT`, `OLLAMA`, `COPILOT`, `OPENROUTER` |
| `llm.model` | `""` | Modelo padrão |
| `secrets.existingSecret` | `""` | Seu Secret, carregado com `envFrom` (chaves são nomes de variáveis) no lugar do Secret do chart |
| `secrets.openaiApiKey`, `anthropicApiKey`, `googleaiApiKey`, `xaiApiKey`, `zaiApiKey`, `minimaxApiKey`, `moonshotApiKey`, `openrouterApiKey`, `githubCopilotToken` | `""` | Chaves dos provedores → `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GOOGLEAI_API_KEY`, `XAI_API_KEY`, `ZAI_API_KEY`, `MINIMAX_API_KEY`, `MOONSHOT_API_KEY`, `OPENROUTER_API_KEY`, `GITHUB_COPILOT_TOKEN` |
| `secrets.minimaxApiCompat` | `""` | `anthropic` = compatibilidade com a Messages API da Anthropic |
| `secrets.stackspotClientId`, `stackspotClientKey`, `stackspotRealm`, `stackspotAgentId` | `""` | StackSpot (`CLIENT_ID`, `CLIENT_KEY`, `STACKSPOT_REALM`, `STACKSPOT_AGENT_ID`) |
| `secrets.awsAccessKeyId`, `awsSecretAccessKey`, `awsSessionToken`, `bedrockRegion`, `awsRegion` | `""` | Bedrock; deixe as chaves vazias para IRSA (`serviceAccount.annotations`) |
| `secrets.chatcliBedrockCaBundle`, `chatcliBedrockInsecureSkipVerify` | `""` | Caminho do CA bundle do Bedrock / pular verificação (só para diagnóstico) |
| `copilot.model`, `copilot.maxTokens`, `copilot.apiBaseUrl` | `""` | `COPILOT_MODEL`, `COPILOT_MAX_TOKENS`, `COPILOT_API_BASE_URL` |
| `ollama.enabled`, `ollama.baseUrl`, `ollama.model` | `false`, `http://ollama:11434`, `""` | Ollama do lado do servidor (não passa pela checagem de SSRF) |

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

```bash theme={"system"}
kubectl -n chatcli create secret generic chatcli-llm-keys \
  --from-literal=CHATCLI_SERVER_TOKEN="$(openssl rand -hex 32)" \
  --from-literal=ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY"
helm install chatcli oci://ghcr.io/diillson/charts/chatcli --version 1.214.0 -n chatcli \
  --set llm.provider=CLAUDEAI --set secrets.existingSecret=chatcli-llm-keys
```

#### Fallback de provedor

| Valor | Padrão | Descrição |
| - | - | - |
| `fallback.enabled` | `false` | Liga a cadeia |
| `fallback.providers` | `[]` | Provedores depois do `llm.provider`, cada um `{name, model}`; sem `model` o provedor roda o próprio modelo padrão |
| `fallback.maxRetries` | `2` | Tentativas por provedor; `0` passa adiante na hora |
| `fallback.cooldownBase` / `fallback.cooldownMax` | `30s` / `5m` | Cooldown depois de falha / teto |

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.

```yaml theme={"system"}
llm:
  provider: CLAUDEAI
  model: claude-sonnet-5
fallback:
  enabled: true
  providers:                    # cadeia efetiva: CLAUDEAI -> OPENAI -> GOOGLEAI
    - name: OPENAI
      model: gpt-6-sol
    - name: GOOGLEAI
      model: gemini-3.8-flash
```

#### MCP

| Valor | Padrão | Descrição |
| - | - | - |
| `mcp.enabled` | `false` | Liga o MCP |
| `mcp.servers` | `[]` | Servidores inline: `name`, `transport` (`stdio`/`sse`), `command`, `args`, `url`, `env`, `enabled`, `overrides` |
| `mcp.existingConfigMap` | `""` | Seu ConfigMap com a chave `mcp_servers.json`, montado em `/etc/chatcli/mcp` (vence `servers`) |

#### K8s watcher

| Valor | Padrão | Descrição |
| - | - | - |
| `watcher.enabled` | `false` | Liga o watcher |
| `watcher.targets` | `[]` | Lista multi-target de `{deployment, kind, namespace, metricsPort, metricsPath, metricsFilter}`; `deployment` é o nome do recurso e `kind` é `Deployment` (padrão), `StatefulSet`, `DaemonSet`, `Job` ou `CronJob` |
| `watcher.deployment` / `watcher.namespace` | `""` | Modo de alvo único (usado quando `targets` está vazio), observa um Deployment; namespace vazio significa o namespace chamado `default` |
| `watcher.interval` / `watcher.window` | `30s` / `2h` | Intervalo de coleta / retenção |
| `watcher.maxLogLines` / `watcher.maxContextChars` | `100` / `32000` | Linhas de log por pod / orçamento de contexto do LLM |

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`).

```yaml theme={"system"}
watcher:
  enabled: true
  interval: "15s"
  targets:
    - deployment: api-gateway
      namespace: production
      metricsPort: 9090
      metricsFilter: ["http_requests_*", "http_request_duration_*"]
    - deployment: postgres
      kind: StatefulSet
      namespace: production
    - deployment: nightly-report
      kind: CronJob
      namespace: batch
```

#### Armazenamento, memória, pipeline e recursos

| Valor | Padrão | Descrição |
| - | - | - |
| `persistence.enabled` | `true` | PVC `<fullname>-sessions` para as sessões, montado em `~/.chatcli/sessions` |
| `persistence.storageClass` / `accessModes` / `size` | `""` / `[ReadWriteOnce]` / `1Gi` | `-` = `storageClassName: ""`. Sem `ReadWriteMany` um rollout para o pod antigo primeiro; `ReadWriteOncePod` falha o render com mais de uma réplica |
| `memory.enabled` | `false` | Memória de longo prazo em `~/.chatcli/memory` (no PVC de sessões quando a persistência está ligada, num emptyDir de 200Mi caso contrário) |
| `memory.subPath` | `memory` | Diretório do PVC de sessões que guarda a memória quando a persistência está ligada; a memória gravada na raiz do PVC por charts anteriores é copiada para ele uma vez. `""` = a raiz do PVC, compartilhada com os arquivos de sessão (o layout antigo) |
| `pipeline.enabled` | `false` | [RPCs de pipeline](/pt/server/server-mode#rpcs-de-pipeline) (`CHATCLI_SERVER_PIPELINE`) |
| `agents.*`, `skills.*`, `bootstrap.*` | desligados | `enabled`, `definitions` (arquivos inline), `existingConfigMap`; ligado sem nenhum dos dois, nada é montado |
| `skillRegistry.enabled`, `registryUrls`, `registryDisable`, `installDir` | `false`, `""` | Configuração dos registries de skills |
| `plugins.enabled`, `initImage`, `existingPVC` | `false`, `""` | Diretório de plugins, preenchido por uma init image ou por um PVC |
| `resources` | requests `100m`/`128Mi`, limits `500m`/`512Mi` | Recursos do container |
| `podSecurityContext` / `securityContext` | UID/GID/fsGroup 1000, não root, `RuntimeDefault`; sem escalonamento de privilégio, raiz somente leitura, drop `ALL` | Segurança do pod e do container |
| `nodeSelector`, `tolerations`, `affinity` | vazios | Agendamento |

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:

```bash theme={"system"}
kubectl -n chatcli patch deploy/chatcli --type=json \
  -p '[{"op":"remove","path":"/spec/strategy/rollingUpdate"},{"op":"replace","path":"/spec/strategy/type","value":"Recreate"}]'
```

```yaml theme={"system"}
strategy:
  type: Recreate
```

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

| Valor | Padrão | Descrição |
| - | - | - |
| `security.jwtSecret` / `jwtSecretRef` | `""` / `{}` | Segredo HS256, inline / de um Secret; defina um, não os dois (os dois falham o render) |
| `security.jwtPublicKey` / `jwtPublicKeyRef` | `""` / `{}` | Chave(s) pública(s) RS256, PEM inline ou caminho / de um Secret; defina um, não os dois (os dois falham o render) |
| `security.jwtIssuer` / `jwtAudience` | `""` | `iss` / `aud` exigidos |
| `security.tlsClientCA` | `""` | Caminho do bundle de CA para mTLS (exige `tls.*`) |
| `security.mtlsRole` | `""` (`user`) | Role de quem se identifica só por certificado |
| `security.rateLimitRps` / `rateLimitBurst` | `""` (10 / 20) | Rate limit por chamador |
| `security.maxRecvMsgSize` / `maxSendMsgSize` / `maxConcurrentStreams` | `""` (50 MB / 50 MB / 100) | Limites gRPC |
| `security.bindAddress` | `""` (`0.0.0.0` no Kubernetes) | Endereço de escuta |
| `security.auditLogPath` | `""` | Caminho absoluto e gravável da trilha de auditoria (monte um volume) |
| `security.debug` | `false` | Stack traces nos logs de erro |
| `security.agentSecurityMode` | `""` (strict) | `strict` ou `permissive` |
| `security.sessionTTL` | `""` (90 dias) | Expiração das sessões em dias |
| `security.envRedactMode` | `""` (permissive) | `strict` ou `permissive` |
| `security.allowUnsignedPlugins` | `false` | Permite plugins não assinados (só desenvolvimento) |
| `security.allowInsecure` | `false` | Define `CHATCLI_ALLOW_INSECURE`, que só o cliente lê; não afeta o servidor |
| `security.encryptionKey` | `""` | Chave de criptografia das sessões, inline (prefira `extraEnv` com `secretKeyRef`) |

```yaml theme={"system"}
# JWT de um Secret, mTLS com CA de cliente separada, log de auditoria num volume
tls:
  enabled: true
  existingSecret: chatcli-tls                 # caminhos assumem /etc/chatcli/tls/tls.crt e tls.key
security:
  jwtSecretRef: {name: chatcli-jwt, key: secret}
  tlsClientCA: /etc/chatcli/client-ca/ca.crt
  mtlsRole: viewer
  auditLogPath: /var/log/chatcli/audit.jsonl
extraVolumes:
  - name: client-ca
    secret: {secretName: chatcli-client-ca}
  - name: audit
    emptyDir: {}
extraVolumeMounts:
  - {name: client-ca, mountPath: /etc/chatcli/client-ca, readOnly: true}
  - {name: audit, mountPath: /var/log/chatcli}
```

#### Service, ingress, network policy

| Valor | Padrão | Descrição |
| - | - | - |
| `service.type` / `service.port` | `ClusterIP` / `50051` | Service |
| `service.headless` | `false` | Service headless para balanceamento no cliente; use com mais de uma réplica |
| `ingress.enabled`, `className`, `annotations`, `hosts`, `tls` | desligado | Ingress; `className: nginx` adiciona `backend-protocol: GRPC` e `ssl-redirect: "true"`, e as mesmas chaves em `annotations` substituem esses padrões |
| `networkPolicy.enabled` | `false` | Ingress para as portas gRPC e de métricas |
| `networkPolicy.ingressFrom` | ausente (qualquer origem) | Peers permitidos |
| `networkPolicy.egress` | `allowAll` | `restricted` = DNS, 443, `kubernetesApiPort` (6443), `egressExtraPorts` |

#### Escala e monitoramento

| Valor | Padrão | Descrição |
| - | - | - |
| `autoscaling.enabled`, `minReplicas`, `maxReplicas`, `targetCPUUtilizationPercentage`, `targetMemoryUtilizationPercentage` | `false`, `1`, `5`, `80`, ausente | HPA |
| `podDisruptionBudget.enabled`, `minAvailable`, `maxUnavailable` | `false`, `1`, ausente | PDB, renderizado só com `replicaCount > 1` |
| `serviceMonitor.enabled`, `interval`, `scrapeTimeout`, `labels` | `false`, `30s` | ServiceMonitor do Prometheus Operator na porta de métricas |
| `serviceAccount.create`, `name`, `annotations` | `true` | Anotações de IRSA / Workload Identity vão aqui |
| `rbac.create`, `clusterWide`, `additionalRules` | `true`, `false`, `[]` | RBAC do watcher e da remediação. Não dá acesso a Secrets (o servidor nunca lê um pela API); adicione em `additionalRules` uma regra restrita com `resourceNames` só para um plugin que precise |
| `crdUpgrade.enabled` | `true` | Hook que reaplica os CRDs (imagem kubectl `registry.k8s.io/kubectl:v1.31.10`) |
| `prometheusUrl` | `""` | Obsoleto, ignorado pelo servidor (defina no chart do operator) |

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

| Forma | Como | Cliente |
| - | - | - |
| Port-forward (desenvolvimento) | `kubectl -n chatcli port-forward svc/chatcli 50051:50051` | `CHATCLI_ALLOW_INSECURE=true chatcli connect localhost:50051 --token …` (ou `--tls --ca-cert` com TLS) |
| Ingress, TLS no ingress | `ingress.className: nginx`, `ingress.tls`, servidor em texto puro | `chatcli connect chatcli.example.com:443 --tls --token …` |
| LoadBalancer / NodePort | `service.type`, com `tls.*` ligado | `chatcli connect <endereço>:50051 --tls --ca-cert ca.crt --token …` |

```yaml theme={"system"}
# values-ingress.yaml: TLS termina no ingress-nginx, gRPC até o pod
ingress:
  enabled: true
  className: nginx
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
  hosts:
    - host: chatcli.example.com
      paths:
        - path: /
          pathType: ImplementationSpecific
  tls:
    - secretName: chatcli-ingress-tls
      hosts: [chatcli.example.com]
```

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

```bash theme={"system"}
kubectl -n chatcli logs deploy/chatcli -f            # o banner e, depois, o log em linhas JSON
kubectl -n chatcli port-forward deploy/chatcli 9090:9090 &
curl -s localhost:9090/healthz                       # ok
curl -s localhost:9090/metrics | grep '^chatcli_server_info'
```

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:

| Valor | Padrão | Variável |
| - | - | - |
| `logging.maxSizeMB` | `20` | `CHATCLI_LOG_MAX_SIZE_MB`: tamanho em que o arquivo roda |
| `logging.maxBackups` | `3` | `CHATCLI_LOG_MAX_BACKUPS`: arquivos rotacionados mantidos |
| `logging.maxAgeDays` | `28` | `CHATCLI_LOG_MAX_AGE_DAYS`: dias que um arquivo rotacionado é mantido |
| `logging.compress` | `true` | `CHATCLI_LOG_COMPRESS`: gzip nos arquivos rotacionados |

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.

````yaml theme={"system"}
logging:
  maxSizeMB: 10
  maxBackups: 5
extraEnv:
  - name: LOG_LEVEL              # debug | info | warn | error
    value: info
``` A porta de métricas não tem autenticação; restrinja com `networkPolicy.ingressFrom` onde isso importar.

### Upgrade, rollback, desinstalação

```bash
helm upgrade chatcli oci://ghcr.io/diillson/charts/chatcli --version 1.214.0 \
  -n chatcli --reset-then-reuse-values
helm history chatcli -n chatcli
helm rollback chatcli <revisão> -n chatcli
helm uninstall chatcli -n chatcli
````

* 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](#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

| Sintoma | Causa | Correção |
| - | - | - |
| Pod em `CrashLoopBackOff`; o `kubectl logs --previous` termina com `refusing to serve an unauthenticated API on 0.0.0.0` | Sem credencial num bind acessível | Defina `server.token` ou JWT |
| As notas do `helm install` mostram `WARNING: no credential is configured` | Sem token, JWT ou CA de cliente nos valores | Defina `server.token` ou `security.jwtSecretRef` |
| Cliente: `tls: first record does not look like a TLS handshake` | Servidor em texto puro (`tls.enabled=false`) | `CHATCLI_ALLOW_INSECURE=true`, ou ligue o TLS |
| `helm install` falha com `tls.enabled=true needs tls.existingSecret` | TLS ligado sem material de certificado | Defina `tls.existingSecret`, ou `tls.certFile` e `tls.keyFile` juntos |
| Cliente: `x509: certificate is valid for …, not localhost` | O port-forward disca `localhost` | Adicione SANs `localhost`/`127.0.0.1`, ou conecte pelo nome do certificado |
| `Error: values don't meet the specifications of the schema(s)` | Valor desconhecido ou com erro de digitação | Confira a chave nas tabelas acima |
| O watcher não traz nada de um namespace | RBAC: o chart criou uma Role e o alvo está em outro lugar | Defina o namespace do alvo em `watcher.targets` ou `watcher.namespace` (ClusterRole automática), ou use `rbac.clusterWide: true` |
| Sessões somem no restart | `persistence.enabled: false` | Ligue |
| O rollout trava; o pod novo fica em `ContainerCreating` com `Multi-Attach error` | Um chart que não renderizava estratégia (até 1.211.x), ou uma `strategy` explícita com surge, num volume `ReadWriteOnce` | Faça o upgrade com a `strategy` padrão (para o antigo primeiro), ou uma réplica num nó |
| Réplicas extras presas em `ContainerCreating` | Várias réplicas num volume de sessões `ReadWriteOnce`, agendadas em outros nós | Uma réplica, ou `persistence.accessModes: [ReadWriteMany]` |
| `helm install` falha com `ReadWriteOncePod admits a single pod` | `ReadWriteOncePod` com `replicaCount` ou `autoscaling.maxReplicas` acima de 1 | Uma réplica, ou `ReadWriteMany` |
| `helm install` falha com `logging.maxSizeMB x (logging.maxBackups + 1) = … MB` | Rotação de log maior que 100 MB no emptyDir de dados | Reduza `logging.maxSizeMB` ou `logging.maxBackups` |
| `helm install` falha com `security.jwtSecret and security.jwtSecretRef both set` (ou `jwtPublicKey` e `jwtPublicKeyRef`) | Valor inline e referência a Secret definidos juntos | Mantenha só a referência |
| `*.existingConfigMap` editado não é aplicado | O servidor só o lê no startup, e o Helm não consegue hasheá-lo | `kubectl -n chatcli rollout restart deploy/chatcli` |
| Duas réplicas, uma recebe todo o tráfego | O ClusterIP prende as conexões HTTP/2 | `service.headless: true` |

Veja também a [tabela de problemas do servidor](/pt/server/server-mode#solução-de-problemas).

## Próximos passos

<CardGroup cols={3}>
  <Card title="Modo Servidor" icon="server" href="/pt/server/server-mode">
    Flags, auth, limites, operação
  </Card>

  <Card title="Conexão Remota" icon="plug" href="/pt/server/remote-connect">
    Conecte ao servidor
  </Card>

  <Card title="K8s Watcher" icon="binoculars" href="/pt/kubernetes/k8s-watcher">
    Monitore workloads
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.