Pular para conteúdo

Deploy do central-backend no Coolify

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

O que é

Sobe (ou atualiza) o serviço auth.xadm.biz no Coolify, com HTTPS via Traefik e acesso ao db_auth na rede interna.

Quando usar

  • Primeiro deploy do serviço num ambiente.
  • Mudança de variável de ambiente (chaves, banco, Firebase).
  • O deploy de código não precisa deste runbook: é automático pelo control-plane — ver Deploy de código abaixo.

Pré-requisitos

  • Chaves geradas (Gerar as chaves e segredos).
  • db_auth acessível na rede do projeto integracao-compartilhado.
  • Acesso ao painel do Coolify.

Passos

  1. Criar o recurso (primeira vez): Coolify → projeto integracao-compartilhado → New Resource → Docker Image, apontando pra imagem native que o CI publica (fonte.xadm.biz/xadm/central-backend:native-amd64; o app é native-only — decisão 0030).

    • Port: 8080
    • Network: a rede compartilhada onde o db_auth está acessível
    • Domains: auth.xadm.biz com Force HTTPS (Traefik emite o certificado) — é o host do issuer/JWKS e não muda (decisão 0020) —, mais central-backend.xadm.biz, o domínio estável do serviço (é o CENTRAL_DEPLOY_URL que o CI chama).

    Build Pack = Docker Image não é preferência, é requisito do control-plane

    O reconcile do app_deploy_targets só ancora recurso com build_pack=dockerimage, derivando o registry_slug do nome da imagem e o target do prefixo da tag (0021). Recurso buildado do git (Dockerfile/nixpacks) não entra no mapa → o POST /api/ci/deploy responde 404 no_deploy_target e o job deploy do pipeline.yml quebra. A única outra família ancorada é build_pack=dockercompose, pelo slug do git repo com target='compose' (0026) — não é o caso deste serviço.

  2. Variáveis de ambiente (mínimo de produção):

    MICRONAUT_ENVIRONMENTS=prod
    DATASOURCES_DEFAULT_URL=jdbc:postgresql://postgresql:5432/db_auth
    DB_USER=auth_service
    DB_PASSWORD=<senha-do-db_auth>
    AUTH_PRIVATE_KEY=<base64 da chave privada>
    AUTH_KEY_ID=auth-1
    AUTH_ISSUER=https://auth.xadm.biz
    # Cai para ≤ 60 (ADR central 0037) só depois que os apps que renovam sessão tiverem o refresh;
    # antes disso, app sem refresh re-loga a cada TTL (decisão 0034):
    AUTH_TOKEN_EXPIRATION_MINUTES=1440
    # Opcional — teto da sessão de app-user desde o último login com credencial (refresh, decisão 0034);
    # máximo 168 (7 dias, ADR central 0037) — valor acima é cortado:
    AUTH_SESSION_MAX_HOURS=168
    # Opcional — habilita login Firebase (admin e client-login):
    AUTH_FIREBASE_PROJECT_ID=<projeto-firebase>
    AUTH_XADM_EMAIL_DOMAIN=xadm.com.br
    # Opcional — notificação de novo acesso pendente (sem a chave, o envio é no-op):
    RESEND_API_KEY=<chave-resend>
    # Shared secret do integrador p/ /api/admin/**:
    CENTRAL_INTEGRATOR_TOKEN=<segredo>
    # Do CI p/ /api/ci/** — OBRIGATÓRIO: ausente ou vazio, o container NÃO SOBE
    # (DeployTokenGuard, ativo só com MICRONAUT_ENVIRONMENTS=prod — acima; o rolling
    # deploy mantém o anterior no ar). Mesmo valor no secret do GitHub.
    CENTRAL_DEPLOY_TOKEN=<segredo>
    # Dos integrador-servers p/ /api/integrador/** (heartbeat do integrador-client) —
    # OBRIGATÓRIO pelo mesmo motivo: ausente ou vazio, o container NÃO SOBE
    # (IntegradorTokenGuard, só em prod). MESMO valor do CENTRAL_API_TOKEN dos
    # integrador-servers. Só abre
    # /api/integrador/** — não tem relação com o CENTRAL_INTEGRATOR_TOKEN acima.
    CENTRAL_API_TOKEN=<segredo>
    

    Heartbeat, alerta de silêncio e o lado do integrador-server: runbook do heartbeat.

    CORS. Não precisa configurar nada para app da casa: a allowlist já cobre https://*.xadm.biz (todo bi.<cliente>.xadm.biz) além do central/login e das portas de dev. CENTRAL_CORS_EXTRA_ORIGINS (CSV) existe só para origem fora do domínio da casa — staging local em porta própria, por exemplo. Vazio em produção.

    E-mail. Produção usa Resend (RESEND_API_KEY, remetente em RESEND_REMETENTE). O endpoint é property (RESEND_ENDPOINT, default a API da Resend) e existe para o teste apontar o transporte a um mock — não mexer em produção. O transporte SMTP (CENTRAL_EMAIL_SMTP_HOST, CENTRAL_EMAIL_SMTP_PORT, CENTRAL_EMAIL_FROM) existe para ambiente de teste com mock (Mailpit no e2e) — sem auth nem TLS, não aponte para um SMTP real. Com as duas configs setadas, o Resend vence (é @Primary).

  3. Health check: GET /health na porta 8080 (o Traefik só roteia após 200). O Flyway e o JwtService falham-fast no startup — config errada não sobe. É o /health flat do xadm-comum-web ({status, versao, flavor, commit}, liveness, 200 fixo): o health do micronaut-management está desligado (endpoints.health.enabled=false no application.yml), então indicador de banco não faz o check oscilar. Ver decisão 0016.

  4. Deploy: salve e dispare o deploy pela UI. Acompanhe os logs até Startup completed. Só este primeiro deploy é manual — do segundo em diante quem dispara é o CI.

