Voltar
Node.jsDockerTemporalIACursorDevOpsArtigo · 12 de jun. de 2026 · 12 min

jonathanjuliani

Código de IA em produção: Alpine, Temporal e o erro que eu cometi

Aceitei um Dockerfile sugerido por IA sem revisar a fundo. Quatro horas depois, workers do Temporal caíram em produção — musl vs glibc e o que aprendi.

Você aceita um trecho de código que a IA sugeriu, não revisa a fundo, e sobe pra produção. Quatro horas depois, workers caindo, serviços fora do ar, deploy revertido.

O pior: o código da IA estava tecnicamente correto. O problema era o que ela não sabia — e eu não disse. Esse post-mortem é sobre código de IA em produção quando a resposta padrão encontra uma dependência nativa no caminho: NestJS, TemporalIO, node:alpine, e a diferença entre musl e glibc que ninguém no time estava olhando.

"Aceitei um trecho de código que a IA me sugeriu. Não revisei a fundo. Subi pra produção. Quatro horas depois, workers caindo, serviços fora do ar, deploy revertido."

— gancho do vídeo da Semana 2 (espelho deste post)

O que você vai aprender:

  • Por que IA acerta o padrão e mesmo assim quebra seu deploy
  • O prompt que eu fiz × o prompt que deveria ter feito (técnica do espelho)
  • Sintomas, logs e causa raiz: @temporalio/core-bridge em Alpine
  • O gap entre dev local, CI e container de produção
  • Diff completo do Dockerfile e tradeoff Alpine vs Bullseye
  • Checklist de revisão antes de aceitar código de IA em infra

NestJS em produção: o contexto que a IA não tinha

Pra esse erro fazer sentido, você precisa do contexto — não do tipo "trabalho com tech há X anos", e sim do ambiente onde o bug ficou invisível.

O sistema era um backend NestJS maduro em produção: processo de review, testes automatizados, homologação antes de deploy. A stack incluía PostgreSQL, filas, integrações externas — o pacote típico de serviço que já rodava há tempo sem drama.

Chegou a hora de integrar TemporalIO para orquestrar workflows assíncronos: jobs de longa duração, retries, processos que não cabem num setTimeout. Spike técnico, implementação normal, PRs no fluxo de sempre.

E como time de 2026, IA já fazia parte do workflow. Cursor, Copilot, Claude — pra boilerplate, revisão de trecho, rascunho de teste, e Dockerfile. Coisa que três anos atrás levava uma hora, hoje sai em dez minutos.

Talvez você já esteja nesse ponto também — IA virou parte do fluxo, não é mais novidade.

Tinha mais uma peça que combina mal com isso: quase ninguém rodava o backend via Docker localmente. A cultura era npm run start:dev ou npm run start:debug direto no Mac ou Linux do notebook. Breakpoint no VS Code, hot reload, zero fricção. Docker era coisa de deploy — CI/CD, Kubernetes, produção.

Justificativa clássica: compose pesado, memória, inércia. Funciona até o dia em que o artefato que sobe em produção não é o mesmo runtime que você testou por meses.

Esses dois fatos — IA no workflow + ninguém validando container local — eram campo minado. A gente não via assim.


O que eu pensava vs o que aprendi

O que eu pensava: Dockerfile é receita. IA sabe o padrão de produção. CI verde + homologação manual = seguro pra subir. node:alpine é o default inteligente — menor, mais rápido, todo tutorial recomenda.

O que aprendi: O padrão só funciona quando o runtime que você testa é o runtime que sobe em produção — e quando o prompt carrega as dependências que mudam a resposta. IA acelera o caminho conhecido. Quando você está no 10% específico do seu projeto, a aceleração vira armadilha se você confia no resultado sem validar o pressuposto.

A narrativa do incidente segue o arco que uso nos vídeos de erro-real: situação → conflito → tentativa que falhou → resolução → lição. A tentativa errada importa tanto quanto o fix.


TemporalIO no Node: o prompt que eu fiz × o que deveria ter feito

Em algum momento veio o Dockerfile. Esse serviço precisava de imagem própria, otimizada pra produção. Tarefa repetitiva, receita pronta — exatamente o tipo de coisa que a gente delega pra IA.

Abri o chat. Pedi mais ou menos isso:

"Me ajuda a montar o Dockerfile pra esse serviço Node em produção. Quero otimizado, multistage, leve."

A IA respondeu o que qualquer dev competente responderia pra essa pergunta: imagem base node:alpine, multistage build, cache de dependências, usuário não-root, healthcheck. Tudo certo. Bonito. Eficiente.

Olhei, achei coerente, copiei. Commit. PR. Code review do colega — foco em segurança e tamanho da imagem. Aprovado. CI verde. Homologação manual rodou. Deploy agendado.

