Dockerfile: boas práticas
Esta aula ensina boas práticas para escrever Dockerfiles eficientes e seguros, cobrindo imagens pequenas, multi-stage builds, ordem de camadas e .dockerignore, com exemplos práticos em bash.
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
.envAlé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
- Dockerfile best practices - Documentação Oficial
- Multi-stage builds - Documentação Docker
- Dockerfile reference - Documentação Docker
- Build context e .dockerignore - Documentação Docker
- Criar imagens base - Documentação Docker
- Instrução HEALTHCHECK - Documentação Docker
Exercícios
- Explique por que é importante usar imagens base pequenas e cite duas estratégias para reduzir o tamanho da imagem final.
- Descreva como um multi-stage build funciona e quais são os benefícios em relação a um Dockerfile de estágio único.
- Por que a ordem das camadas no Dockerfile afeta o desempenho do build? Dê um exemplo de ordem eficiente.
- O que é .dockerignore e por que ele é importante para a segurança e performance do build?
- Escreva um Dockerfile multi-stage para compilar um programa em Go e gerar uma imagem final mínima usando alpine. Inclua comentários explicativos.
# 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"]