Dockerfile: boas práticas
Nesta aula, você aprenderá as boas práticas essenciais para escrever Dockerfiles eficientes e seguros, incluindo a criação de imagens pequenas, o uso de multi-stage builds, a otimização da ordem das camadas e a utilização de .dockerignore. Essas técnicas reduzem o tempo de build, o tamanho da imagem e melhoram a segurança do seu ambiente de contêineres.
Escrever um Dockerfile é mais do que simplesmente empilhar comandos: é uma arte que equilibra eficiência, segurança e manutenibilidade. Nesta aula, vamos explorar as boas práticas que separam um Dockerfile comum de um profissional. Você entenderá por que imagens menores são melhores, como o multi-stage build pode simplificar seu fluxo de trabalho e como a ordem das instruções afeta o cache e o desempenho. Ao final, terá um repertório de técnicas para aplicar imediatamente em seus projetos.
Essas práticas não são apenas estéticas; elas impactam diretamente o tempo de deploy, a superfície de ataque da imagem e a experiência dos desenvolvedores que consomem sua imagem. Vamos mergulhar em cada uma delas com exemplos práticos e explicações detalhadas.
Imagens pequenas
Imagens menores são mais rápidas de transferir, ocupam menos espaço em disco e reduzem a superfície de ataque, pois contêm menos binários e bibliotecas potencialmente vulneráveis. Uma imagem enxuta também acelera o pull em ambientes de orquestração como Kubernetes, melhorando o tempo de escalonamento.
Uma das estratégias mais eficazes é usar imagens base Alpine, que são extremamente pequenas (cerca de 5 MB) devido ao uso do musl libc e de um conjunto mínimo de utilitários. Por exemplo, uma imagem Ubuntu pode ter 50 MB ou mais, enquanto Alpine tem apenas alguns MB. No entanto, é preciso estar ciente de que alguns pacotes podem não estar disponíveis nos repositórios Alpine ou podem ter nomes diferentes, exigindo ajustes.
Outra técnica é remover arquivos temporários e caches dentro do mesmo comando, usando && para encadear comandos e rm -rf para limpar. Isso evita que camadas intermediárias armazenem dados desnecessários.
FROM alpine:3.19
RUN apk add --no-cache curl \
&& curl -sL https://example.com/install.sh | sh \
&& rm -rf /var/cache/apk/*
No exemplo acima, usamos --no-cache para evitar o cache do gerenciador de pacotes e removemos qualquer resíduo. Isso mantém a camada final limpa. Além disso, prefira imagens oficiais e específicas (como python:3.12-slim em vez de python:latest) para garantir consistência e menor tamanho.
Multi-stage builds
O multi-stage build é um recurso poderoso que permite usar múltiplas instruções FROM no mesmo Dockerfile, cada uma criando um estágio. Você pode compilar e testar em um estágio com todas as ferramentas necessárias e, em seguida, copiar apenas os artefatos de build para um estágio final leve. Isso é ideal para linguagens compiladas como Go, Java, ou para projetos que exigem dependências de build pesadas.
Considere um exemplo com uma aplicação Go: o estágio de build usa a imagem golang:1.22 com o compilador e as bibliotecas, e o estágio final usa alpine com apenas o binário compilado. O resultado é uma imagem final com dezenas de MB a menos.
# Estágio de build
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o meuapp .
# Estágio final
FROM alpine:3.19
RUN adduser -D appuser
USER appuser
COPY --from=builder /app/meuapp /usr/local/bin/meuapp
CMD ["meuapp"]
Além de reduzir o tamanho, o multi-stage permite que você não exponha ferramentas de desenvolvimento na imagem final, melhorando a segurança. Você também pode usar alvos intermediários para testes, como um estágio de teste que roda go test antes de compilar. Isso é uma prática recomendada para pipelines de CI/CD.
Ordem de camadas
O Docker armazena cada instrução do Dockerfile como uma camada, e essas camadas são cacheadas. Se uma camada não mudar, ela é reutilizada em builds subsequentes, acelerando o processo. Portanto, a ordem das instruções é crucial para maximizar o cache.
A regra de ouro é colocar as instruções que mudam com menos frequência no início (como a instalação de dependências) e as que mudam com mais frequência no final (como a cópia do código-fonte). Isso significa que, se você alterar apenas o código, o cache das camadas de dependência será reutilizado, e o build será rápido.
Um erro comum é copiar o código antes de instalar as dependências, o que invalida o cache de dependências a cada alteração no código. Veja um exemplo ruim e um bom:
# Ruim: copia o código antes de instalar dependências
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
# Bom: instala dependências antes de copiar o código
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
No exemplo bom, se package.json não mudar, a camada RUN npm install será cacheada, e apenas a cópia do código será refeita. Isso economiza tempo significativo em projetos com muitas dependências.
.dockerignore
O arquivo .dockerignore funciona como um .gitignore para o build do Docker. Ele especifica quais arquivos e diretórios devem ser excluídos do contexto de build, que é enviado ao daemon do Docker. Isso reduz o tamanho do contexto, acelerando o envio e evitando que arquivos sensíveis ou desnecessários sejam copiados para a imagem.
Um exemplo típico inclui diretórios como node_modules, .git, pastas de build, arquivos temporários e ambientes virtuais. Excluí-los não só melhora a performance, mas também evita que segredos (como .env) sejam acidentalmente incluídos na imagem.
# .dockerignore
.git
node_modules
npm-debug.log
.env
Dockerfile*
docker-compose*
README.md
dist
coverage
É importante lembrar que o .dockerignore não afeta as instruções COPY e ADD dentro do Dockerfile, pois essas já são aplicadas após o contexto ser enviado. No entanto, ele influencia o que está disponível no contexto, então se você excluir um diretório, não poderá copiá-lo.
Uma boa prática é sempre criar um .dockerignore mesmo em projetos pequenos, pois isso evita surpresas e mantém o build limpo.
Referências
- Dockerfile best practices - Documentação oficial do Docker
- Multi-stage builds - Documentação do Docker
- Dockerfile reference - Documentação oficial
- .dockerignore file - Documentação oficial
- Image best practices - Guia do Docker
- Alpine Linux - Site oficial
Exercícios
Crie um Dockerfile para uma aplicação Node.js que use multi-stage build para reduzir o tamanho da imagem final. O estágio de build deve instalar as dependências e compilar o código, e o estágio final deve apenas rodar a aplicação.
✓ Resposta:# Estágio de build FROM node:20 AS builder WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build # Estágio final FROM node:20-slim WORKDIR /app COPY --from=builder /app/package*.json ./ RUN npm install --omit=dev COPY --from=builder /app/dist ./dist EXPOSE 3000 CMD ["node", "dist/server.js"]Explique por que a ordem das camadas no Dockerfile é importante e dê um exemplo de um Dockerfile com boa ordem para uma aplicação Python.
✓ Resposta: A ordem é importante porque o Docker cacheia cada camada. Se uma camada não mudar, ela é reutilizada. Portanto, devemos colocar instruções que mudam raramente (como instalar dependências) antes das que mudam frequentemente (como copiar o código). Exemplo:FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD ["python", "app.py"]Escreva um arquivo
.dockerignorepara um projeto Node.js que ignore pelo menos 5 itens desnecessários.✓ Resposta:# .dockerignore node_modules .git .env npm-debug.log Dockerfile* docker-compose* README.mdQual é a vantagem de usar uma imagem base Alpine em vez de uma imagem Ubuntu para uma aplicação simples? Cite duas vantagens e uma desvantagem.
✓ Resposta: Vantagens: 1) Tamanho muito menor (cerca de 5 MB vs 50+ MB), resultando em downloads mais rápidos e menor uso de disco. 2) Menor superfície de ataque, pois há menos pacotes instalados. Desvantagem: Algumas bibliotecas podem não estar disponíveis ou ter comportamento diferente devido ao musl libc, exigindo mais ajustes.No Dockerfile a seguir, identifique o problema relacionado à ordem de camadas e reescreva-o seguindo as boas práticas.
FROM node:20 WORKDIR /app COPY . . RUN npm install✓ Resposta: O problema é queCOPY . .copia todo o código antes de instalar as dependências, invalidando o cache deRUN npm installsempre que o código mudar. A versão corrigida:FROM node:20 WORKDIR /app COPY package*.json ./ RUN npm install COPY . .
Boas práticas adicionais
Além das técnicas abordadas, considere usar usuários não-root nas imagens finais, definir ENV e EXPOSE claramente, e evitar instalar pacotes desnecessários. Use também o recurso de HEALTHCHECK para monitorar a saúde do contêiner e mantenha o Dockerfile legível com comentários. Essas práticas tornam suas imagens mais seguras, confiáveis e fáceis de manter.