Agora — e essa é a parte que eu quero que você pare e pense:

O que eu deveria ter perguntado, e não perguntei?

O prompt honesto teria sido:

"Me ajuda a montar o Dockerfile pra esse serviço Node em produção. Importante: esse serviço usa o SDK do TemporalIO, que tem dependências nativas. Quero otimizado, mas compatível."

Esse contexto — Temporal + dependência nativa — só eu tinha. A IA respondeu o melhor possível com o contexto que eu dei.

"Tá Jon, mas todo Dockerfile com node:alpine tem que falhar?"

Não. Pra 90% dos serviços Node, Alpine funciona perfeitamente. Mas o meu serviço caía nos 10%. E eu sabia que Temporal tinha binário nativo. Eu só não disse.


node:alpine em produção: sintomas e investigação

Deploy executado. Imagem buildada com exatamente o que a IA sugeriu:

código
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
 
FROM node:22-alpine AS runtime
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
ENV NODE_ENV=production
CMD ["node", "dist/main.js"]

O pod subiu. Health check genérico respondia. E aí vieram os alertas.

Sintomas: container vivo, processo Node morrendo ao carregar @temporalio/core-bridge. Workers do Temporal não inicializavam. Serviços que dependiam da integração começaram a falhar. Rollback, investigação, horas até isolar a causa.

Log típico em linux/amd64:

código
Error: Error loading shared library ld-linux-x86-64.so.2: No such file or directory
(needed by /app/node_modules/@temporalio/core-bridge/releases/x86_64-unknown-linux-gnu/index.node)

Em arm64 (ou reprodução em Apple Silicon), a mensagem muda de forma, mas o padrão é o mesmo — falha ao carregar o .node contra a libc do container:

código
temporal-worker-1 exited with code 1 (restarting)
Error: Error relocating .../aarch64-unknown-linux-gnu/index.node: __register_atfork: symbol not found

Em ambos os casos: dynamic linker não resolve o binário nativo — musl no Alpine vs binário compilado para glibc.

A tentativa que não funcionou

Primeiro movimento foi rollback — correto. Depois, o time ficou preso na pergunta óbvia: o que mudou nesse deploy? Resposta: integração Temporal. Código novo, Dockerfile novo.

A tentativa que não resolveu: tratar como bug de aplicação. Revisar diff de TypeScript. Rodar testes de novo no CI (fora do container). Comparar versões de Node no package.json. Tudo verde — porque ninguém estava testando o artefato que ia pro cluster.

A lição está na tentativa falha: quando produção roda em imagem que dev nunca executou, investigar só o código-fonte é perder horas olhando pro lugar errado.

"Tá Jon, mas se funcionou no dev e no homologação, por que iria quebrar só em produção?"

Essa pergunta travou o time por um tempo. Porque em tese o ambiente era o mesmo: Node, mesmas versões, mesmo código. Em tese.

A diferença estava num lugar que ninguém olhava: o runtime do container.

AmbienteComo rodavalibcWorker Temporal
Dev localnpm run start:dev no hostglibc (Mac/Linux)OK
CInpm test / integração fora do containerglibcOK (não testava a imagem)
ProduçãoContainer node:22-alpinemuslFalha ao carregar .node

musl vs glibc: a causa raiz por trás do Temporal SDK

A imagem node:alpine usa Alpine Linux — uns ~50 MB contra 350+ MB de uma base Debian completa. O motivo do tamanho: distribuição minimalista com musl libc.

Linux convencional — Ubuntu, Debian, RHEL — usa glibc. É a implementação da C standard library usada há décadas, suportada por praticamente tudo.

Alpine usa musl. Menor, eficiente, parte do motivo de Alpine ser enxuta. Mas não é binariamente compatível com glibc.

Pra aplicações Node puras — JavaScript ou TypeScript compilado — isso é irrelevante. O runtime abstrai.

Mas algumas dependências não são JavaScript puro. Toda vez que você vê node-gyp no npm install, ou uma pasta build/ com arquivo .node dentro, isso é binário nativo compilado contra a libc do sistema.

No nosso caso, @temporalio/worker@1.5.2 puxa @temporalio/core-bridge, que distribui bibliotecas pré-compiladas para alvos *-unknown-linux-gnu — ou seja, glibc. Dentro de Alpine (musl), o linker não encontra o que precisa.

Snippet relevante do package.json da época:

código
{
  "dependencies": {
    "@temporalio/worker": "1.5.2",
    "@temporalio/client": "1.5.2"
  }
}

"Tá Jon, mas eu instalei a lib no container, ela compilou, não daria certo?"

Boa pergunta. Em alguns casos sim — a lib detecta Alpine e compila contra musl. Em outros, não. Algumas libs assumem glibc ou usam binários pré-compilados que não funcionam em musl. O Temporal 1.5.2 caía nesse segundo grupo.

