A regra do cache, em uma frase
O Docker guarda o resultado de cada instrução. Quando você constrói de novo, ele reaproveita as camadas até a primeira que mudou — e a partir dali refaz tudo, mesmo o que continua idêntico.
Para COPY e ADD a conta inclui o
conteúdo dos arquivos. É por isso que uma linha só no lugar errado
custa minutos em cada build.
O erro que faz o npm install rodar sempre
Este é o campeão absoluto, e cabe em duas linhas:
COPY . . ← copia TODO o código
RUN npm ci ← e só depois instala
Você mudou uma vírgula num arquivo qualquer. O COPY . .
percebe, invalida a camada — e o npm ci, que está abaixo,
refaz do zero. Toda. Santa. Vez.
A inversão resolve, e é a otimização de melhor retorno em Docker:
COPY package*.json ./ ← só o manifesto, que muda pouco
RUN npm ci ← esta camada vira cache de longa duração
COPY . . ← o código depois
Agora mudar código invalida só a última linha. O
npm ci só roda de novo quando uma dependência realmente muda —
que é o comportamento que você esperava desde o começo.
Multi-stage: o que corta mais tamanho
A imagem final não precisa do compilador, do TypeScript, nem das dependências de desenvolvimento. Com multi-stage você constrói num estágio e leva só o resultado para o outro:
FROM node:22-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci # com devDependencies, para compilar
COPY . .
RUN npm run build
FROM node:22-slim
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev # só o que roda em produção
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/index.js"]
O que fica de fora da imagem final: o código-fonte, as ferramentas de build, o cache do npm e as devDependencies. Em projeto Node típico é a diferença entre centenas de megabytes e algumas dezenas.
As três linhas que incham sem você ver
1. apt-get sem limpeza no mesmo RUN
Camada é imutável: apagar arquivo num RUN seguinte
não diminui a imagem, só adiciona uma camada dizendo que o
arquivo sumiu. O peso continua lá. A limpeza precisa acontecer no mesmo
RUN que criou a sujeira:
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
No Alpine o equivalente é o --no-cache, que evita gravar o
índice: apk add --no-cache curl.
2. apt-get update separado do install
Se o update está numa camada e o install em
outra, o Docker pode reaproveitar um índice velho e instalar pacote que não
existe mais — o famoso 404 Not Found que aparece do nada meses
depois. Os dois no mesmo RUN, sempre.
3. FROM sem versão fixa
node:latest parece cômodo e é o oposto: a imagem muda embaixo
de você, e o build que passou ontem quebra hoje sem ninguém ter mexido em
nada. Fixe a versão — e prefira -slim ou -alpine,
que já cortam bastante.
O .dockerignore que quase ninguém escreve
Sem ele, o COPY . . leva junto o node_modules da
sua máquina, a pasta .git inteira e todo arquivo local de
ambiente. Isso engorda a imagem, atrasa o build e — pior — pode
vazar segredo para dentro dela.
node_modules
.git
.env
.env.*
dist
coverage
*.log
ARG ou ENV fica na imagem.
Quem tiver a imagem lê com docker history, mesmo que a linha
seguinte apague a variável. Para segredo em build, use
--mount=type=secret; para execução, passe na hora de subir o
container.
Rodar como root é o padrão — e não devia ser
Sem USER, o processo dentro do container roda como root. Se
alguém escapar da aplicação, começa com o poder máximo. As imagens oficiais
do Node já trazem um usuário pronto:
USER node
CMD ["node", "dist/index.js"]
Se o container sobe mas o proxy não alcança, o problema costuma ser endereço: está no diagnóstico de Nginx 502 e no artigo de proxy reverso.