Dicas para escrever Dockerfiles mais eficientes

1. Otimização da camada de base

A escolha da imagem base é o primeiro e mais impactante passo para escrever Dockerfiles eficientes. Imagens oficiais como Alpine e Slim reduzem drasticamente o tamanho final e a superfície de ataque.

# Ruim: imagem pesada e tag genérica
FROM node:latest

# Bom: imagem leve com tag específica
FROM node:20-alpine

Por que Alpine? A imagem base Alpine Linux tem cerca de 5 MB, enquanto versões completas do Ubuntu ou Debian ultrapassam 100 MB. Para aplicações Node.js, Python ou Go, essa diferença é significativa.

# Exemplo prático: Python com imagem slim
FROM python:3.12-slim-bookworm

RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    && rm -rf /var/lib/apt/lists/*

Sempre especifique tags exatas como 20-alpine ou 3.12-slim em vez de latest. A tag latest muda com o tempo e quebra builds reproduzíveis.

2. Ordenação estratégica de instruções

O Docker constrói imagens em camadas e cada instrução RUN, COPY ou ADD cria uma nova camada. O cache de camadas é invalidado quando uma instrução muda. Portanto, ordene as instruções das menos voláteis para as mais voláteis.

# Eficiente: instala dependências antes de copiar código
FROM node:20-alpine

WORKDIR /app

# Primeiro: dependências (mudam raramente)
COPY package.json package-lock.json ./
RUN npm ci --only=production

# Depois: código-fonte (muda frequentemente)
COPY . .

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

Nesse exemplo, se apenas o código-fonte mudar, o Docker reutiliza as camadas de instalação de dependências do cache, acelerando o rebuild em segundos.

3. Redução do tamanho da imagem

Combine comandos RUN em um único layer e limpe artefatos temporários imediatamente.

# Ineficiente: múltiplos layers e sem limpeza
RUN apt-get update
RUN apt-get install -y curl git
RUN apt-get clean

# Eficiente: um único layer com limpeza
RUN apt-get update && apt-get install -y \
    curl \
    git \
    && rm -rf /var/lib/apt/lists/*

O .dockerignore é essencial para evitar que arquivos desnecessários entrem no contexto de build:

# .dockerignore
node_modules
.git
*.md
.gitignore
.env
dist
# Exemplo de Dockerfile com .dockerignore eficiente
FROM golang:1.22-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 go build -o /app/server

FROM alpine:3.19
COPY --from=builder /app/server /server
CMD ["/server"]

4. Gerenciamento de dependências

Instale apenas pacotes essenciais para produção e separe dependências de build das de runtime.

# Python: separação de dependências
FROM python:3.12-slim

WORKDIR /app

# Apenas dependências de produção
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .
CMD ["python", "app.py"]

Para projetos com lockfiles, copie-os antes do código para aproveitar o cache:

# Node.js com lockfile
FROM node:20-alpine

WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile --production

COPY . .
CMD ["yarn", "start"]

5. Uso de multi-stage builds

Multi-stage builds são a técnica mais poderosa para reduzir o tamanho da imagem final. Crie um estágio de build com todas as ferramentas necessárias e copie apenas os artefatos para o estágio final.

# Exemplo completo com Go
FROM golang:1.22-alpine AS builder

WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 go build -o /bin/app .

# Estágio final: apenas o binário
FROM alpine:3.19
RUN apk add --no-cache ca-certificates
COPY --from=builder /bin/app /app
USER nobody
CMD ["/app"]
# Node.js com multi-stage
FROM node:20-alpine AS builder

WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile

COPY . .
RUN yarn build

FROM node:20-alpine AS production

WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package.json ./

USER node
CMD ["node", "dist/server.js"]

6. Boas práticas de segurança

Execute aplicações com usuário não-root e prefira COPY em vez de ADD.

# Inseguro: execução como root
FROM node:20-alpine
COPY . /app
CMD ["node", "app.js"]

# Seguro: usuário não-root
FROM node:20-alpine
RUN addgroup -g 1001 -S appgroup && \
    adduser -S appuser -u 1001 -G appgroup

WORKDIR /app
COPY --chown=appuser:appgroup . .
USER appuser
CMD ["node", "app.js"]

ADD tem comportamentos imprevisíveis com arquivos compactados e URLs. Use COPY para transferir arquivos locais e RUN curl para downloads.

# Evite ADD para downloads
# ADD https://example.com/file.tar.gz /tmp/

# Prefira COPY + RUN
COPY scripts/download.sh .
RUN ./download.sh && rm download.sh

Escaneie vulnerabilidades regularmente:

# Exemplo de comando para escanear (fora do Dockerfile)
docker scout quickview minha-imagem:tag

7. Dicas avançadas de performance

Use --mount=type=cache para acelerar instalações repetitivas em builds.

# Cache de pacotes npm
FROM node:20-alpine

WORKDIR /app
COPY package.json package-lock.json ./

RUN --mount=type=cache,target=/root/.npm \
    npm ci --only=production

COPY . .
CMD ["node", "server.js"]
# Cache de pacotes apt
FROM ubuntu:22.04

RUN --mount=type=cache,target=/var/cache/apt \
    apt-get update && apt-get install -y \
    curl \
    git \
    && rm -rf /var/lib/apt/lists/*

Adicione HEALTHCHECK para monitoramento eficiente:

FROM node:20-alpine

WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
COPY . .

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:3000/health || exit 1

USER node
CMD ["node", "server.js"]

Conclusão

Escrever Dockerfiles eficientes reduz o tempo de build, o tamanho da imagem e melhora a segurança. Comece com imagens base leves, ordene instruções estrategicamente, use multi-stage builds e aplique boas práticas de segurança. O cache de camadas e o .dockerignore são ferramentas simples que geram ganhos imediatos. Com essas técnicas, seus containers serão mais rápidos, menores e mais seguros.

Referências