Talvez você esteja pensando: "mas Jon, a IA não deveria saber disso?". Pode até saber, se você perguntar no contexto certo. Com o prompt que eu dei — Dockerfile Node otimizado, multistage — a resposta correta é node:alpine. A IA não tem como adivinhar quais dependências o seu projeto vai instalar daqui a três semanas.

Nota de versão: SDKs Temporal 1.10+ melhoraram documentação e cenários Alpine. O princípio permanece: native deps são contrato de plataforma — e a versão importa.


Dev, CI e container: por que ninguém viu antes do deploy

Porque local, todo mundo rodava Node direto. Glibc. Dependências nativas resolviam, funcionavam, ninguém via problema.

O CI rodava testes unitários e integração fora do container de produção. Validava regras de negócio e cobertura — não o artefato final que ia pro cluster.

Homologação exercitava fluxos manuais no mesmo runtime de dev, não na imagem Alpine.

Esse era o gap. Invisível. Por meses.

O detalhe que dói: se eu tivesse rodado o Docker localmente — mesmo uma vez, antes de mergear — o erro teria aparecido na minha máquina, não no log da produção.

Isso conecta direto com o que eu argumentei no post sobre arquitetura Node errada: o gap entre o que o tutorial ensina e o que produção exige não é só sobre pastas e controllers — é sobre qual ambiente você realmente valida.


node:bullseye vs Alpine: o fix e o tradeoff de tamanho

A solução foi simples: trocar a imagem base.

código
# Antes — musl, Worker não sobe
FROM node:22-alpine AS builder
# ...
FROM node:22-alpine AS runtime
 
# Depois — glibc, Worker sobe
FROM node:22-bullseye-slim AS builder
# ...
FROM node:22-bullseye-slim AS runtime

Bullseye é Debian. Usa glibc. As dependências nativas encontram o que precisam. Workers subiram, serviços voltaram.

Imagem ficou maior — aceitamos esse tradeoff.

O restante do Dockerfile (multistage, cache de layers, usuário não-root) continuou válido. A IA não errou no padrão — errou no pressuposto que eu não corrigi.


4 princípios pra usar código de IA sem cair na armadilha

A parte mais importante não é a troca da imagem. É o seguinte:

E se o problema não fosse a IA ter sugerido Alpine? E se fosse o jeito que eu pedi?

Spoiler: era.

1. IA não sabe seu stack

IA é boa em padrão. "Dockerfile pra Node em produção", "API REST com Express", "componente React com hook X". Tudo isso ela faz bem porque viu milhões de exemplos.

O que ela não sabe é o seu projeto: quais libs você instalou, que versão de Node precisa, que esse serviço tem dependência nativa, que o CI valida ou não a imagem real, que produção roda Alpine ou Debian.

Esse contexto só você tem. Se você não passar, ela responde o padrão. O padrão funciona em 90% dos casos. O seu projeto vive nos 10%.

2. Revisar código de IA é revisar duas vezes

Code review humano: faz sentido? está claro? tem bug?

Code review de IA precisa de segunda camada: o que ela não viu?

Perguntas que eu rodo hoje, antes de aceitar trecho não-trivial de IA:

  1. Qual contexto eu não passei? Se faltou, qual a chance da resposta mudar?
  2. O que ela assumiu como padrão? Imagem Docker padrão, biblioteca padrão, versão padrão.
  3. Isso atravessa um boundary do meu sistema? Infra, banco, API externa, build — se sim, dobra a revisão.

Não é desconfiar da IA. É lembrar que ela responde com o que você deu — e nunca com o que ela não pode saber.

3. Alpine não é grátis

Técnico puro — independente de IA. Alpine é boa; uso até hoje em contextos certos. Mas tem tradeoff real que tutoriais — humanos ou IA — raramente mencionam: musl versus glibc.

Pra Node puro, Alpine funciona perfeitamente. Com dependência nativa — node-gyp, arquivos .node, SDKs com bindings — verifique compatibilidade com musl antes. Não assume.

Imagem baseTamanho aprox.libcNative deps (ex. Temporal 1.5.x)
node:22-alpine~50 MBmuslRisco alto se dep usa binário glibc
node:22-bullseye-slim~150 MBglibcCompatível na maioria dos casos
node:22-bookworm-slim~160 MBglibcMesmo perfil, Debian mais novo

node:bullseye-slim ou node:bookworm-slim entregam imagem enxuta com glibc. Meio termo decente.

4. Native deps são contrato de plataforma (e a IA não vê esse contrato)

Toda vez que você adiciona dependência nativa, faz contrato implícito com a plataforma onde o código vai rodar. A IA, ao sugerir Dockerfile, não vê esse contrato — vê o pedido, vê padrões, sugere.

