Escrever um Dockerfile eficiente é essencial para otimizar o processo de build, reduzir o tamanho da imagem final e melhorar a segurança das suas aplicações containerizadas. Nesta aula, vamos explorar as boas práticas mais importantes que todo profissional de DevOps deve dominar ao criar imagens Docker.

Essas práticas não apenas tornam o build mais rápido e confiável, mas também facilitam a manutenção e o deploy em produção. Vamos abordar desde a escolha de uma base leve até técnicas avançadas como multi-stage builds, garantindo que você consiga aplicar esses conceitos no seu dia a dia.

Imagens pequenas

O tamanho da imagem Docker impacta diretamente no tempo de pull, no consumo de armazenamento e na superfície de ataque de segurança. Imagens menores são mais rápidas de transferir e ocupam menos espaço no registry e nos hosts. Para reduzir o tamanho, comece escolhendo uma imagem base enxuta, como alpine, slim ou versões baseadas em distroless.

Além disso, evite instalar pacotes desnecessários e limpe arquivos temporários no mesmo RUN em que são criados. Combine comandos com && e remova caches de gerenciadores de pacotes, como apt-get clean ou rm -rf /var/lib/apt/lists/*. Isso reduz drasticamente o tamanho final da imagem.

FROM alpine:3.19
RUN apk add --no-cache curl \
    && rm -rf /var/cache/apk/*

Outra prática é usar apenas as dependências de produção, evitando incluir ferramentas de desenvolvimento ou testes na imagem final. Isso pode ser alcançado com multi-stage builds, como veremos a seguir.

Multi-stage builds

Multi-stage builds permitem usar múltiplas instruções FROM no mesmo Dockerfile, copiando apenas os artefatos necessários para a imagem final. Essa técnica é ideal para linguagens compiladas, como Go, Java ou C, e também para reduzir o tamanho de imagens com dependências de build.

No primeiro estágio, você compila o código e instala todas as ferramentas necessárias. No segundo, você copia apenas o binário gerado ou os arquivos de runtime para uma imagem base leve. Isso evita que ferramentas de build e fontes de código fiquem na imagem final.

# Estágio de build
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN go build -o meuapp .

# Imagem final
FROM alpine:3.19
COPY --from=builder /app/meuapp /usr/local/bin/meuapp
CMD ["meuapp"]

Além de reduzir o tamanho, multi-stage builds melhoram a segurança, pois a imagem final contém apenas o necessário para executar a aplicação, sem ferramentas de compilação ou código-fonte que poderiam ser explorados.

Ordem de camadas

Cada instrução no Dockerfile cria uma camada na imagem. O Docker armazena essas camadas em cache e as reutiliza em builds futuros se os arquivos envolvidos não mudaram. Portanto, a ordem das instruções é crucial para aproveitar o cache e acelerar o processo de build.

Coloque instruções que mudam com menos frequência no início do Dockerfile, como a instalação de dependências do sistema. Depois, copie o código-fonte e execute comandos que mudam com frequência, como RUN npm install ou COPY . .. Isso garante que, quando o código mudar, o Docker possa reutilizar as camadas anteriores e apenas reconstruir as últimas.

FROM node:20-alpine
WORKDIR /app
# Dependências primeiro
COPY package*.json ./
RUN npm install
# Código depois
COPY . .
CMD ["npm", "start"]

Lembre-se de que o cache é invalidado quando qualquer arquivo usado em uma instrução muda. Portanto, se você copiar todos os arquivos de uma vez, qualquer alteração invalidará o cache a partir daquele ponto. Separar a cópia de arquivos de dependências do código-fonte é uma prática recomendada.

.dockerignore

O arquivo .dockerignore define quais arquivos e diretórios devem ser excluídos do contexto de build. Isso reduz o tamanho do contexto enviado ao daemon do Docker, acelerando o build e evitando que arquivos sensíveis, como chaves privadas ou diretórios node_modules, sejam copiados para a imagem.

O .dockerignore funciona de forma semelhante ao .gitignore e deve ser colocado na raiz do contexto de build. Você pode usar padrões como *, ** e ! para excluir ou incluir arquivos específicos.

# .dockerignore
git
node_modules
npm-debug.log
Dockerfile*
.dockerignore
.git
*.md
.env

Além de melhorar a performance do build, o .dockerignore é uma medida de segurança, pois impede que informações confidenciais sejam enviadas ao daemon e potencialmente incluídas em camadas da imagem, mesmo que não sejam usadas na imagem final.

Boas práticas adicionais e observações finais

Além das práticas principais, considere usar usuários não-root na imagem final para reduzir riscos de segurança. Use a instrução USER para definir um usuário sem privilégios. Também é recomendado fixar versões das imagens base, evitando tags como latest, para garantir builds reprodutíveis.

Outra dica é usar a instrução HEALTHCHECK para que o Docker possa monitorar a saúde do container, e definir metadados com LABEL para organização. Lembre-se de sempre testar suas imagens e analisar vulnerabilidades com ferramentas como docker scan ou Trivy.

Dominar essas boas práticas não só melhora a eficiência do seu pipeline, mas também contribui para uma infraestrutura mais segura e escalável. Aplique esses conceitos em seus projetos e observe a diferença na qualidade das suas imagens.

Referências

Exercícios

  1. Explique por que é importante usar imagens base pequenas e cite duas estratégias para reduzir o tamanho da imagem final.
  2. ✓ Resposta: Imagens menores reduzem o tempo de pull, o uso de armazenamento e a superfície de ataque. Estratégias: usar base alpine/slim e limpar caches de pacotes no mesmo RUN, além de evitar instalar dependências desnecessárias.
  3. Descreva como um multi-stage build funciona e quais são os benefícios em relação a um Dockerfile de estágio único.
  4. ✓ Resposta: Multi-stage build usa múltiplas instruções FROM, permitindo compilar em um estágio e copiar apenas artefatos para o estágio final. Benefícios: imagem final menor, sem ferramentas de build e código-fonte, maior segurança e builds mais rápidos.
  5. Por que a ordem das camadas no Dockerfile afeta o desempenho do build? Dê um exemplo de ordem eficiente.
  6. ✓ Resposta: O Docker usa cache de camadas; instruções que mudam menos devem vir antes para aproveitar o cache. Exemplo: copiar package.json e rodar npm install antes de copiar o código, para que mudanças no código não invalidem o cache de dependências.
  7. O que é .dockerignore e por que ele é importante para a segurança e performance do build?
  8. ✓ Resposta: .dockerignore exclui arquivos do contexto de build, reduzindo o tamanho enviado ao daemon e evitando que dados sensíveis sejam incluídos na imagem. Isso acelera o build e melhora a segurança.
  9. Escreva um Dockerfile multi-stage para compilar um programa em Go e gerar uma imagem final mínima usando alpine. Inclua comentários explicativos.
  10. ✓ Resposta:
    # Estágio de build
    FROM golang:1.22 AS builder
    WORKDIR /app
    COPY go.mod go.sum ./
    RUN go mod download
    COPY . .
    RUN CGO_ENABLED=0 go build -o meuapp .
    
    # Imagem final
    FROM alpine:3.19
    RUN adduser -D appuser
    USER appuser
    COPY --from=builder /app/meuapp /usr/local/bin/meuapp
    CMD ["meuapp"]