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-reportsão Oracle-GraalVM-only (indisponíveis); e-O3em 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.jsonganhanative: true. É o discriminador canônico de "este app deploya native" — o sinal que o gate doci.ymle o e2e native leem (server que deploya viadockerfile-java-native).
Correção 2026-09-12: o app é native se e só se o
build.targetsdodocs/app.jsoncontémnative; o camponativesaiu (0033). - Template Dockerfile native.templates/dockerfile-java-native(rastreado, par dodockerfile-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 trailerDeploy:da tag /build.targetsdoapp.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 debuild-native.ymlem 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), jobsbuild_<alvo>edeploy; obuild-deploy.ymlsaiu do kit. -imageName.set("application")é obrigatório nograalvmNative. OrootProject.namediverge do slug em vários apps (ex.central-backend,onpetro-xls); sem fixar o nome, onativeCompileemite o binário com o nome dorootProjecte oCOPY build/native/nativeCompile/applicationdo Dockerfile quebra. O nome do binário da casa éapplication. - No native, oSENTRY_DSNvem por ENV, não por ponte no build. ODockerfile.nativetemENTRYPOINTexec, sem shell —["/usr/bin/tini", "--", "/app/application"]: otinié o init de PID1 (colhe ocurl <defunct>doHEALTHCHECK, java-micronaut §Deploy),--separa o init do binário, e continua puro (tinié exec, nãosh -c). O DSN chega pelo ambiente do Coolify e o logback resolve${SENTRY_DSN:-}em runtime. AlgunsDockerfileJVM injetam o DSN viajqdoapp.jsonnum 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 doapp.json== nome da imagem no registry. A convenção de nomenclatura de slug/registry (e por que o slug pode divergir doapp_id) vive em 0024. - Reflect-config para o que instancia por reflexão. OSentryAppenderdo 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>dologback.xmlsã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:nativeCompileverde no repo do app; runtime no repo de e2e. No repo do app o gate é onativeCompilecompilar — 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. - OnativeCompileé gate PRÉ-TAG, não só do workflow na tag. Obuild-deploy.yml(hoje o jobbuild_nativedopipeline.yml, 0027) compila native na tag (§Topologia) — se fosse o único, falha só-de-native (título OpenAPI não-ASCII usado como path emMETA-INF/swagger/; dep-SNAPSHOTausente do registry) só apareceria pós-tag. O pré-flight da/xadm-releasede app native builda oDockerfile.native(docker build -f Dockerfile.native) como gate de container: rodanativeCompilenum container limpo antes da tag — compila native e resolve toda dep pelo registry (semmavenLocal, onde um-SNAPSHOTlocal 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 PackDocker Image(nuncaDockerfileno 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 é opipeline.yml, jobsbuild_*— 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 runnerubuntu-latestdo GitHub (grátis, descartável) absorve o pico fora da infra de produção. Quais alvos o workflow builda por run: o trailerDeploy: <alvos>da tag (o/xadm-releaseo crava por diff de path desde a última tag), com fallback probuild.targetsdodocs/app.json. O template foi renomeado debuild-native.ymlparabuild-deploy.ymlem 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), buildalinux/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 PackDocker 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ó rodadeploy <uuid>(wrapper com whitelist dos recursos, sem shell) e faz ocurlna 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
dockerBuildNativefica 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-nativeagora diz destinoDockerfile.native(par doDockerfileJVM) — 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/
-O3real 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.