Checklist antes de mergear qualquer Dockerfile — gerado por IA ou por mim:

  1. Alguma lib do package.json tem gyp, binding, ou node-pre-gyp no nome?
  2. O npm install mostra node-gyp rebuild ou demora anormal?
  3. O CI testa contra a imagem real de produção?
  4. Rodei o container localmente antes do merge?

"Tá Jon, mas eu não consigo decorar isso pra cada PR."

Coloca como step no CI ou no CONTRIBUTING.md. Não precisa ser memória — precisa ser fluxo. Três perguntas. Dois minutos. Teria evitado horas de incidente.


Reproduza o bug em 2 comandos

Montei um projeto mínimo que reproduz o incidente: NestJS + Temporal SDK 1.5.2 + Dockerfile Alpine que falha + Bullseye que corrige.

Repositório público: github.com/jonathanjuliani/code-samples/tree/main/alpine-temporal-bug

código
# Subir Temporal local
./scripts/start-temporal.sh
 
# Reproduzir o erro (Alpine)
./scripts/reproduce-bug.sh
 
# Ver a correção (Bullseye)
./scripts/run-fixed.sh

O README do repo é post-mortem técnico completo — útil se você quiser mostrar isso pro time ou gravar demo.


Checklist: 10 minutos que teriam evitado 4 horas

Use antes de aceitar código de IA em Dockerfile, compose ou pipeline:

  • Passei todas as deps nativas e constraints de runtime no prompt?
  • Revisei o que a IA assumiu como padrão (imagem base, versão, libc)?
  • O diff atravessa boundary de infra/build/deploy?
  • Existe .node, node-gyp ou SDK com binding no caminho?
  • O CI builda e testa dentro da imagem de produção?
  • Rodei o container localmente pelo menos uma vez?

Esse checklist é o núcleo da newsletter desta semana — "10 minutos que teriam evitado 4 horas": post-mortem em detalhe + o que eu teria perguntado pra IA antes de aceitar o código. Inscrição em /pt-BR/newsletter.


O que eu ainda não sei

Não tenho resposta fechada pra tudo — e acho importante deixar isso explícito:

  • Se o mesmo incidente aconteceria com Temporal SDK 1.10+ e Alpine hoje — a documentação melhorou, mas native deps continuam sendo contrato de plataforma.
  • Qual é o custo mínimo de CI pra validar imagem real sem travar o time — ainda estou calibrando isso nos projetos que pego.
  • Se feature flags teriam mascarado o problema por mais tempo (spoiler: provavelmente sim — e isso seria pior).

Se você já testou Alpine com deps nativas recentes e o resultado foi diferente do meu, comenta — isso enriquece o post.


Próximos passos

Se o gap tutorial-vs-produção te soa familiar no contexto de arquitetura — controllers inchados, pastas que mentem — vale ler Arquitetura Node errada: as 4 que vejo em todo projeto.

Se workers assíncronos e filas te levam a repensar como Node lida com concorrência e I/O, Promises em JavaScript: do zero ao async/await ajuda a separar o que é runtime JS do que é contrato de plataforma.


TL;DR

EtapaO que aconteceuLição
PromptPedido genérico "Dockerfile otimizado"Contexto de dep nativa muda a resposta
ReviewCI verde, homolog OKTestes fora do container ≠ artefato de prod
Deploynode:alpine + Temporal 1.5.2musl ≠ glibc; .node pré-compilado quebra
Fixnode:22-bullseye-slimImagem maior, runtime compatível
ProcessoChecklist de revisão de IARevisar o que ela fez e o que ela não viu

O que me ficou desse incidente não foi só o tempo offline. Foi perceber que deleguei decisão de infra pra IA sem dar o contexto que só eu tinha. A IA não errou — respondeu o que qualquer dev competente responderia pra pergunta que eu fiz.

O erro foi meu, em três camadas: não passei o contexto, não revisei pensando no que ela não viu, e não rodei o container localmente antes de mergear.

Se você já passou por algo parecido — IA entregou código limpo, você aceitou, o tiro saiu pela culatra — me conta nos comentários. Qual ferramenta era (Cursor, Copilot, Claude?) e o que aconteceu. Se viu alguma cagada neste post-mortem, fala também — a gente troca.

Se quiser o checklist em formato newsletter e sinais de produção toda semana, inscreve na newsletter.

Inscreva-se no Ledger da Engenharia

Receba notas de arquitetura e performance na sua caixa de entrada. Mesma lista do convite—inscreva-se aqui quando quiser.

Sem spam. Sem APIs de terceiros. Apenas eu enviando updates.

O Ledger da Engenharia

Transmissoes quinzenais sobre arquitetura, performance e engenharia aplicada. Inscreva-se a partir de qualquer artigo—sem spam.