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

Exercícios

  1. 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 como RUN, COPY e ADD. 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.
  2. Escreva um Dockerfile para uma aplicação Node.js que usa o arquivo package.json e server.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"]
    
  3. Qual é a diferença entre docker build -t minha-imagem . e docker build -t minha-imagem:1.0 .?

    ✓ Resposta: A diferença está na tag atribuída à imagem. No primeiro caso, a tag padrão latest é usada; no segundo, a tag é 1.0. É boa prática usar tags semânticas para versionamento, pois latest pode ser ambígua.
  4. 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, um COPY de 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.
  5. Cite duas boas práticas para reduzir o tamanho de uma imagem Docker.

    ✓ Resposta: 1) Usar imagens base pequenas, como alpine. 2) Combinar comandos RUN em uma única instrução e limpar caches e arquivos temporários no mesmo passo, por exemplo usando apt-get clean e removendo listas de pacotes.