Imagens Docker e Dockerfile
Esta aula ensina a criar e gerenciar imagens Docker usando Dockerfiles, explorando conceitos fundamentais como camadas, build, tag e cache de build. Você aprenderá a escrever Dockerfiles eficientes, otimizar o processo de build e aplicar boas práticas para reduzir o tamanho e o tempo de construção das imagens.
Nesta aula, vamos mergulhar no coração do Docker: as imagens e os arquivos que as definem, os Dockerfiles. Uma imagem Docker é um pacote executável que inclui tudo o que é necessário para rodar uma aplicação: código, runtime, bibliotecas, variáveis de ambiente e configurações. Compreender como as imagens são construídas, como funcionam suas camadas e como otimizar o processo de build é essencial para qualquer profissional de DevOps.
Veremos na prática como escrever um Dockerfile, construir uma imagem com o comando docker build, atribuir tags significativas e aproveitar o cache de build para acelerar o desenvolvimento. Ao final, você terá uma base sólida para criar imagens eficientes e reproduzíveis, prontas para serem usadas em pipelines de CI/CD e em produção.
Camadas
As imagens Docker são compostas por uma série de camadas (layers) empilhadas. Cada instrução no Dockerfile que modifica o sistema de arquivos cria uma nova camada. Essas camadas são imutáveis e armazenadas em cache, o que permite que builds incrementais sejam muito rápidos: se uma camada não mudou, o Docker reutiliza a versão em cache.
Entender o conceito de camadas é crucial para otimizar o tamanho e a eficiência das imagens. Por exemplo, instruções como RUN, COPY e ADD criam novas camadas, enquanto ENV, CMD e ENTRYPOINT adicionam metadados, mas não criam camadas de sistema de arquivos. Cada camada é como um diff da anterior; quando você puxa uma imagem, o Docker baixa apenas as camadas que faltam, economizando banda e espaço.
Um ponto importante é que camadas intermediárias podem conter dados sensíveis ou desnecessários. Por isso, boas práticas recomendam combinar comandos RUN em uma única instrução, usar arquivos .dockerignore e limpar artefatos temporários dentro da mesma camada. Vejamos um exemplo de como as camadas se acumulam:
# Exemplo de Dockerfile com várias camadas
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y curl
COPY app.py /app/
CMD ["python", "/app/app.py"]
Neste exemplo, temos as seguintes camadas: a base do Ubuntu, uma camada com o resultado do RUN (que inclui o curl), uma camada com o arquivo app.py copiado, e finalmente a instrução CMD que define o comando padrão, mas não adiciona uma camada de sistema de arquivos. Se alterarmos o app.py, apenas a camada correspondente ao COPY será recriada, enquanto as anteriores permanecem em cache.
Dockerfile
O Dockerfile é um arquivo de texto com instruções que o Docker segue para montar uma imagem automaticamente. Cada instrução é uma linha no formato INSTRUÇÃO argumentos. As instruções mais comuns incluem FROM, RUN, COPY, ADD, ENV, EXPOSE, CMD e ENTRYPOINT.
A instrução FROM define a imagem base, que pode ser uma distribuição Linux, uma imagem oficial de uma linguagem (como python:3.11) ou até uma imagem minimalista como alpine. A escolha da imagem base influencia diretamente o tamanho final e a segurança da imagem. RUN executa comandos em uma camada intermediária, COPY copia arquivos do contexto de build para a imagem, e CMD define o comando padrão a ser executado quando o contêiner é iniciado.
Vamos criar um Dockerfile simples para uma aplicação Python que usa Flask. O arquivo ficaria assim:
# Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]
Analisando linha a linha: FROM define a base; WORKDIR define o diretório de trabalho; COPY requirements.txt copia o arquivo de dependências; RUN instala as dependências; COPY . . copia o restante do código; EXPOSE documenta a porta; e CMD define o comando de execução. Observe que copiamos primeiro o requirements.txt e depois o restante, para aproveitar o cache de camadas: se as dependências não mudarem, o passo de instalação não precisa ser refeito.
build e tag
O comando docker build é usado para construir uma imagem a partir de um Dockerfile e um contexto. O contexto é o conjunto de arquivos que estão no diretório especificado (geralmente o diretório atual, representado por .). O comando básico é:
docker build -t nome-da-imagem:tag .
A opção -t (ou --tag) permite dar um nome e uma tag à imagem. A tag é uma etiqueta que identifica uma versão específica (por exemplo, v1.0, latest). Se você não especificar uma tag, o Docker usará latest por padrão. É uma boa prática usar tags semânticas, como 1.0.0, e reservar latest para a versão estável mais recente.
Para listar as imagens locais, use docker images. Para inspecionar os detalhes de uma imagem, docker inspect nome-imagem. Se precisar remover uma imagem, docker rmi nome-imagem. Vejamos um fluxo completo de build e tag:
# Construir a imagem com tag 'v1.0'
docker build -t minha-app:v1.0 .
# Listar imagens
docker images
# Executar um contêiner a partir da imagem
docker run -p 5000:5000 minha-app:v1.0
Além disso, é possível usar múltiplas tags para a mesma imagem, por exemplo, docker build -t minha-app:latest -t minha-app:1.0 .. Isso é útil para manter referências fáceis. Em pipelines de CI, é comum usar o hash do commit como tag, garantindo rastreabilidade.
Cache de build
O cache de build é um dos recursos mais valiosos do Docker para acelerar o desenvolvimento. Quando você executa docker build, o Docker verifica se cada camada pode ser reutilizada de um build anterior. Se as instruções e os arquivos envolvidos não mudaram, a camada é recuperada do cache, evitando repetir trabalho.
No entanto, o cache é invalidado quando algo muda. Por exemplo, se você alterar o conteúdo de um arquivo copiado por COPY, todas as camadas subsequentes serão reconstruídas. Por isso, a ordem das instruções no Dockerfile é crucial: coloque instruções que mudam com pouca frequência no início, e as que mudam com frequência no final. O exemplo clássico é copiar o requirements.txt antes do restante do código, para que a instalação das dependências seja cacheada enquanto o código muda.
Para visualizar o cache em ação, execute um build duas vezes. Na segunda vez, você verá a mensagem Using cache para as camadas inalteradas. Você também pode forçar a reconstrução sem cache com --no-cache, útil quando você suspeita que o cache está desatualizado ou quando quer garantir uma construção limpa.
Outra técnica avançada é o BuildKit, que é o mecanismo de build moderno do Docker. Ele oferece melhorias de performance, cache de camadas mais inteligente e suporte a builds paralelos. Para ativá-lo, basta definir a variável de ambiente DOCKER_BUILDKIT=1 ou usar o comando docker buildx build.
Boas práticas e observações finais
Para criar imagens eficientes e seguras, siga estas recomendações: use imagens base oficiais e pequenas (como alpine), combine comandos RUN para reduzir o número de camadas, limpe arquivos temporários e caches dentro da mesma instrução, e use .dockerignore para evitar enviar arquivos desnecessários ao daemon.
Além disso, prefira copiar apenas o necessário, utilize usuários não-root no contêiner, e sempre defina EXPOSE para documentar as portas. Lembre-se de que cada camada adiciona tamanho, então busque um equilíbrio entre legibilidade e otimização. Com essas práticas, suas imagens serão mais rápidas de construir, menores e mais seguras.
Referências
- Dockerfile best practices - Documentação oficial do Docker
- Dockerfile reference - Documentação oficial
- Docker Build - Documentação oficial
- Cache de build - Documentação oficial
- Criando imagens base - Documentação oficial
- Comando docker build - Referência
Exercícios
Explique o que são camadas em uma imagem Docker e como elas influenciam o processo de build.
✓ Resposta: Camadas são estados intermediários do sistema de arquivos da imagem, criados por instruções comoRUN,COPYeADD. Elas são imutáveis e armazenadas em cache. O Docker reutiliza camadas inalteradas, acelerando builds. O tamanho da imagem é a soma das camadas, então reduzir o número de camadas ou seu conteúdo ajuda a otimizar.Escreva um Dockerfile para uma aplicação Node.js que usa o arquivo
package.jsoneserver.js. Inclua a instalação de dependências e a exposição da porta 3000.✓ Resposta:FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm install COPY . . EXPOSE 3000 CMD ["node", "server.js"]Qual é a diferença entre
docker build -t minha-imagem .edocker build -t minha-imagem:1.0 .?✓ Resposta: A diferença está na tag atribuída à imagem. No primeiro caso, a tag padrãolatesté usada; no segundo, a tag é1.0. É boa prática usar tags semânticas para versionamento, poislatestpode ser ambígua.Explique como o cache de build funciona e por que a ordem das instruções no Dockerfile é importante para aproveitá-lo.
✓ Resposta: O cache de build reutiliza camadas de builds anteriores se a instrução e os arquivos envolvidos não mudaram. A ordem importa porque, se uma camada é invalidada (por exemplo, umCOPYde um arquivo alterado), todas as camadas subsequentes são reconstruídas. Colocar instruções que mudam pouco (como instalação de dependências) antes das que mudam muito (como o código) maximiza o cache.Cite duas boas práticas para reduzir o tamanho de uma imagem Docker.
✓ Resposta: 1) Usar imagens base pequenas, comoalpine. 2) Combinar comandosRUNem uma única instrução e limpar caches e arquivos temporários no mesmo passo, por exemplo usandoapt-get cleane removendo listas de pacotes.