Deploy de código (o fluxo normal)

Não há webhook do Coolify nem ponte SSH: o deploy de código passa pelo control-plane (0021), e o central deploya a si mesmo por ele (0022).

  1. Push em master (ou tag v*) dispara o pipeline.yml no GitHub Actions.
  2. O build_native roda em paralelo ao gate e publica duas tags: fonte.xadm.biz/xadm/central-backend:native-amd64 (móvel, a que o deploy consome) e …:<sha>-native (imutável, o alvo de rollback do smoke). O build recebe --build-arg XADM_COMMIT=<sha>, que o Dockerfile.native promove a ENV e a xadm-comum-web emite como commit no /health — é o que prova que o container no ar é o desta entrega.
  3. Só com o gate verde (e, em tag, o release-check), o job deploy faz POST $CENTRAL_DEPLOY_URL (https://central-backend.xadm.biz/api/ci/deploy — flavor-agnóstica: URL que outro sistema consome não carrega flavor de build, constituição, Entrega) com Authorization: Bearer $CENTRAL_DEPLOY_TOKEN e {"target":"native","image":"fonte.xadm.biz/xadm/central-backend:native-amd64"}. Antes do POST ele registra o commit que está no ar — depois do POST já é o novo.
  4. O central-atual (no ar, dentro da casa) resolve o próprio recurso no app_deploy_targets e chama a API do Coolify localmente — essa API é restrita ao IP da casa e o runner do GitHub não a alcança; é por isso que o POST passa pelo central e não pelo runner.
  5. O Coolify faz rolling deploy health-gated: a imagem nova sobe, passa no /health → substitui; falha → a anterior segue no ar.

  6. O job smoke roda depois do deploy (que é assíncrono: o POST retorna antes de o container novo servir) e é o gate de produção — ver decisão 0027 e a norma da casa. Ele afirma que o commit do /health é o desta entrega e que as rotas de docs/app.json → smoke.routes respondem o status declarado. Reprovou → o pipeline fica vermelho, e o relatório dá o alvo de reversão …:<sha anterior>-<target>. A reversão automática está pendente no control-plane: o POST /api/ci/deploy aceita só a tag móvel e responde 400 invalid_image à imutável, então quem reverte é o operador (seção Reversão, abaixo).

A auto-referência não trava porque o central-atual está no ar no disparo e a chamada ao Coolify enfileira e retorna antes do swap do container.

Break-glass — central fora do ar

Sem central no ar não há quem receba o POST, então o deploy automático não acontece (o job falha no curl -fsS). Suba manualmente pela UI do Coolify, de dentro da casa: recurso native → Redeploy (ou aponte a tag imutável da imagem boa). É cenário de incidente — o caminho normal cobre todo release.

Verificação (go-live)

O job smoke já roda os dois primeiros itens automaticamente (são as rotas de smoke.routes). Os manuais abaixo servem para incidente e para o primeiro deploy, que é manual.

# 1. Serviço no ar — e QUAL entrega/flavor está no ar
curl -sf https://central-backend.xadm.biz/health | jq '{status, versao, flavor, commit}'
# status "UP"; flavor "native"|"jvm"; commit = o sha da entrega esperada.
# `commit` ausente = imagem buildada sem --build-arg XADM_COMMIT (ou lib < 0.9.0).
# `commit: "HEAD"` = imagem AINDA no nome antigo (SOURCE_COMMIT), que o Coolify sobrescreve
# em runtime. Nao e identidade: rebuilde com XADM_COMMIT (lib >= 0.9.0 ja o omite).

# 2. JWKS com alg=RS256 e o kid esperado
curl -sf https://central-backend.xadm.biz/.well-known/jwks.json | jq '.keys[0] | {alg, kid}'

# 3. Login pAbast válido → 200 + JWT scope=full
curl -sf -X POST https://auth.xadm.biz/api/auth/pabast/login \
  -H "Content-Type: application/json" \
  -d '{"cliente":"vantroba","id_usuario":"123","senha":"1234","senha_compl":"567"}' \
  | jq '{tem_token: (.access_token != null), scope}'

# 4. Credenciais inválidas → 401
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://auth.xadm.biz/api/auth/pabast/login \
  -H "Content-Type: application/json" \
  -d '{"cliente":"vantroba","id_usuario":"123","senha":"0000","senha_compl":"000"}'

Logs relevantes

INFO  ... Login pAbast OK: id_usuario=... cliente=...
WARN  ... Rate limit atingido: ip=...
ERROR ... Falha ao conectar ao banco

Configure alerta (Coolify/Uptime Kuma) para /health ≠ UP.

Reversão

  • Código — rollback automático: pendente no control-plane. O job smoke reprova, calcula o alvo …:<sha anterior>-<target> (o commit lido do /health antes do deploy) e pede a reversão ao POST /api/ci/deploy, que aceita só a tag móvel <target>-amd64 e responde 400 invalid_image à imutável. O relatório sai com ROLLBACK FALHOU no control-plane e o alvo.
  • Código, manual (o caminho de hoje): no recurso central-backend-native-pull, aponte a tag da imagem para <sha anterior>-native e faça Redeploy; depois de corrigir, volte a tag para native-amd64, senão o próximo release re-puxa a imagem antiga. Consequência operacional: a tag imutável é ativo de produção — o registry retém as dez últimas por imagem e alvo, e nunca poda a que está no ar.
  • Configuração: ajuste a variável e redeploy. Um deploy que não passa no /health não substitui o container anterior (rolling health-gated), então config errada derruba o deploy, não o serviço.
  • Migration: o rollback não desfaz o schema — o Flyway CE é forward-only. A norma expand/contract cobre isso: a release N não dropa o que N-1 ainda usa, então a imagem anterior roda sobre o schema novo. Para desfazer DDL, uma migration nova.