Pular para conteúdo

0022 — native-image é o alvo de deploy do server Micronaut elegível

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

Contexto

Os servers Micronaut da casa rodam na JVM (shadowJar + JRE no Coolify). A JVM tem dois custos que doem em produção: RSS alto (heap + metaspace + JIT) e imagem grande (JRE + jar), e o build no host — o Coolify builda a imagem no próprio host de produção (dind), com pico brutal de RAM/CPU. Esse pico foi o gatilho do freeze da VM de produção documentado na operação do host do Coolify.

GraalVM native-image compila o app a um binário nativo: RSS ~3× menor, imagem ~9× menor, boot quase instantâneo. A frota já caminhou pra lá — vários servers já rodam native — mas o central nunca documentou o padrão: o conhecimento vive nos repos de app, sem dono citável (cada migração reinventa o Dockerfile, o perfil GraalVM e o gate, e erra nas bordas). Esta RD ratifica native-image como o padrão de deploy da casa e amarra os artefatos que faltavam.

Decisão

Todo app server Micronaut elegível deploya como binário GraalVM native-image. É o default do scaffold; a JVM é o caminho de exceção declarada.

  • Elegível = server Micronaut. O integrador-client (Java 8) e os apps Flutter estão fora por stack, não por débito — a mesma fronteira de elegibilidade das bibliotecas da casa (0019). Não é dívida a pagar; é escopo.
  • Community Edition (CE). A casa usa GraalVM CE. Consequência: --gc=G1, PGO (profile-guided optimization) e --emit build-report são Oracle-GraalVM-only (indisponíveis); e -O3 em CE equivale a -O2 (não há ganho a prometer). O perfil da casa usa -Os (prod) / -Ob (loop de dev) e --gc=serial (o default da CE, explicitado).
  • Exceção JVM declarada (§1.6 / REGRA Nº 3). App que depende de biblioteca hostil ao native (o caso concreto é ler XLSX com Apache POI — 0023) fica JVM, com limite de memória no Coolify. É decisão declarada por app, não silêncio.

Consequências

  • Ler XLSX muda de biblioteca. POI/XMLBeans não roda em native → a casa lê XLSX com FastExcel-reader (0023). É a única troca de biblioteca forçada pela decisão.
  • docs/app.json ganha native: true. É o discriminador canônico de "este app deploya native" — o sinal que o gate do ci.yml e o e2e native leem (server que deploya via dockerfile-java-native).

Correção 2026-09-12: o app é native se e só se o build.targets do docs/app.json contém native; o campo native saiu (0033). - Template Dockerfile native. templates/dockerfile-java-native (rastreado, par do dockerfile-java): builder GraalVM CE + ./gradlew nativeCompile + runtime glibc com curl (o HEALTHCHECK da casa exige curl). Recipe em java-micronaut §Deploy. - Template do workflow de build off-host. templates/build-deploy.yml (rastreado, par dos Dockerfiles): destino .github/workflows/build-deploy.yml, builda no GitHub o(s) alvo(s) do release (jar e/ou native, pelo trailer Deploy: da tag / build.targets do app.json), publica a imagem e dispara o deploy via ponte SSH (ver §Topologia). Sem ele cada app copiava o workflow à mão — drift garantido. Renomeado de build-native.yml em 0.37.0, quando o jar também passou a buildar aqui (o nome antigo enganava — "build-native" buildando jar).

Correção 2026-09-12: o workflow é o pipeline.yml (0027), jobs build_<alvo> e deploy; o build-deploy.yml saiu do kit. - imageName.set("application") é obrigatório no graalvmNative. O rootProject.name diverge do slug em vários apps (ex. central-backend, onpetro-xls); sem fixar o nome, o nativeCompile emite o binário com o nome do rootProject e o COPY build/native/nativeCompile/application do Dockerfile quebra. O nome do binário da casa é application. - No native, o SENTRY_DSN vem por ENV, não por ponte no build. O Dockerfile.native tem ENTRYPOINT exec, sem shell — ["/usr/bin/tini", "--", "/app/application"]: o tini é o init de PID1 (colhe o curl <defunct> do HEALTHCHECK, java-micronaut §Deploy), -- separa o init do binário, e continua puro (tini é exec, não sh -c). O DSN chega pelo ambiente do Coolify e o logback resolve ${SENTRY_DSN:-} em runtime. Alguns Dockerfile JVM injetam o DSN via jq do app.json num entrypoint script — isso não sobe pro native (nem precisa: o DSN é público e o env é o caminho canônico da casa — java-micronaut §Observabilidade). - Slug do app.json == nome da imagem no registry. A convenção de nomenclatura de slug/registry (e por que o slug pode divergir do app_id) vive em 0024. - Reflect-config para o que instancia por reflexão. O SentryAppender do logback é instanciado por reflexão pelo Joran → sem registro, o boot native quebra. E registrar só a classe não basta (emenda de 2026-09-10): o Joran também acha os setters por reflexão, então sem os métodos registrados o <options> e o <minimumEventLevel> do logback.xml são ignorados em silêncio — o registro completo (appender, SentryOptions, Level.valueOf) está em java-micronaut §Observabilidade.

