Dockerfile em produção
O Dockerfile que aparece no primeiro tutorial funciona. Ele também produz uma imagem de
1,1 GB, roda como root, ignora SIGTERM e refaz o build inteiro quando você muda uma
linha de código.
Cada uma dessas quatro coisas tem uma correção simples e um motivo que vale entender.
Ordem das instruções e cache
Seção intitulada “Ordem das instruções e cache”Cada instrução do Dockerfile gera uma camada. O build reaproveita uma camada se a instrução e todas as anteriores não mudaram. Uma instrução invalidada invalida tudo abaixo dela.
Isso torna a ordem a decisão de desempenho mais importante do arquivo.
# RUIM: qualquer mudança em qualquer arquivo refaz a instalação de dependências.COPY . .RUN npm ci# BOM: a instalação só refaz quando os manifests mudam.COPY package.json package-lock.json ./RUN npm ciCOPY . .A regra geral: o que muda pouco vem primeiro; o que muda a cada commit vem por último. Na prática isso significa quase sempre: manifests de dependência → instalação → código-fonte.
Um .dockerignore também não é opcional. Sem ele, node_modules e .git locais entram
no contexto de build, deixando tudo mais lento e invalidando cache por mudanças
irrelevantes:
.gitnode_modulesdist*.log.env*Dockerfile.dockerignoreMulti-stage build
Seção intitulada “Multi-stage build”A ferramenta de build não precisa existir na imagem final. Compilador, cabeçalhos de desenvolvimento, dependências de teste — nada disso serve em produção, e tudo isso aumenta a superfície de ataque.
FROM node:22-bookworm-slim AS buildWORKDIR /appCOPY package.json package-lock.json ./RUN npm ci # inclui devDependencies, necessárias para o buildCOPY . .RUN npm run build
FROM node:22-bookworm-slim AS depsWORKDIR /appCOPY package.json package-lock.json ./RUN npm ci --omit=dev # só o que roda em produção
FROM gcr.io/distroless/nodejs22-debian12:nonrootWORKDIR /appCOPY --from=deps /app/node_modules ./node_modulesCOPY --from=build /app/dist ./distUSER nonrootCMD ["dist/server.js"]Os estágios build e deps são descartados: só o que foi copiado explicitamente entra
na imagem final. Repare que eles rodam em paralelo no BuildKit, porque não dependem um
do outro.
O template completo, comentado, está em templates.
Escolhendo a imagem base
Seção intitulada “Escolhendo a imagem base”| Base | Tamanho típico | Tem shell? | Quando usar |
|---|---|---|---|
debian:bookworm |
~120 MB | Sim | Precisa de muitas bibliotecas de sistema |
*-bookworm-slim |
~30 MB | Sim | Padrão sensato para a maioria dos casos |
alpine |
~7 MB | Sim | Menor, mas usa musl em vez de glibc |
distroless |
~20 MB | Não | Produção, quando você quer superfície mínima |
scratch |
0 | Não | Binário estático (Go, Rust) |
Sobre Alpine. Ela usa musl libc, não glibc. Isso quebra binários pré-compilados e, em Python, invalida as wheels prontas — o build passa a compilar tudo do zero, ficando muito mais lento. Há também casos documentados de diferenças de desempenho em alocação de memória. Alpine é ótima para Go e para ferramentas; pense duas vezes para Python e Node.
Sobre distroless. Sem shell, sem gerenciador de pacotes, sem ls. Isso é a
vantagem: um atacante que consiga execução remota não tem ferramenta nenhuma. E é
também a desvantagem: kubectl exec deixa de funcionar. A resposta é
container efêmero, não
voltar para uma base com shell.
Usuário não-root
Seção intitulada “Usuário não-root”O padrão é root. Isso significa que um escape de container te encontra como root no namespace do host.
RUN groupadd --system --gid 10001 app \ && useradd --system --uid 10001 --gid app --no-create-home --shell /usr/sbin/nologin app
COPY --from=build --chown=app:app /app /appUSER appTrês detalhes que costumam passar:
- UID numérico alto e fixo. Kubernetes com
runAsNonRoot: trueprecisa saber que o UID não é 0; um nome de usuário só funciona se houver/etc/passwd, o que não existe emscratch. --chownno COPY é mais eficiente que umRUN chowndepois — este último duplica todos os arquivos numa nova camada.- Sistema de arquivos somente leitura. Com
readOnlyRootFilesystem: trueno Kubernetes, o container precisa de umemptyDirmontado em/tmp, senão qualquer escrita temporária falha.
Sinais, PID 1 e encerramento gracioso
Seção intitulada “Sinais, PID 1 e encerramento gracioso”Este é o problema mais sutil e o que mais causa erro 502 durante rollout.
O processo definido no CMD roda como PID 1, e o PID 1 no Linux tem tratamento
especial: sinais sem handler explícito são ignorados. Se a sua aplicação não trata
SIGTERM, ela não morre — o Kubernetes espera o terminationGracePeriodSeconds inteiro
e depois manda SIGKILL, matando as requisições em andamento.
A forma exec é obrigatória:
# RUIM: o shell vira PID 1 e não repassa os sinais ao filho.CMD npm start
# BOM: o processo recebe os sinais diretamente.CMD ["node", "dist/server.js"]Note que CMD npm start também é ruim por outro motivo: o npm fica no meio e não
repassa sinais para o Node.
E a aplicação precisa tratar o sinal — parar de aceitar conexões novas, terminar as em andamento, fechar o pool do banco e só então sair:
process.on('SIGTERM', async () => { server.close(async () => { // para de aceitar, drena as conexões abertas await pool.end(); process.exit(0); });});O que realmente reduz a imagem
Seção intitulada “O que realmente reduz a imagem”Em ordem de impacto, medido:
-
Multi-stage — tirar ferramentas de build. Costuma cortar 60% a 80%.
-
Base menor — de
node:22paranode:22-bookworm-slimjá corta ~800 MB. -
Dependências de produção apenas —
--omit=dev,--no-dev,--only=main. -
Limpar cache de pacote na mesma instrução — em camada separada não adianta, porque os arquivos continuam na camada anterior:
RUN apt-get update \&& apt-get install -y --no-install-recommends ca-certificates \&& rm -rf /var/lib/apt/lists/*
O que não ajuda tanto quanto dizem: espremer dezenas de RUN em um só. Isso prejudica
o cache e economiza pouco. Otimize o que pesa, não o que parece bonito.
Para ver onde o peso está:
docker history --no-trunc <imagem> | head -20Healthcheck
Seção intitulada “Healthcheck”O HEALTHCHECK do Dockerfile é útil em Docker e em Compose. Sob Kubernetes ele é
ignorado — quem manda são as probes do Pod. Não confie nele se o destino é um cluster.
Reprodutibilidade
Seção intitulada “Reprodutibilidade”FROM node:22 muda de conteúdo ao longo do tempo. Para builds que precisam ser
auditáveis, fixe o digest:
FROM node:22-bookworm-slim@sha256:0000000000000000000000000000000000000000000000000000000000000000Isso trava também as atualizações de segurança da base — então combine com uma ferramenta que abra pull request quando houver digest novo, como o Renovate. O ganho é que a atualização passa a ser uma mudança revisada e versionada, e não algo que acontece sozinho num build de sexta à noite.
Checklist antes de mandar para produção
Seção intitulada “Checklist antes de mandar para produção”-
.dockerignoreexiste e exclui.git, dependências locais e artefatos - Dependências instaladas antes de copiar o código
- Multi-stage: nenhuma ferramenta de build na imagem final
-
USERcom UID numérico não-zero -
CMDna forma exec, e a aplicação trataSIGTERM - Nenhum segredo em
ARG,ENVou em camada intermediária - Imagem escaneada, e o resultado tratado — não só gerado
Leituras relacionadas
Seção intitulada “Leituras relacionadas”- Templates de Dockerfile — Node, Python e Go, comentados
- Segurança de imagem — scan, SBOM e assinatura
- Registries e distribuição — por que digest e não tag
- Reduzir o tamanho de uma imagem Docker — o guia passo a passo