Cloud e plataforma

Docker BuildKit com Multi-Stage: Reduzindo Tamanho de Imagem em Produção

Como BuildKit e multi-stage builds transformam containers inchados em imagens de produção eficientes, reduzindo custo de armazenamento e acelerando deploys.

17/03/20266 min de leituraCloud
Docker BuildKit com Multi-Stage: Reduzindo Tamanho de Imagem em Produção

Resumo executivo

Como BuildKit e multi-stage builds transformam containers inchados em imagens de produção eficientes, reduzindo custo de armazenamento e acelerando deploys.

Ultima atualizacao: 17/03/2026

O problema do container inchado

Um container de produção Node.js que deveria pesar 50MB pode facilmente acabar com 500MB+, 1GB ou mais. Isso acontece quando dependências de build, ferramentas de desenvolvimento, caches e artefatos temporários são incluídos na imagem final.

Imagens grandes significam:

  • Deploy mais lento: 500MB demora 10x mais para fazer pull do que 50MB
  • Custo mais alto: armazenamento em registry custa dinheiro
  • Superfície de ataque maior: mais pacotes instalados = mais vulnerabilidades
  • Cache menos eficiente: pequenas mudanças invalidam grandes camadas

Multi-stage builds com BuildKit resolvem isso permitindo que você use múltiplos Dockerfiles em um único build, copiando apenas artefatos necessários para a imagem final.

BuildKit: O novo motor de build

BuildKit é o próximo motor de build do Docker, habilitado por padrão desde Docker 18.09. Ele oferece:

  • Execução paralela de stages: stages independentes rodam simultaneamente
  • Caching inteligente: cache granular por instruções, não por camada inteira
  • Build secrets: passar secrets sem incluí-los na imagem final
  • Inline caching: cache remoto para builds de CI/CD

Habilitar BuildKit:

bash# Linux/Mac
export DOCKER_BUILDKIT=1

# Windows
$env:DOCKER_BUILDKIT=1

# Ou configure globalmente em ~/.docker/config.json
{
  "features": {
    "buildkit": true
  }
}

Multi-stage: O padrão fundamental

Exemplo básico: Node.js

dockerfile# Stage 1: Build
FROM node:18-alpine AS builder

WORKDIR /app

# Copia package files primeiro para cache de dependências
COPY package*.json ./

# Instala todas as dependências
RUN npm ci --only=production=false

# Copia código fonte
COPY . .

# Build da aplicação
RUN npm run build

# Stage 2: Produção
FROM node:18-alpine AS production

WORKDIR /app

# Cria usuário não-root para segurança
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

# Copia apenas arquivos de produção do stage anterior
COPY --from=builder --chown=nodejs:nodejs /app/package*.json ./
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist

# Troca para usuário não-root
USER nodejs

EXPOSE 3000

CMD ["node", "dist/index.js"]

Resultado: imagem final sem devDependencies, sem código fonte TypeScript, sem ferramentas de build. Redução típica de 60-80%.

Exemplo: Go

dockerfile# Stage 1: Build
FROM golang:1.21-alpine AS builder

WORKDIR /app

# Instala dependências de build
RUN apk add --no-cache git ca-certificates

# Baixa dependências (cache separado de código)
COPY go.mod go.sum ./
RUN go mod download

# Copia código e build
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-w -s" -o main .

# Stage 2: Produção
FROM alpine:3.19

# Instala certificados SSL
RUN apk --no-cache add ca-certificates

WORKDIR /root/

# Copia apenas o binário compilado
COPY --from=builder /app/main .

# Usa ca-certificates do Alpine
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

EXPOSE 8080

CMD ["./main"]

Resultado: binário Go estático com ~10-15MB, sem Go SDK, sem ferramentas.

Padrões avançados de otimização

Cache de dependências isolado

dockerfile# Isola instalação de dependências para cache granular
FROM node:18-alpine AS deps

WORKDIR /app

COPY package*.json ./
RUN npm ci

# Stage de build
FROM node:18-alpine AS builder

WORKDIR /app

# Copia dependências do stage deps
COPY --from=deps /app/node_modules ./node_modules
COPY . .

RUN npm run build

# Stage de produção
FROM node:18-alpine AS production

WORKDIR /app

# Copia apenas dependências de produção
COPY --from=deps /app/package*.json ./
RUN npm prune --production

COPY --from=builder /app/dist ./dist

CMD ["node", "dist/index.js"]

Benefício: mudanças em código não invalidam cache de npm ci, acelerando builds subsequentes.