Correção 2026-09-12: a metadata native do Sentry vem da xadm-comum-web; o app não carrega reflect-config de Sentry/logback. - Build fora do host de produção vira ainda mais duro. O compilador native-image come muito mais RAM/CPU que o shadowJar → nunca buildar na VM de prod (CI ou Build Server; runner na mesma VM = zero ganho). É a direção da fábrica de imagens de CI (0003) e a cura do freeze do host. - Gate: nativeCompile verde no repo do app; runtime no repo de e2e. No repo do app o gate é o nativeCompile compilar — não se replica e2e/boot-smoke native ali. O runtime (o binário sobe, conecta, responde) é o e2e que sobe a imagem native Linux em Docker (como no Coolify), no repo de e2e de fluxo (e2e-<cliente>-<fluxo>, separado) — coerente com o native buildar fora do repo do app. Compilar verde não garante runtime, mas o gate de runtime não mora no repo do app. Ver e2e native. - O nativeCompile é gate PRÉ-TAG, não só do workflow na tag. O build-deploy.yml (hoje o job build_native do pipeline.yml, 0027) compila native na tag (§Topologia) — se fosse o único, falha só-de-native (título OpenAPI não-ASCII usado como path em META-INF/swagger/; dep -SNAPSHOT ausente do registry) só apareceria pós-tag. O pré-flight da /xadm-release de app native builda o Dockerfile.native (docker build -f Dockerfile.native) como gate de container: roda nativeCompile num container limpo antes da tag — compila native e resolve toda dep pelo registry (sem mavenLocal, onde um -SNAPSHOT local se esconde). O e2e (mavenLocal/jar pré-buildado) prova o fio, não a disponibilidade da dep no registry que o deploy vai puxar. Ver java-micronaut §Native-image.

Topologia — o native builda no GitHub Actions do repo do app

A frota (7 apps: central-backend, integrador-server, integrador-sascar, maxsul-pied, vantroba-xls, onpetro-xls + o app de referência thoms-webstorm) convergiu para um modelo único de build native, reconciliando o caminho interino do plugin dockerBuildNative (base wolfi, imagem empurrada pelo próprio repo) para uma receita à mão + workflow de CI.

Correção 2026-09-12: topologia superada pela 0033.

  • O repo do app carrega os DOIS Dockerfiles. Dockerfile (JVM, dockerfile-java, shadowJar) é o caminho de exceção/fallback; Dockerfile.native (dockerfile-java-native, à mão) é o default. Nenhum dos dois builda no host — os dois são buildados off-host no CI e consumidos pelo Coolify via Build Pack Docker Image (nunca Dockerfile no painel).
  • As imagens (jar e/ou native) são buildadas por .github/workflows/build-deploy.yml (template rastreado, par dos Dockerfiles), no runner do GitHub — não no runner Forgejo self-hosted da org, e nunca na VM de prod. (Legado desde a 0027: hoje é o pipeline.yml, jobs build_* — ver a evolução acima.) Razão dura: o compilador native-image come ~6 GB de RAM/CPU e o shadowJar tem seu próprio pico; o runner self-hosted não aguenta e o build-spike na VM é o gatilho do freeze do host (coolify §Não buildar no host). O runner ubuntu-latest do GitHub (grátis, descartável) absorve o pico fora da infra de produção. Quais alvos o workflow builda por run: o trailer Deploy: <alvos> da tag (o /xadm-release o crava por diff de path desde a última tag), com fallback pro build.targets do docs/app.json. O template foi renomeado de build-native.yml para build-deploy.yml em 0.37.0 (o nome antigo enganava, com o jar buildando num "build-native").
  • Publish + gatilho via ponte SSH. O workflow dispara em tag v* (+ workflow_dispatch), builda linux/amd64, empurra a imagem pro registry Forgejo (fonte.xadm.biz/xadm/<slug>:{jar,native}-amd64 + :<sha>) com espelho GHCR, e então dispara o deploy. O Coolify roda a imagem via Build Pack Docker Image (nunca compila/builda no painel) — mas com Docker Image publicar não basta: o Coolify não puxa a imagem nova sozinho e não há webhook de repo (não é push de código). O gatilho é uma ponte SSH: uma chave forced-command na VM da casa que só roda deploy <uuid> (wrapper com whitelist dos recursos, sem shell) e faz o curl na API local do Coolify — o token do Coolify fica na VM, fora do GitHub. Isso supera a redação anterior ("publish-only, não deploya"): sem o gatilho, a imagem nova ficava no registry e o Coolify seguia rodando a velha até redeploy manual. Receita da ponte: coolify §Atualizar.
  • O plugin dockerBuildNative fica aposentado. A wolfi-base do plugin é mínima e não traz curl → o HEALTHCHECK da casa (§5) quebra; a receita da casa (dockerfile-java-native, debian-slim + curl) é a forma conforme única. Apps que ainda buildam native pelo plugin migram para o modelo acima — ação de repo de app (§3).

Nota (§3 · fonte no GitHub): buildar no GitHub Actions manda a árvore do app pro runner do GitHub — vale para app Java/Micronaut, nunca para fonte ZIM do ERP. O binário é amd64 (a frota roda em host amd64).

O header do dockerfile-java-native agora diz destino Dockerfile.native (par do Dockerfile JVM) — os dois coexistem no repo do app; não há mais "estado final de cara única".

Alternativas descartadas

  • Manter JVM (status quo). Não resolve o build-spike no host (a causa do freeze) nem o custo de RSS/imagem. A migração já estava acontecendo na frota sem norma; esta RD a formaliza.
  • Oracle GraalVM (em vez da CE). G1/PGO/build-report/-O3 real dariam binários melhores, mas com custo/licença que a casa não justifica agora. A CE entrega o ganho principal (RSS/imagem/boot) sem isso. Reavaliar se um app provar necessidade dura de PGO.