multi-stage builds em imagens de produção: erros que geram falhas, lentidão ou retrabalho
O build passou, mas o container falhou? Entenda como corrigir dependências ausentes, incompatibilidades, cache ineficiente e permissões em multi-stage builds, com exemplos de Node.js e validações da imagem final.
Max Alex

Neste artigo (9 tópicos)
Multi-stage builds em produção: erros que causam falhas, lentidão e retrabalho
Como organizar estágios, dependências, cache e validações para entregar imagens Docker que funcionam fora do ambiente de build.
O pipeline termina sem erros, a imagem é publicada e o container encerra assim que inicia. Em outra equipe, qualquer alteração de código dispara uma instalação completa de dependências. Os dois problemas podem nascer de decisões no mesmo Dockerfile.
Usar multi-stage builds em produção permite separar construção e execução. Para obter uma imagem confiável, porém, é preciso definir o que atravessa essa fronteira, verificar sua compatibilidade e testar o resultado entregue.
O exemplo deste guia é um serviço Node.js com TypeScript. A compilação gera dist/server.js, mas a aplicação continua dependendo de pacotes instalados em node_modules. Casos com módulos nativos exigem verificações adicionais.
O build passou, mas a imagem falhou: onde investigar primeiro
Cada instrução FROM inicia um estágio com um sistema de arquivos derivado da base escolhida. Um estágio pode partir de outro já declarado; quando usa uma nova imagem-base, os arquivos criados no builder não aparecem automaticamente nele.
Considere este trecho de um Docker multi-stage build:
FROM node:24-bookworm-slim AS runtime
WORKDIR /app
COPY --from=build /app/dist ./dist
CMD ["node", "dist/server.js"]Ele pressupõe um estágio anterior chamado build. Se o JavaScript gerado importar um pacote externo que não foi incorporado ao artefato, copiar apenas dist pode provocar Cannot find module ou ERR_MODULE_NOT_FOUND.
O compilador conseguiu produzir arquivos. Isso não confirma que o ambiente final tenha tudo para executá-los.
- Módulo ou arquivo ausente: confira dependências, templates e caminhos.
- Erro ao carregar um binário: investigue arquitetura, bibliotecas e versão do runtime.
- Permissão negada: verifique usuário, propriedade e diretórios graváveis.
- Build repetidamente lento: examine contexto, ordem das cópias e cache disponível.
Se precisar revisar essa separação, o guia sobre como funcionam os estágios de um Dockerfile oferece a base para acompanhar o diagnóstico.
Copiar o artefato sem suas dependências de execução
Antes de modificar o Dockerfile, liste o que o processo abre ou carrega: pacotes, arquivos estáticos, templates, certificados específicos, migrações e configurações não sensíveis. Um diretório compilado só é autossuficiente quando o processo de empacotamento realmente produz esse resultado.
No Node.js, pacotes importados durante a execução devem estar declarados em dependencies. TypeScript e ferramentas de teste normalmente ficam em devDependencies. Confira também o package.json: o campo type, por exemplo, pode ser necessário para interpretar os arquivos gerados.
O Dockerfile abaixo separa instalação, compilação, testes e runtime. Ele pressupõe um projeto simples, sem workspaces, com scripts build e test, e compilação configurada para gerar dist/server.js.
# syntax=docker/dockerfile:1
ARG NODE_IMAGE=node:24-bookworm-slim
FROM ${NODE_IMAGE} AS base
WORKDIR /app
FROM base AS deps
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
FROM deps AS build
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
FROM build AS test
COPY test ./test
RUN npm test
FROM base AS prod-deps
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev
FROM base AS runtime
ENV NODE_ENV=production
COPY --from=prod-deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json ./
RUN mkdir /app/cache && chown node:node /app/cache
USER node
CMD ["node", "dist/server.js"]npm ci --omit=dev instala as dependências de produção a partir do lockfile. Ele ainda pode executar scripts de instalação. Se algum script do projeto precisar de código-fonte ou de ferramentas de desenvolvimento, ajuste esse fluxo explicitamente; não acrescente --ignore-scripts sem verificar suas consequências.
Adapte os caminhos dos testes e copie os recursos que sua aplicação realmente utiliza. Em monorepos, inclua os manifests dos workspaces e os arquivos exigidos pela instalação.
O exemplo não instala compiladores. Pacotes nativos que precisem de compilação podem exigir ferramentas nos estágios de instalação e bibliotecas compartilhadas no runtime. Evite resolver essas diferenças copiando todo o diretório do builder: isso também pode transportar testes, fontes e dependências desnecessárias.
Misturar bases e arquiteturas incompatíveis entre os estágios
Uma cópia bem-sucedida não demonstra compatibilidade. Um módulo nativo instalado em Debian pode depender de glibc e de bibliotecas que não existem em uma imagem Alpine, cuja base usa musl.
A falha costuma aparecer quando a aplicação carrega o módulo. Mensagens sobre bibliotecas compartilhadas ausentes, símbolos não encontrados ou formato de executável são pistas para investigar o ambiente de execução.
Comece alinhando a família da distribuição, a versão do Node.js e a plataforma entre instalação e runtime. No exemplo, todos os estágios derivam da mesma base. Isso reduz diferenças, mas não instala automaticamente bibliotecas adicionais exigidas pelos pacotes.
Para testar um módulo nativo, execute seu carregamento na imagem final:
docker run --rm --entrypoint node servico:candidato \
-e "require('nome-do-modulo-nativo')"Substitua o nome pelo pacote real e acrescente uma operação representativa: algumas bibliotecas carregam componentes apenas quando uma função é chamada. Para pacotes exclusivamente ESM, adapte a verificação para import().
Confira também a arquitetura. Dependências instaladas para linux/amd64 não devem ser tratadas como artefatos portáveis para linux/arm64. Em builds multiplataforma, instale ou compile para cada destino e valide cada variante publicada. Se usar um estágio preso à plataforma do builder, configure a compilação cruzada quando necessária.
Alpine pode ser uma escolha adequada, mas a decisão deve considerar suporte dos pacotes, bibliotecas e manutenção. Compare o resultado funcional antes de aceitar a redução de tamanho.
Invalidar o cache a cada alteração de código
Quando COPY . . vem antes da instalação, uma mudança em um arquivo de código pode invalidar a camada que antecede npm ci. A instalação volta a executar mesmo sem alteração nas dependências.
No exemplo, manifests e lockfile entram primeiro. O código-fonte entra depois da instalação. Para conferir o efeito, faça dois builds, alterando apenas um arquivo TypeScript entre eles:
docker build --progress=plain --target runtime -t servico:candidato .Nos logs do segundo build, procure CACHED nas etapas de dependências. A compilação deve executar novamente quando o código mudar.
Separe três mecanismos:
- Cache de camadas: reutiliza o resultado de uma instrução quando suas entradas relevantes continuam iguais.
- Cache do npm: o mount em
/root/.npmreaproveita downloads quando a instalação precisa executar. - Cache externo: permite recuperar resultados em outro builder, inclusive em runners efêmeros.
No CI, configure importação e exportação com --cache-from e --cache-to, usando um backend compatível. Não presuma que exportar camadas também persista o conteúdo dos cache mounts.
Meça separadamente o build inicial, a repetição sem mudanças, a alteração de código e a alteração de lockfile. --no-cache impede reutilizar camadas, mas não equivale necessariamente a um ambiente sem cache de downloads. Para uma medição realmente fria, controle também o estado do builder e dos mounts.
Enviar arquivos desnecessários e usar COPY --from sem precisão
Contexto de build e imagem final são conjuntos diferentes. Um arquivo pode ser disponibilizado ao builder e nunca entrar no runtime. Mesmo assim, pode aumentar transferências ou interferir nas etapas que o consomem.
Use um .dockerignore adequado ao projeto. Para o serviço apresentado, um ponto de partida é:
node_modules
dist
.git
coverage
*.log
.env
.env.*
.npmrcA lista pressupõe compilação dentro do Docker e credenciais fornecidas separadamente. Preserve arquivos necessários ao build; se houver um exemplo de configuração indispensável, crie uma exceção específica.
Com uma cópia ampla, node_modules local pode sobrescrever ou se misturar às dependências instaladas no container. Isso introduz arquivos produzidos para outro sistema operacional ou arquitetura. A combinação de exclusões e cópias explícitas reduz essa interferência.
Nomeie estágios como build, prod-deps e runtime. Em COPY --from=build /app/dist ./dist, a origem é resolvida a partir da raiz do estágio de origem; o destino relativo usa o WORKDIR do estágio atual.
Quando um arquivo não aparecer onde deveria, confira a saída real da compilação e ambos os caminhos antes de ampliar a cópia.
Descobrir problemas de permissões e inicialização apenas no deploy
Execute o container com o usuário previsto para produção. Um serviço iniciado como root pode esconder permissões que falharão sob uma política mais restritiva.
No Dockerfile proposto, o código pode permanecer pertencendo a root e legível pelo usuário node. Somente /app/cache recebe propriedade para escrita. Isso evita tornar toda a aplicação modificável pelo processo.
Reproduza a necessidade de escrita com uma verificação direta:
docker run --rm --entrypoint node servico:candidato \
-e "require('node:fs').writeFileSync('/app/cache/probe', 'ok')"Se houver erro de acesso, ajuste o diretório necessário. Para arquivos que precisem pertencer ao usuário da aplicação, COPY --chown=node:node permite definir a propriedade durante a cópia. Evite usar permissões irrestritas como correção genérica.
Confira WORKDIR, CMD e qualquer ENTRYPOINT herdado. A forma executável de CMD, como no exemplo, evita adicionar um shell ao comando de inicialização. A aplicação ainda precisa tratar os sinais de encerramento e concluir operações pendentes.
Imagens mínimas podem não conter shell ou utilitários chamados por scripts. Para conexões HTTPS, teste a cadeia de confiança usada pela aplicação, incluindo autoridades certificadoras privadas quando existirem. Não contorne erros de certificado desativando a validação TLS.
Se o ambiente usar sistema de arquivos somente leitura, forneça mounts graváveis para os caminhos necessários. A propriedade configurada na imagem não concede escrita em um filesystem montado como somente leitura.
Usar multi-stage como garantia de segurança e reprodutibilidade
Remover compiladores da imagem final reduz componentes entregues. Isso não substitui análise de vulnerabilidades, atualização de dependências e inspeção do conteúdo publicado.
Para imagens Docker reproduzíveis, comece por lockfiles versionados e instalações consistentes. Tags de imagens-base podem mudar; fixar uma referência por digest torna essa entrada explícita. Defina também um processo para atualizar o digest e repetir as verificações.
Esses controles aumentam a previsibilidade. Uma reprodução binária idêntica exige controlar outras entradas, como ferramentas, downloads externos, plataforma e timestamps produzidos pelo build.
Credenciais temporárias para pacotes privados
Não use ARG ou ENV para proteger tokens. Para um registro npm privado, substitua a instalação do estágio correspondente por uma instrução com secret mount:
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc,required=true \
--mount=type=cache,target=/root/.npm \
npm ci --omit=devForneça o arquivo de autenticação pelo cliente de build, ajustando seu caminho local:
docker build --secret id=npmrc,src=/caminho/seguro/npmrc \
--target runtime -t servico:candidato .Se a instalação completa também acessar pacotes privados, aplique o mount ao estágio deps. O segredo fica disponível temporariamente para a instrução, mas um script ainda pode copiá-lo ou imprimi-lo. Revise logs, artefatos e camadas exportadas; confirme que arquivos de autenticação não chegaram ao runtime.
Excluir uma credencial em uma instrução posterior não remove o conteúdo de uma camada anterior. Evite introduzi-la por COPY desde o início.
Testar o builder e publicar uma imagem final nunca validada
Um estágio chamado test não é executado apenas por existir. O BuildKit processa as dependências do alvo solicitado. No Dockerfile deste guia, runtime não depende de test; o pipeline precisa solicitar os testes explicitamente.
docker build --target test --progress=plain .
docker build --target runtime -t servico:candidato .Configure o CI para interromper a entrega se o primeiro comando falhar. Quando for necessário executar os testes novamente apesar de uma camada válida, defina uma política de invalidação específica para esse estágio.
Depois, valide a imagem final. Supondo que o serviço escute em 0.0.0.0:3000 e ofereça /health, um smoke test local pode começar assim:
docker run -d --name servico-smoke \
-p 127.0.0.1:3000:3000 servico:candidato
curl --fail --retry 10 --retry-connrefused --retry-delay 1 \
http://127.0.0.1:3000/health
docker logs servico-smoke
docker stop --time 10 servico-smoke
docker inspect --format '{{.State.ExitCode}}' servico-smoke
docker rm servico-smokeAdapte porta, endpoint e variáveis. No CI, garanta a limpeza mesmo se a consulta falhar. Acrescente uma operação essencial que carregue templates, módulos nativos e demais recursos relevantes; um endpoint que retorna uma constante não cobre essas dependências.
Observe também o encerramento: o processo deve receber o sinal, finalizar dentro do prazo e registrar um resultado coerente. Repita a validação sob as restrições relevantes do ambiente, incluindo usuário, mounts e limites de recursos.
Para publicar, envie a imagem candidata ao registro, obtenha o digest e valide essa referência imutável. Em imagens multiplataforma, cubra as variantes entregues. Promova o mesmo artefato aprovado; reconstruí-lo depois do teste cria outra saída que precisa de validação.
Como revisar um multi-stage build antes da próxima publicação
Uma revisão útil conecta o sintoma a uma verificação observável. Use a tabela para priorizar a investigação do seu Dockerfile para produção.
| Sintoma | Causa provável | Verificação | Correção |
|---|---|---|---|
| Módulo ou recurso ausente | Runtime recebeu somente parte da aplicação | Executar a operação que carrega o recurso na imagem final | Incluir dependências de runtime e arquivos necessários |
| Módulo nativo não carrega | Incompatibilidade de libc, ABI ou arquitetura | Carregar e exercitar o módulo em cada plataforma | Alinhar bases e produzir dependências para o destino |
| Instalação repete após mudar código | Código copiado antes das dependências | Comparar logs após alterar somente um arquivo-fonte | Copiar manifests e instalar antes de copiar o código |
| CI perde o ganho observado localmente | Runner inicia com outro builder sem cache | Inspecionar importação e exportação do cache | Configurar persistência compatível com o ambiente |
| Arquivo aparece no local errado | Origem ou destino da cópia incorretos | Conferir saída do build e WORKDIR final | Usar caminhos explícitos e estágios nomeados |
| Permissão negada | Usuário sem acesso ao caminho necessário | Reproduzir leitura ou escrita com o usuário final | Ajustar somente propriedade, permissões ou mount necessários |
| Testes passam, deploy falha | Imagem final não foi exercitada | Executar smoke test pela referência publicada | Validar e promover o mesmo digest |
No serviço Node.js, os critérios de aceite são concretos: iniciar sem ferramentas de desenvolvimento, carregar dependências e recursos, escrever apenas nos locais previstos, responder à operação essencial e encerrar corretamente. Após uma mudança somente no código, a instalação de dependências deve continuar aproveitando o cache quando suas entradas permanecerem iguais.
Registre tamanho da imagem, tempo de build frio, tempo após mudança de código, tempo após mudança no lockfile e resultado dos testes. Compare antes e depois sob condições equivalentes de máquina, rede e cache.
A otimização de imagens Docker deve considerar funcionamento e manutenção junto com tamanho. Não existe um percentual universal de redução que comprove a qualidade de um multi-stage build.
Escolha um Dockerfile usado em produção e aplique a revisão deste guia antes do próximo deploy. Registre as falhas encontradas e compare o tempo de build, o tamanho da imagem e o funcionamento do container após os ajustes.
Max Alex
Desenvolvedor full stack especializado em SaaS, Laravel, IA aplicada, WhatsApp API, automações e integrações. Criação de sistemas web escaláveis para empresas.
Continue lendo

Como implementar multi-stage builds em imagens de produção: guia prático para produção
Aprenda a implementar multi-stage builds Docker para separar compilação e runtime, reduzir imagens de produção e fortalecer a segurança dos containers.

OpenTelemetry em microserviços: erros que geram falhas, lentidão ou retrabalho
Traces incompletos, overhead e perda de telemetria podem ter causas diferentes. Entenda oito erros de OpenTelemetry em microserviços e use um checklist para diagnosticar, corrigir e validar a instrumentação em produção.

GitHub Actions em monorepos: erros que geram falhas, lentidão ou retrabalho
Seu CI executa projetos demais, ignora dependências ou bloqueia merges? Conheça dez erros de GitHub Actions em monorepos e como verificar filtros, checks, cache, matrizes e permissões.