Pular para conteúdo

0019 — Monitor de host/apps do Coolify (Fase 1, read-only)

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-15 · Decidido em: 2026-08-19

Contexto

A VM de produção bi-ubuntu (host do Coolify) congelou durante deploy em 2026-08-18 (2×) e de novo em 2026-08-19, por falta de baseline de host: swap saturado, OOM, apps sem limite de memória. Ninguém enxerga isso até travar. O recon de 2026-08-19 confirmou o quadro: 13 dos 24 apps rodam com limits_memory:"0" (sem teto) e o /swap.img primário está 99,99% cheio. O auth é o SPOF do ecossistema e já roda como container no mesmo daemon Docker do Coolify — pode ler o host de dentro.

Precisávamos expor métrica de saúde (por app e do host) para a Central diagnosticar antes de travar, sem introduzir um daemon que a VM (que já congela) tenha de pagar, e sem dar ao serviço acesso perigoso ao host.

Decisão

Dois endpoints REST admin read-only, sob demanda, na feature nova monitoramento: GET /api/admin/monitor/apps (grade de apps: limites + RAM/CPU viva, limits_memory=0 destacável) e GET /api/admin/monitor/host (RAM, swap, disco /, build-cache). Nenhuma ação destrutiva — Fase 1 é 100% leitura (prune/restart/limpeza ficam para uma Fase 2 com whitelist + confirmação + auditoria, desenhada à parte).

Como lê, com segurança (o socket do Docker é equivalente a root no host):

  • Socket só via docker-socket-proxy com allowlist mínima liberando só /containers/json, /containers/{id}/stats e /system/df (POST=0, EXEC=0). Nunca o socket cru montado no auth.
  • Mount do host read-only de superfície mínima: /proc → /host/proc (mem/swap) e um arquivo público (/etc/os-release → /host/probe) para o statvfs do disco /. Não se monta / inteiro (evita expor segredos de outros apps em leitura).
  • Endpoints atrás do filtro admin já existente (@ServerFilter("/api/admin/**"), Firebase xadm_admin ou shared secret). Timeout curto por chamada ao socket (~3 s) — o monitor não pode estressar a VM que observa.

Correção/Evolução 2026-09-11: o proxy implantado é o wollomatic/socket-proxy (1.13.1, pinado por digest), não o tecnativa/docker-socket-proxy que as flags acima (POST=0/EXEC=0) pressupõem. No tecnativa, CONTAINERS=1 é regra por prefixo (^(/v[\d\.]+)?/containers) e libera todo GET sob /containers — /containers/{id}/json expõe o env de todos os containers (inclusive AUTH_PRIVATE_KEY), além de archive/export/logs —, então a allowlist mínima desta decisão não era realizável nele. O wollomatic casa regex ancorada por rota e libera só as 3 rotas GET; o resto dá 403 e método ≠ GET dá 405 (verificado em produção). O /system/df passou a ser pedido com ?type=build-cache (sem o type o daemon percorre todos os volumes do host a cada chamada). O probe do disco virou um diretório vazio dedicado (/srv/xadm-probe, 0555 root:root) no lugar do /etc/os-release, e o serviço hoje é o central-backend (0020). A decisão — socket só via proxy com allowlist mínima, mount de superfície mínima — segue de pé; o provisionamento está no runbook.

Correção (2026-09-15): o mount do host não é read-only. O Directory Mount do Coolify é bind rw — a API do Coolify não tem mount read-only —, e a proteção é a pré-condição da casa (coolify, Criar uma aplicação): o container roda não-root (USER app, uid 1001, no Dockerfile.native) e só lê. Em container root, o bind do /proc do host não herdaria o read-only que o Docker aplica a /proc/sys e /proc/sysrq-trigger. Os endpoints seguem só de leitura, e a superfície mínima (/proc e o probe, nunca / inteiro) fica.

Degradação parcial por fonte: cada resposta é 200 mesmo com fonte fora; a fonte que falha aparece em fontes: {..:"erro"} + erros: {code, msg} e seu dado vem null — nunca engolida em WARN (feedback §6). Contêiner lento degrada por-item (aquele app fica com vivos null), não derruba o endpoint.

Reuso: o CoolifyClient migrou de centralapps para um slice dedicado coolify (integração externa, irmão das features), agora consumido por centralapps (broker) e monitoramento (grade). O join app↔container é por coolify.name ⊇ uuid do app (a API do Coolify retorna id: null, então o label numérico não serve); um app compose (stacks ps.*) mapeia N containers → soma o RAM vivo.

Alternativas preteridas

  • Coolify Sentinel — sobe agente que polla todo container em intervalo curto. Custo de recurso contínuo que a VM que já congela não deve pagar. Métrica sob demanda basta.
  • Socket cru montado no auth — comprometimento total de produção num bug/invasão. Proxy com allowlist é inegociável.
  • Montar / inteiro read-only — simples (1 mount, todas as métricas), mas expõe todos os arquivos do host em leitura. O par /proc + probe de 1 arquivo dá as mesmas métricas com superfície mínima.
  • 2ª chamada ao Coolify (/deployments) para deploy-status — +latência/+fonte/+carga. Deferido: a Fase 1 usa só o que GET /applications já entrega (status traz estado+health de graça).

Deferido (Regra Nº 3)

  • kills do earlyoom — exigem o journal do host (journald binário → mount + parse caros), ROI baixo perto de mem/swap. Fase 2.
  • Swap por-device — o /proc/swaps capta a saturação do device primário; a Fase 1 reporta o agregado (mem/swap do meminfo). Refino barato futuro.
  • ultima_deploy (sucesso/quando) — inexistente no /applications; a grade usa last_online_at como proxy de atividade.

Consequências

  • A Central passa a ver, sob demanda, os limits_memory=0 e o swap saturado antes do próximo freeze — o valor direto da feature.
  • Novo slice coolify no grafo de arquitetura (ArchUnit): centralapps→coolify, monitoramento→coolify, acíclico.
  • Provisionamento de infra novo no Coolify (proxy + mounts) — ver runbook.
  • O contrato JSON dos 2 endpoints é consumido pela tela do central-ui (que roda o próprio fluxo).