Docker multi-stage build: imagens menores

    Uma imagem Docker de desenvolvimento com todas as dependências de build pode ter 1 GB+. Com multi-stage build, a imagem final de produção tem apenas o que precisa para executar — geralmente 50-150 MB. Menos bytes = pull mais rápido, boot mais rápido, menos surface de ataque e menos custo de armazenamento no registry.

    Multi-stage build para Node.js

    Separar build de produção em estágios:

    dockerfile
    # Dockerfile — Node.js com TypeScript
    
    # ── Estágio 1: build ──────────────────────────────────────
    FROM node:22-alpine AS builder
    WORKDIR /app
    
    # Instalar dependências (separado do COPY . para aproveitar cache):
    COPY package*.json ./
    RUN npm ci
    
    # Copiar código e compilar TypeScript:
    COPY tsconfig.json ./
    COPY src/ ./src/
    RUN npm run build
    
    # ── Estágio 2: produção ───────────────────────────────────
    FROM node:22-alpine AS production
    WORKDIR /app
    
    # Apenas dependências de produção:
    COPY package*.json ./
    RUN npm ci --omit=dev && npm cache clean --force
    
    # Copiar apenas o build compilado do estágio anterior:
    COPY --from=builder /app/dist ./dist
    
    # Usuário não-root:
    RUN addgroup -S app && adduser -S app -G app
    USER app
    
    EXPOSE 3000
    CMD ["node", "dist/index.js"]
    
    # Comparar tamanho:
    # docker build -t minha-api . && docker images minha-api
    # Sem multi-stage:  ~600 MB
    # Com multi-stage:  ~120 MB

    Multi-stage para Python com Poetry

    Imagem Python mínima para APIs FastAPI/Flask:

    dockerfile
    # Dockerfile — Python com Poetry e FastAPI
    
    # ── Estágio 1: builder ────────────────────────────────────
    FROM python:3.12-slim AS builder
    WORKDIR /app
    
    RUN pip install poetry
    ENV POETRY_VIRTUALENVS_IN_PROJECT=true
    
    COPY pyproject.toml poetry.lock ./
    # Instalar apenas dependências de produção:
    RUN poetry install --only=main --no-root
    
    COPY . .
    
    # ── Estágio 2: produção ───────────────────────────────────
    FROM python:3.12-slim AS production
    WORKDIR /app
    
    # Copiar apenas o virtualenv com dependências instaladas:
    COPY --from=builder /app/.venv /app/.venv
    COPY --from=builder /app/src ./src
    
    # Ativar venv:
    ENV PATH="/app/.venv/bin:$PATH"
    
    # Usuário não-root:
    RUN useradd --create-home app
    USER app
    
    EXPOSE 8000
    CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
    
    # Alternativa com imagem distroless (ainda menor):
    FROM gcr.io/distroless/python3-debian12 AS production
    WORKDIR /app
    COPY --from=builder /app/.venv /app/.venv
    COPY --from=builder /app/src ./src
    ENV PATH="/app/.venv/bin:$PATH"
    CMD ["/app/.venv/bin/uvicorn", "src.main:app", "--host", "0.0.0.0"]

    Cache de layers do Docker para builds rápidos

    Organizar o Dockerfile para aproveitar o cache ao máximo:

    dockerfile
    # Regra: o que muda menos frequentemente vai primeiro
    
    # ❌ Antipadrão — COPY . . antes de instalar deps invalida cache sempre:
    COPY . .
    RUN npm ci          # reexecutado a cada mudança de código!
    
    # ✓ Correto — instalar deps antes de copiar código:
    COPY package*.json ./
    RUN npm ci          # cacheado enquanto package.json não mudar
    
    COPY . .            # só este layer é invalidado ao mudar código
    RUN npm run build
    
    # BuildKit e cache de mount (Docker 23.0+):
    # Cache de npm entre builds (não precisa re-baixar packages):
    RUN --mount=type=cache,target=/root/.npm \
        npm ci --prefer-offline
    
    # Cache de pip entre builds:
    RUN --mount=type=cache,target=/root/.cache/pip \
        pip install -r requirements.txt
    
    # Habilitar BuildKit:
    # DOCKER_BUILDKIT=1 docker build .
    # ou no /etc/docker/daemon.json:
    # { "features": { "buildkit": true } }
    
    # Build com cache exportado para CI/CD:
    docker buildx build \
      --cache-from type=registry,ref=ghcr.io/usuario/api:cache \
      --cache-to   type=registry,ref=ghcr.io/usuario/api:cache,mode=max \
      -t ghcr.io/usuario/api:latest --push .

    Imagens base: alpine vs slim vs distroless

    Escolher a imagem base certa para cada caso:

    bash
    # Comparação de tamanhos base:
    # ubuntu:22.04           → 77 MB  (completo, muitas ferramentas)
    # debian:bookworm-slim   → 74 MB  (slim = sem docs e extras)
    # node:22                → 1.1 GB (node sobre ubuntu)
    # node:22-slim           → 230 MB (node sobre debian-slim)
    # node:22-alpine         → 58 MB  (node sobre Alpine Linux)
    # gcr.io/distroless/nodejs22 → 102 MB (apenas Node.js, sem shell)
    
    # Alpine Linux — mais compacto, usa musl libc:
    # Vantagem: imagens muito menores
    # Desvantagem: alguns pacotes nativos precisam ser recompilados
    # Use quando: Node.js puro, Python sem extensões C complexas
    
    # Slim — Debian sem extras:
    # Vantagem: mais compatível que Alpine, ainda compacto
    # Use quando: libs nativas (sharp, bcrypt, tensorflow)
    
    # Distroless — sem shell, sem package manager:
    # Vantagem: mínima surface de ataque (sem bash = sem exploração de shell)
    # Desvantagem: difícil de debugar (exec -it não tem shell)
    # Use quando: segurança é prioridade máxima
    
    # Adicionar shell temporário para debug em distroless:
    docker run --rm -it --entrypoint sh minha-api:debug  # usar tag com shell para dev
    # docker run -it minha-api:prod (sem shell — correto para prod)

    Verificar e reduzir tamanho de imagens

    Ferramentas para analisar o que engorda as imagens:

    bash
    # Ver tamanho de imagens:
    docker images --format "table {{.Repository}}	{{.Tag}}	{{.Size}}"
    
    # Dive — inspecionar cada layer da imagem:
    docker run --rm -it \
      -v /var/run/docker.sock:/var/run/docker.sock \
      wagoodman/dive minha-api:latest
    
    # Ver o que está em cada layer:
    docker history minha-api:latest
    
    # Limpar imagens e layers não utilizados:
    docker image prune -a          # remove imagens sem tag e intermediárias
    docker system prune -a --volumes  # limpeza total (cuidado!)
    
    # Verificar quais arquivos foram copiados:
    docker run --rm minha-api:latest find / -name "*.ts" 2>/dev/null
    # Se achar .ts: você copiou fonte TypeScript para produção
    
    # .dockerignore — excluir arquivos desnecessários:
    node_modules/
    src/
    *.ts
    !dist/
    .env*
    .git/
    tests/
    *.test.js
    coverage/

    $ runstack deploy --plan starter

    Não quer configurar manualmente?

    Não quer configurar manualmente? Implante o VPS para Docker em menos de 3 minutos com a Runstack. Infraestrutura da OPEN DATACENTER, com servidores no Brasil.

    Perguntas frequentes