Arquitetura de multi-repositório

dockerfile# Stage de frontend
FROM node:18-alpine AS frontend-builder

WORKDIR /app
COPY frontend/package*.json ./frontend/
RUN cd frontend && npm ci

COPY frontend/ ./frontend/
RUN cd frontend && npm run build

# Stage de backend
FROM node:18-alpine AS backend-builder

WORKDIR /app
COPY backend/package*.json ./backend/
RUN cd backend && npm ci

COPY backend/ ./backend/
RUN cd backend && npm run build

# Stage de produção
FROM node:18-alpine

WORKDIR /app

# Copia artefatos de ambos os repositórios
COPY --from=frontend-builder /app/frontend/dist ./public
COPY --from=backend-builder /app/backend/dist ./dist
COPY --from=backend-builder /app/backend/package*.json ./
RUN npm prune --production

CMD ["node", "dist/index.js"]

Técnicas de redução de tamanho

Imagens Alpine vs Slim vs Debian

dockerfile# Debian (maior)
FROM node:18
# Tamanho: ~900MB

# Debian Slim (balanceado)
FROM node:18-slim
# Tamanho: ~250MB

# Alpine (menor)
FROM node:18-alpine
# Tamanho: ~180MB

Trade-off: Alpine usa musl libc em vez de glibc. A maioria das aplicações funciona, mas alguns pacotes nativos podem ter problemas. Use Alpine quando possível; Slim quando precisar de compatibilidade glibc.

Multi-stage com distroless

dockerfile# Build stage
FROM golang:1.21 AS builder

WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-w -s" -o main .

# Produção com distroless (Google)
FROM gcr.io/distroless/base-debian12

COPY --from=builder /app/main .

EXPOSE 8080

CMD ["./main"]

Benefício: distroless não tem shell, gerenciador de pacotes ou outras ferramentas. Superfície de ataque mínima. Tamanho: ~5-10MB.

Eliminação de arquivos desnecessários

