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-proxycom allowlist mínima liberando só/containers/json,/containers/{id}/statse/system/df(POST=0,EXEC=0). Nunca o socket cru montado noauth. - 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 ostatvfsdo 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/**"), Firebasexadm_adminou 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 otecnativa/docker-socket-proxyque 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}/jsonexpõe o env de todos os containers (inclusiveAUTH_PRIVATE_KEY), além dearchive/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/dfpassou a ser pedido com?type=build-cache(sem otypeo 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 é ocentral-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, noDockerfile.native) e só lê. Em container root, o bind do/procdo host não herdaria o read-only que o Docker aplica a/proc/syse/proc/sysrq-trigger. Os endpoints seguem só de leitura, e a superfície mínima (/proce 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 queGET /applicationsjá entrega (statustraz 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/swapscapta a saturação do device primário; a Fase 1 reporta o agregado (mem/swap domeminfo). Refino barato futuro. ultima_deploy(sucesso/quando) — inexistente no/applications; a grade usalast_online_atcomo proxy de atividade.
Consequências¶
- A Central passa a ver, sob demanda, os
limits_memory=0e o swap saturado antes do próximo freeze — o valor direto da feature. - Novo slice
coolifyno 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).