1001Ferramentas
🐳 Dev

Dockerfile multistage build

Gera um Dockerfile multistage (builder + runtime) para apps Node.js — copia node_modules apenas do builder.

Por que o build em vários estágios deixa a imagem pequena

Uma imagem Docker feita sem cuidado carrega tudo que foi usado para construí-la: compilador, dependências de desenvolvimento, cache do gerenciador de pacotes, código-fonte. Isso incha a imagem, aumenta o tempo de deploy e amplia a superfície de ataque — cada ferramenta a mais lá dentro é uma ferramenta a mais na mão de quem conseguir entrar no contêiner.

O build em vários estágios corta esse problema pela raiz. Um primeiro estágio, o builder, instala tudo e compila. Um segundo estágio parte de uma imagem limpa e copia apenas o resultado, com COPY --from=builder. Tudo que ficou no primeiro estágio é descartado e não aparece na imagem final. Informe a versão do Node e a página gera o Dockerfile com os dois estágios já encadeados.

A ordem das instruções no arquivo gerado não é casual. O package.json é copiado e as dependências instaladas antes do restante do código, porque cada instrução vira uma camada com cache próprio: mudar uma linha do código-fonte não invalida a camada de npm ci, e o build seguinte aproveita o que já estava pronto. Inverter essas duas linhas faz a instalação inteira rodar de novo a cada alteração.

Perguntas frequentes

Quantos estágios posso ter?
Quantos precisar. É comum ver três: um para instalar dependências, um para compilar e um final só com o resultado. Também dá para copiar de um estágio que não é o imediatamente anterior, e até de uma imagem externa, com COPY --from=nginx:alpine.
Por que npm ci e não npm install?
O ci instala exatamente o que está no package-lock.json, sem atualizar nada, e falha se o lock estiver fora de sincronia com o package.json. Em build isso é o que você quer: resultado idêntico a cada execução. O install pode alterar o lock, o que torna a imagem não reproduzível.
A imagem final precisa ser a mesma do builder?
Não, e frequentemente não é. O padrão gerado aqui mantém o Node porque a aplicação precisa dele para rodar. Para binário compilado — Go, Rust — o estágio final pode ser uma imagem distroless ou até scratch, sem shell nem gerenciador de pacotes, o que reduz a imagem a poucos megabytes.

Ferramentas Relacionadas