dockerfile# No stage final, remove caches, logs e arquivos temporários
RUN npm cache clean --force && \
    rm -rf /tmp/* /var/tmp/* && \
    rm -rf /root/.npm /root/.cache && \
    find /app -name "*.log" -delete && \
    find /app -type d -name ".git" -prune -exec rm -rf {} +

# Para Alpine
RUN rm -rf /var/cache/apk/*

BuildKit Features avançadas

Inline caching para CI/CD

bash# Primeiro build: cria cache local
docker build \
  --build-arg BUILDKIT_INLINE_CACHE=1 \
  --cache-from myregistry.io/myapp:buildcache \
  -t myapp:latest \
  .

# Push do cache
docker push myregistry.io/myapp:buildcache

# Builds subsequentes: usa cache remoto
docker build \
  --build-arg BUILDKIT_INLINE_CACHE=1 \
  --cache-from myregistry.io/myapp:buildcache \
  --cache-to myregistry.io/myapp:buildcache \
  -t myapp:latest \
  .

Benefício: builds de CI/CD podem aproveitar cache de builds anteriores, reduzindo tempo de build de minutos para segundos.

Build secrets sem vazamento

dockerfile# Sintaxe de secret do BuildKit
RUN --mount=type=secret,id=aws,dst=/root/.aws/credentials \
    npm ci

# Para builds que precisam de access tokens privados
RUN --mount=type=secret,id=github_token,dst=/tmp/token \
    npm config set //registry.npmjs.org/:_authToken $(cat /tmp/token)
bash# Passa secret via CLI (não fica na imagem)
docker build \
  --secret id=aws,src=$HOME/.aws/credentials \
  --secret id=github_token,src=$GITHUB_TOKEN \
  -t myapp:latest \
  .

Benefício: credenciais são acessíveis durante build mas nunca são incluídas na imagem final.

Build mounts para desempenho

dockerfile# Usa bind mount do host ao invés de COPY para desenvolvimento
FROM node:18-alpine AS development

WORKDIR /app

# Mount do código fonte (read-only)
RUN --mount=type=bind,source=.,target=/app,readonly \
    npm run build

# Para watch mode em desenvolvimento
RUN --mount=type=bind,source=.,target=/app \
    npm run dev

Benefício: durante desenvolvimento, código fonte é montado diretamente, evitando cópias e cache do Docker.

Boas práticas de segurança

Usuário não-root

dockerfileFROM node:18-alpine

WORKDIR /app

# Cria usuário e grupo
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

# Copia arquivos com ownership correto
COPY --chown=nodejs:nodejs package*.json ./
RUN npm ci --only=production && \
    npm cache clean --force

COPY --chown=nodejs:nodejs . .

# Troca para usuário não-root
USER nodejs

EXPOSE 3000

CMD ["node", "index.js"]

Scan de vulnerabilidades no build

bash# Usa Trivy durante build
FROM node:18-alpine AS security-scan

# Instala Trivy
RUN apk add --no-cache curl

# Scan da imagem final
COPY --from=production /app /app
RUN apk add --no-cache trivy && \
    trivy image --exit-code 1 --severity HIGH,CRITICAL myapp:latest

Multi-stage com security scan

dockerfile# Stage de produção
FROM node:18-alpine AS production
# ... configuração de produção ...

# Stage de segurança
FROM aquasec/trivy:latest AS security-scan

COPY --from=production / /app
RUN trivy image --exit-code 0 --severity HIGH,CRITICAL /app

# Imagem final passa por scan
FROM production

Medindo impacto de otimização

Métricas antes/depois

bash# Ver tamanho de imagem
docker images myapp:latest --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"

# Histórico de camadas
docker history myapp:latest

# Análise de conteúdo de camada
docker diff <container_id>

# Tamanho de artefatos
docker run --rm myapp:latest du -sh /app/node_modules
docker run --rm myapp:latest du -sh /app/dist

Benchmarks típicos

AplicaçãoAntes (monolítico)Depois (multi-stage)Redução
Node.js + TypeScript850MB180MB79%
Go application900MB12MB99%
Python (FastAPI)1.2GB350MB71%
React + Nginx700MB25MB96%

Anti-padrões comuns

Anti-padrão 1: COPY no final do Dockerfile

dockerfile# RUIM: Copia tudo primeiro, invalida todo cache quando qualquer arquivo muda
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN npm ci

Solução:

dockerfile# BOM: Isola dependências para cache
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .

Anti-padrão 2: Não remover cache de npm

dockerfile# RUIM: npm cache permanece na imagem
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN npm install

Solução:

dockerfile# BOM: Remove cache após install
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && \
    npm cache clean --force

Anti-padrão 3: DevDependencies em produção

dockerfile# RUIM: Instala tudo, incluindo devDependencies
FROM node:18-alpine
COPY package*.json ./
RUN npm install

Solução:

dockerfile# BOM: Instala apenas dependências de produção
FROM node:18-alpine
COPY package*.json ./
RUN npm ci --only=production

Integração com CI/CD

GitHub Actions

yamlname: Build and Push

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Cache Docker layers
        uses: actions/cache@v3
        with:
          path: /tmp/.buildx-cache
          key: ${{ runner.os }}-buildx-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-buildx-

      - name: Login to registry
        uses: docker/login-action@v3
        with:
          registry: ${{ secrets.REGISTRY_URL }}
          username: ${{ secrets.REGISTRY_USERNAME }}
          password: ${{ secrets.REGISTRY_PASSWORD }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ secrets.REGISTRY_URL }}/myapp:latest
          cache-from: type=local,src=/tmp/.buildx-cache
          cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
          build-args: |
            BUILDKIT_INLINE_CACHE=1

Conclusão

Multi-stage builds com BuildKit transformam containers inchados em imagens de produção eficientes. A técnica é simples em conceito — separar ambiente de build de ambiente de runtime — mas exige disciplina para implementar consistentemente.

O impacto é mensurável: reduções de 60-99% no tamanho de imagem, deploys significativamente mais rápidos, menor custo de armazenamento e superfície de ataque reduzida. Para empresas executando centenas ou milhares de containers, essas economias se acumulam em números significativos.

BuildKit adiciona features como inline caching e build secrets que tornam o processo mais seguro e eficiente, especialmente em ambientes de CI/CD. Combinar multi-stage builds com imagens Alpine ou distroless cria imagens pequenas, seguras e rápidas de fazer deploy.

A melhor parte: a técnica é linguagem-agnóstica. Node.js, Go, Python, Java, Ruby — todas se beneficiam de isolar build de produção. O próximo passo não é apenas implementar multi-stage builds, mas estabelecer padrões organizacionais: templates de Dockerfile, linhas de base de segurança e métricas de otimização contínua.


Seus containers estão inchados e lentos de fazer deploy? Fale com especialistas DevOps da Imperialis para otimizar sua estratégia de containers, reduzir custos e acelerar deploys.

Fontes

Leituras relacionadas