Reduza o Tamanho da Imagem Docker: Um Guia Prático para Builds Mais Rápidos
Torne as imagens Docker menores e os builds mais rápidos com builds multi-estágio, Dockerfiles amigáveis ao cache e contextos de build limpos.
Reduza o Tamanho da Imagem Docker: Um Guia Prático para Builds Mais Rápidos
Reduzir o tamanho da imagem Docker não é sobre perseguir o menor número em docker image ls. O objetivo útil é um build que seja rápido, reproduzível, fácil de corrigir e pequeno o suficiente para não atrasar CI, implantações ou desenvolvimento local.
Uma imagem inchada geralmente vem de alguns hábitos comuns: copiar o repositório inteiro muito cedo, instalar ferramentas de build na imagem de runtime, deixar caches de gerenciadores de pacotes para trás, usar tags latest e enviar um contexto de build enorme para o Docker. Corrija esses hábitos e a maioria das equipes verá builds mais limpos sem fazer nada exótico.
Comece medindo a imagem atual:
docker image ls my-app
docker history --no-trunc my-app:latest
docker history mostra quais instruções do Dockerfile adicionaram tamanho. Se uma etapa RUN adicionar uma grande quantidade, inspecione essa etapa antes de mudar todo o resto. Para uma análise mais aprofundada, ferramentas como dive podem mostrar arquivos adicionados e removidos em cada camada, mas os comandos integrados são suficientes para a primeira passagem.
Escolha uma imagem base pela compatibilidade primeiro, tamanho depois
A escolha da imagem base importa, mas a imagem menor nem sempre é a melhor. Alpine é pequena porque usa musl libc e um userspace mínimo. Isso funciona bem para muitas imagens Go, Node, Python e utilitárias, mas pode criar atrito com extensões nativas, binários pré-compilados, comportamento de DNS ou pacotes que esperam glibc.
Uma ordem prática é:
- Use uma imagem oficial para seu runtime.
- Prefira uma tag versionada como
python:3.12-slimem vez depython:latest. - Tente
slimantes de pular para Alpine se seu aplicativo tiver dependências nativas. - Use distroless ou
scratchquando você já souber exatamente o que o runtime precisa.
Por exemplo, um serviço Python com psycopg, bibliotecas de imagem ou pacotes de criptografia pode ser mais fácil de manter em python:3.12-slim do que python:3.12-alpine. A imagem Alpine pode ser menor no início, mas se você adicionar compiladores e pacotes de compatibilidade para fazer as dependências serem construídas, o resultado final pode não ser mais simples ou muito menor.
Para serviços Go, scratch ou distroless podem ser excelentes porque um binário compilado estaticamente pode precisar de muito pouco em runtime. Mesmo assim, lembre-se de certificados TLS, dados de fuso horário e usuários não-root. Uma imagem minúscula que não pode fazer requisições HTTPS não é uma imagem de produção útil.
Use builds multi-estágio para manter ferramentas de build fora da produção
Builds multi-estágio são a maneira mais confiável de reduzir o tamanho da imagem Docker para aplicativos compilados e builds de frontend. O estágio de build pode ser grande. Ele pode conter compiladores, gerenciadores de pacotes, ferramentas de teste e arquivos fonte. O estágio final deve conter apenas o que o aplicativo precisa para executar.
Um exemplo Go:
FROM golang:1.22 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /out/server ./cmd/server
FROM gcr.io/distroless/static-debian12
COPY --from=builder /out/server /server
USER nonroot:nonroot
ENTRYPOINT ["/server"]
A parte importante não é a imagem base exata. A parte importante é o limite: código fonte, cache de módulos e compilador ficam em builder; a imagem final recebe o binário.
Um aplicativo Node tem uma forma semelhante, mas você precisa separar dependências de desenvolvimento das dependências de produção:
FROM node:22-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM deps AS build
COPY . .
RUN npm run build
FROM node:22-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]
Para assets de frontend servidos por nginx, a imagem final não precisa de Node:
FROM node:22-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:1.27-alpine
COPY --from=build /app/dist /usr/share/nginx/html
Esse padrão remove node_modules, a árvore de código fonte e as ferramentas de build da imagem de runtime.
Faça o cache do Docker funcionar para você
A invalidação de cache do Docker é simples, mas implacável. Se uma instrução muda, o Docker reconstrói essa camada e todas as camadas depois dela. Se você copiar o projeto inteiro antes de instalar as dependências, cada edição de código pode forçar uma reinstalação de dependências.
Isso é lento:
FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "app.py"]
Isso geralmente é mais rápido:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]
A camada de dependências só é reconstruída quando requirements.txt muda. Mudanças no código fonte só reconstroem a camada posterior COPY . ..
A mesma regra se aplica a módulos Maven, Gradle, npm, pnpm, Cargo e Go. Copie os manifestos de dependência primeiro, instale ou baixe as dependências, depois copie o resto do código fonte.
Mantenha o contexto de build pequeno com .dockerignore
O Docker envia o contexto de build para o daemon antes de começar a construir. Se seu contexto incluir .git, node_modules local, relatórios de cobertura, vídeos, capturas de tela, bancos de dados de teste e artefatos de build, seu build será mais lento antes mesmo do Dockerfile executar.
Um .dockerignore básico pode ser assim:
.git
.gitignore
.env
.env.*
node_modules
coverage
dist
build
target
.pytest_cache
__pycache__
*.log
.DS_Store
Tenha cuidado com ignores amplos. Se você ignorar dist mas seu Dockerfile espera copiar um dist pré-construído, o build falhará. Se você ignorar .env, isso geralmente é bom para segredos, mas seu build não deve depender disso. O build deve receber valores necessários através de argumentos de build explícitos ou configuração de CI.
Você pode ver o tamanho do contexto no início da saída clássica do build Docker, e a saída do BuildKit também torna grandes transferências de contexto visíveis. Se o primeiro passo demorar muito antes de qualquer instrução do Dockerfile ser executada, verifique .dockerignore.
Limpe os caches do gerenciador de pacotes na mesma camada
Um erro frequente é instalar pacotes em uma camada e excluir caches em uma camada posterior. Os arquivos desaparecem da visão final do sistema de arquivos, mas ainda podem existir no histórico da imagem.
Para imagens Debian e Ubuntu:
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl && rm -rf /var/lib/apt/lists/*
Para Alpine:
RUN apk add --no-cache ca-certificates curl
Para Python:
RUN pip install --no-cache-dir -r requirements.txt
Para npm:
RUN npm ci --omit=dev && npm cache clean --force
Não use comandos de limpeza sem pensar. Use a limpeza que corresponde ao gerenciador de pacotes e à imagem base. Evite também apt-get upgrade na maioria dos Dockerfiles de aplicação. Isso torna os builds menos previsíveis e pode trazer mais mudanças do que você pretendia. Corrija reconstruindo regularmente a partir de imagens base mantidas e fixando a família de imagens que você espera.
Prefira COPY a menos que você precise de ADD
COPY copia arquivos do contexto de build. ADD tem comportamento extra: pode descompactar arquivos tar locais e buscar URLs remotas em alguns casos. Esse comportamento extra pode surpreender mantenedores e tornar o comportamento do cache mais difícil de raciocinar.
Use COPY para arquivos de aplicação normais:
COPY ./src ./src
Use ADD apenas quando você intencionalmente quiser seu comportamento de extração de arquivo. Para downloads remotos, prefira curl ou wget em uma etapa RUN onde você pode verificar checksums e falhar claramente.
Reduza o que você instala, não apenas o que você exclui
O melhor cache de limpeza é não instalar arquivos desnecessários em primeiro lugar. Use --no-install-recommends com apt-get. Evite instalar editores, shells, gerenciadores de pacotes e ferramentas de depuração em imagens de produção, a menos que você tenha um motivo.
Dito isso, não remova todas as ferramentas se isso dificultar as operações. Uma imagem de produção pode ser mínima enquanto ainda observável. Para algumas equipes, uma imagem slim com um shell é um trade-off melhor do que uma imagem distroless porque a depuração de plantão é mais simples. Para outras equipes com forte logging, tracing e contêineres de depuração efêmeros, distroless é uma boa escolha.
A resposta honesta é específica da carga de trabalho. Imagens menores geralmente puxam mais rápido e expõem menos pacotes para corrigir, mas a capacidade de manutenção ainda importa.
Use montagens de cache do BuildKit para downloads de dependências
O BuildKit pode manter caches de pacotes fora da imagem final enquanto os reutiliza entre builds. Isso é diferente de deixar caches dentro da imagem.
Para npm:
# syntax=docker/dockerfile:1.7
FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
Para apt, montagens de cache podem ajudar em ambientes de CI onde builds repetidos acontecem no mesmo builder, embora muitos sistemas de CI usem workers novos a menos que você configure cache persistente. Os recursos do BuildKit são poderosos, mas mantenha o Dockerfile legível para a equipe que precisa mantê-lo.
Verifique o conteúdo da imagem final
Após as alterações, prove o que melhorou:
docker build -t my-app:optimized .
docker image ls my-app
docker history my-app:optimized
Em seguida, execute a imagem da mesma forma que a produção executa. Uma imagem menor que falha porque não tem certificados, dados de localidade, arquivos de fuso horário ou um diretório gravável não é uma melhoria.
Testes de fumaça úteis incluem:
docker run --rm my-app:optimized --version
docker run --rm -p 8080:8080 my-app:optimized
curl -f http://localhost:8080/health
Se o aplicativo escreve arquivos, teste esse caminho. Se ele chama serviços HTTPS, teste TLS. Se ele executa como não-root, teste permissões.
Uma ordem prática de otimização
Quando reviso uma imagem Docker superdimensionada, uso esta ordem:
- Adicione ou corrija
.dockerignore. - Reordene o Dockerfile para que a instalação de dependências possa ser armazenada em cache.
- Introduza builds multi-estágio.
- Mude de imagens base completas para
slim, Alpine, distroless ouscratchonde compatível. - Remova caches de gerenciadores de pacotes e dependências de desenvolvimento na mesma camada em que são criados.
- Inspecione com
docker historyoudivee repita apenas onde a evidência aponta.
Esta ordem evita esperteza prematura. Também mantém o Dockerfile compreensível, o que importa mais do que extrair os últimos megabytes.