Docker multi-stage para Node.js é uma técnica para criar imagens menores, reproduzíveis e mais seguras. Em vez de manter compiladores, dependências de desenvolvimento e arquivos temporários na imagem final, o Dockerfile usa estágios separados para instalar, compilar e executar a aplicação. Apenas os artefatos necessários chegam ao runtime.
Essa separação reduz o tamanho da imagem, diminui a superfície de ataque e acelera distribuição entre registro, servidores e clusters. Ela não substitui atualização de dependências, varredura de vulnerabilidades ou configuração correta do processo. Para revisar a plataforma, consulte o que é Node.js e o guia sobre o que é Docker.
Por que usar múltiplos estágios?
Projetos TypeScript, bundlers e módulos nativos podem precisar de ferramentas que não são usadas em produção. Um estágio de build instala tudo e gera a pasta compilada. O estágio final começa de uma imagem limpa, instala somente dependências de produção e copia o resultado.
Sem essa separação, a imagem pode carregar código-fonte, caches, testes, credenciais acidentalmente copiadas e centenas de pacotes desnecessários. Uma imagem menor também reduz o tempo entre o deploy e o início do contêiner em novos nós.
Arquivos essenciais
Antes do Dockerfile, crie um .dockerignore. Ele evita que o contexto de build envie arquivos locais para o daemon ou serviço remoto.
node_modules
.git
.env
.env.*
coverage
dist
*.log
Dockerfile*
README.mdNão use o .dockerignore como única proteção para segredos. Eles não devem estar no diretório de build nem no histórico do repositório.
Dockerfile multi-stage
# syntax=docker/dockerfile:1
FROM node:24-bookworm-slim AS base
WORKDIR /app
ENV NODE_ENV=production
FROM base AS dependencies
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
FROM dependencies AS build
ENV NODE_ENV=development
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
FROM base AS production-dependencies
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --omit=dev && npm cache clean --force
FROM base AS runtime
COPY --from=production-dependencies /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json ./
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]O runtime não recebe TypeScript, ferramentas de teste ou compilador. O formato JSON do CMD permite que o processo Node.js receba sinais diretamente, o que ajuda no graceful shutdown.
Ordem das camadas e cache
Copie primeiro package.json e lockfile. Assim, uma mudança no código-fonte não invalida a camada de dependências. O cache do npm com BuildKit acelera downloads sem permanecer na imagem final.
Use npm ci para instalações reproduzíveis baseadas no lockfile. Se o package-lock estiver incompatível com o package.json, o build deve falhar em vez de atualizar silenciosamente a resolução.
NODE_ENV durante o build
Um erro comum é definir NODE_ENV=production cedo demais e depois esperar que ferramentas de desenvolvimento estejam disponíveis. No exemplo, o estágio de dependências instala tudo; o estágio de build define desenvolvimento; o estágio de produção instala somente o necessário para executar.
Outra opção é compilar em um estágio completo e executar npm prune --omit=dev, mas uma instalação limpa de produção costuma facilitar auditoria e reduzir arquivos residuais.
Usuário sem privilégio
Executar como root aumenta o impacto de uma falha. As imagens oficiais do Node.js incluem o usuário node. Garanta que os arquivos copiados sejam legíveis e que diretórios de escrita pertençam ao usuário correto.
RUN mkdir -p /app/tmp && chown node:node /app/tmp
USER nodeEvite tornar toda a aplicação gravável. Separe diretórios de dados temporários e prefira filesystem somente leitura quando a plataforma permitir.
Imagens base
Imagens slim baseadas em Debian equilibram compatibilidade e tamanho. Alpine é menor, mas usa musl em vez de glibc e pode exigir cuidados com módulos nativos. Distroless reduz ferramentas no runtime, porém torna diagnóstico interativo mais difícil.
Fixe uma versão principal suportada do Node.js e atualize regularmente a imagem base. Para builds altamente reproduzíveis, o digest pode ser fixado, mas um processo automatizado deve abrir atualizações quando o digest mudar.
Módulos nativos
Se uma dependência compila código nativo, o artefato precisa ser compatível com o sistema do runtime. Use bases compatíveis nos estágios de build e execução. Não compile em Alpine e copie para Debian, nem o contrário, sem confirmar a compatibilidade.
Ferramentas como Python, make e compiladores devem permanecer apenas no estágio que constrói o módulo. Isso evita carregá-las na imagem final.
Variáveis e segredos
Configurações de runtime entram por variáveis de ambiente, arquivos montados ou serviço de segredos. Não use ARG ou ENV para gravar tokens no Dockerfile, pois valores podem aparecer no histórico ou metadados.
Durante o build, use secret mounts do BuildKit para acessar registros privados sem persistir credenciais:
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm ciHealth check e readiness
Uma rota de health check deve ser barata. Liveness indica que o processo funciona; readiness indica que pode receber tráfego. Não faça o contêiner reiniciar por uma dependência externa temporariamente indisponível sem avaliar o efeito em cascata.
O artigo sobre health checks no Node.js detalha essas diferenças.
Graceful shutdown
Node.js deve receber SIGTERM, parar de aceitar conexões, aguardar requisições em andamento e fechar pools. Não execute a aplicação por meio de um shell desnecessário. Consulte graceful shutdown no Node.js.
process.on('SIGTERM', async () => {
server.close();
await closeDatabase();
process.exit(0);
});Segurança da imagem
- use base oficial e suportada;
- execute como usuário sem privilégio;
- copie somente artefatos necessários;
- não instale ferramentas de diagnóstico sem necessidade;
- gere SBOM e faça varredura de vulnerabilidades;
- assine imagens no pipeline;
- mantenha filesystem somente leitura quando possível;
- não inclua segredos no contexto.
Testando a imagem
O pipeline deve construir a imagem, iniciar o contêiner, aguardar readiness e executar testes HTTP. Confirme usuário, arquivos, variáveis obrigatórias e resposta a sinais.
docker build -t orders-api:test .
docker run --rm -d \
--name orders-api-test \
-p 3000:3000 \
-e DATABASE_URL=postgres://test \
orders-api:test
docker inspect \
--format '{{.Config.User}}' \
orders-api:testTambém verifique o tamanho e o conteúdo com ferramentas de análise de camadas. Um crescimento inesperado pode indicar caches ou diretórios copiados por engano.
CI e publicação
Automatize lint, testes, build, scan e push. Marque a imagem com commit imutável, além de tags amigáveis. Nunca substitua uma versão já implantada com conteúdo diferente. O tutorial sobre GitHub Actions ajuda a montar o pipeline.
Erros comuns
- Copiar todo o projeto antes do npm ci: qualquer mudança invalida o cache;
- Executar como root: uma falha ganha privilégios desnecessários;
- Levar devDependencies ao runtime: a imagem cresce e aumenta a superfície;
- Usar latest: builds podem mudar sem alteração no código;
- Gravar segredos em ENV: credenciais ficam persistidas;
- Usar npm start com shell extra: sinais podem não chegar corretamente.
Checklist
- .dockerignore está configurado;
- lockfile é usado com npm ci;
- build e runtime são separados;
- somente dependências de produção chegam ao final;
- processo usa usuário node;
- CMD está no formato exec;
- imagem foi escaneada e testada;
- shutdown recebe SIGTERM.
Referências oficiais
Conclusão
Docker multi-stage para Node.js separa as necessidades de construção das necessidades de execução. O resultado é uma imagem menor, com menos ferramentas, dependências e arquivos expostos.
Combine estágios claros, cache eficiente, usuário sem privilégio, secrets mounts, testes e atualizações regulares da base. Assim, a imagem deixa de ser apenas um pacote que inicia e se torna um artefato reproduzível, auditável e adequado para produção.




