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-bridgeem 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:alpinetem 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:
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:
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:
temporal-worker-1 exited with code 1 (restarting)
Error: Error relocating .../aarch64-unknown-linux-gnu/index.node: __register_atfork: symbol not foundEm 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.
| Ambiente | Como rodava | libc | Worker Temporal |
|---|---|---|---|
| Dev local | npm run start:dev no host | glibc (Mac/Linux) | OK |
| CI | npm test / integração fora do container | glibc | OK (não testava a imagem) |
| Produção | Container node:22-alpine | musl | Falha 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:
{
"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.
# 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 runtimeBullseye é 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:
- Qual contexto eu não passei? Se faltou, qual a chance da resposta mudar?
- O que ela assumiu como padrão? Imagem Docker padrão, biblioteca padrão, versão padrão.
- 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 base | Tamanho aprox. | libc | Native deps (ex. Temporal 1.5.x) |
|---|---|---|---|
node:22-alpine | ~50 MB | musl | Risco alto se dep usa binário glibc |
node:22-bullseye-slim | ~150 MB | glibc | Compatível na maioria dos casos |
node:22-bookworm-slim | ~160 MB | glibc | Mesmo 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:
- Alguma lib do
package.jsontemgyp,binding, ounode-pre-gypno nome? - O
npm installmostranode-gyp rebuildou demora anormal? - O CI testa contra a imagem real de produção?
- 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
# Subir Temporal local
./scripts/start-temporal.sh
# Reproduzir o erro (Alpine)
./scripts/reproduce-bug.sh
# Ver a correção (Bullseye)
./scripts/run-fixed.shO 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-gypou 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
| Etapa | O que aconteceu | Lição |
|---|---|---|
| Prompt | Pedido genérico "Dockerfile otimizado" | Contexto de dep nativa muda a resposta |
| Review | CI verde, homolog OK | Testes fora do container ≠ artefato de prod |
| Deploy | node:alpine + Temporal 1.5.2 | musl ≠ glibc; .node pré-compilado quebra |
| Fix | node:22-bullseye-slim | Imagem maior, runtime compatível |
| Processo | Checklist de revisão de IA | Revisar 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.
Artigos relacionados

Arquitetura Node errada: as 4 que vejo em todo projeto
Arquitetura Node errada tem nome: Controller Severino, God Service, Hexagonal de Cinema e NASA. Código real e quando cada padrão quebra em produção.
Ler publicaçãoNodeJS - Entendendo de vez `require`,`exports` e `module.exports`
require, exports e module.exports no Node.js explicados com exemplos e erros comuns. Entenda CommonJS de vez, sem decorar.
Ler publicação