Técnicas Avançadas de Dockerfile

DockerBeginner
Pratique Agora

Introdução

Neste laboratório, vamos nos aprofundar nas técnicas de Dockerfile, explorando conceitos avançados que ajudarão você a criar imagens Docker mais eficientes e flexíveis. Abordaremos instruções detalhadas de Dockerfile, builds de múltiplos estágios (multi-stage builds) e o uso de arquivos .dockerignore. Também exploraremos o conceito crucial de camadas (layers) em imagens Docker. Ao final deste laboratório, você terá uma compreensão abrangente dessas técnicas avançadas e será capaz de aplicá-las em seus próprios projetos.

Este laboratório foi projetado pensando em iniciantes, fornecendo explicações detalhadas e abordando possíveis pontos de confusão. Usaremos o WebIDE (VS Code) para todas as nossas tarefas de edição de arquivos, facilitando a criação e modificação de arquivos diretamente no navegador.

Entendendo Instruções e Camadas do Dockerfile

Vamos começar criando um Dockerfile que utiliza várias instruções. Construiremos uma imagem para uma aplicação web Python usando Flask e, ao longo do caminho, exploraremos como cada instrução contribui para as camadas da nossa imagem Docker.

  1. Primeiro, vamos criar um novo diretório para o nosso projeto. No terminal do WebIDE, execute:
mkdir -p ~/project/advanced-dockerfile && cd ~/project/advanced-dockerfile

Este comando cria um novo diretório chamado advanced-dockerfile dentro da pasta project e, em seguida, entra nesse diretório.

  1. Agora, vamos criar o arquivo da nossa aplicação. No explorador de arquivos do WebIDE (geralmente no lado esquerdo da tela), clique com o botão direito na pasta advanced-dockerfile e selecione "New File". Nomeie este arquivo como app.py.

  2. Abra o app.py e adicione o seguinte código Python:

from flask import Flask
import os

app = Flask(__name__)

@app.route('/')
def hello():
    return f"Hello from {os.environ.get('ENVIRONMENT', 'unknown')} environment!"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

Esta é uma aplicação Flask simples que responde com uma mensagem de saudação, incluindo o ambiente em que está sendo executada.

  1. Em seguida, precisamos criar um arquivo requirements.txt para especificar nossas dependências Python. Crie um novo arquivo chamado requirements.txt no mesmo diretório e adicione o seguinte conteúdo:
Flask==2.0.1
Werkzeug==2.0.1

Aqui, estamos especificando versões exatas para o Flask e o Werkzeug para garantir a compatibilidade.

  1. Agora, vamos criar o nosso Dockerfile. Crie um novo arquivo chamado Dockerfile (com 'D' maiúsculo) no mesmo diretório e adicione o seguinte conteúdo:
## Use an official Python runtime as the base image
FROM python:3.9-slim

## Set the working directory in the container
WORKDIR /app

## Set an environment variable
ENV ENVIRONMENT=production

## Copy the requirements file into the container
COPY requirements.txt .

## Install the required packages
RUN pip install --no-cache-dir -r requirements.txt

## Copy the application code into the container
COPY app.py .

## Specify the command to run when the container starts
CMD ["python", "app.py"]

## Expose the port the app runs on
EXPOSE 5000

## Add labels for metadata
LABEL maintainer="Your Name <your.email@example.com>"
LABEL version="1.0"
LABEL description="Flask app demo for advanced Dockerfile techniques"

Agora, vamos detalhar essas instruções e entender como elas contribuem para as camadas da nossa imagem Docker:

  • FROM python:3.9-slim: Esta é sempre a primeira instrução. Ela especifica a imagem base a partir da qual estamos construindo. Isso cria a primeira camada da nossa imagem, que inclui o ambiente de execução Python.
  • WORKDIR /app: Define o diretório de trabalho para as instruções subsequentes. Não cria uma nova camada, mas afeta o comportamento das instruções seguintes.
  • ENV ENVIRONMENT=production: Define uma variável de ambiente. Variáveis de ambiente não criam novas camadas, mas são armazenadas nos metadados da imagem.
  • COPY requirements.txt .: Copia o arquivo de requisitos do nosso host para dentro da imagem. Isso cria uma nova camada contendo apenas este arquivo.
  • RUN pip install --no-cache-dir -r requirements.txt: Executa um comando no contêiner durante o processo de build. Ele instala nossas dependências Python. Isso cria uma nova camada que contém todos os pacotes instalados.
  • COPY app.py .: Copia o código da nossa aplicação para a imagem, criando outra camada.
  • CMD ["python", "app.py"]: Especifica o comando a ser executado quando o contêiner for iniciado. Não cria uma camada, mas define o comando padrão para o contêiner.
  • EXPOSE 5000: Na verdade, é apenas uma forma de documentação. Informa ao Docker que o contêiner ouvirá nesta porta em tempo de execução, mas não publica a porta de fato. Não cria uma camada.
  • LABEL ...: Adiciona metadados à imagem. Assim como as instruções ENV, não criam novas camadas, mas são armazenadas nos metadados da imagem.

Cada instrução RUN, COPY e ADD em um Dockerfile cria uma nova camada. As camadas são um conceito fundamental no Docker que permite o armazenamento e a transferência eficiente de imagens. Quando você faz alterações no seu Dockerfile e reconstrói a imagem, o Docker reutiliza as camadas em cache que não foram alteradas, acelerando o processo de build.

  1. Agora que entendemos o que nosso Dockerfile está fazendo, vamos construir a imagem Docker. No terminal, execute:
docker build -t advanced-flask-app .

Este comando constrói uma nova imagem Docker com a tag advanced-flask-app. O . no final diz ao Docker para procurar o Dockerfile no diretório atual.

Você verá uma saída mostrando cada etapa do processo de build. Observe como cada etapa corresponde a uma instrução em nosso Dockerfile e como o Docker menciona "Using cache" para etapas que não mudaram se você executar o comando de build várias vezes.

  1. Assim que o build for concluído, podemos executar um contêiner baseado em nossa nova imagem:
docker run -d -p 5000:5000 --name flask-container advanced-flask-app

Este comando faz o seguinte:

  • -d executa o contêiner em modo desanexado (em segundo plano)
  • -p 5000:5000 mapeia a porta 5000 do seu host para a porta 5000 no contêiner
  • --name flask-container dá um nome ao nosso novo contêiner
  • advanced-flask-app é a imagem que estamos usando para criar o contêiner

Você pode verificar se o contêiner está rodando verificando a lista de contêineres ativos:

docker ps
  1. Para testar se nossa aplicação está funcionando corretamente, podemos usar o comando curl:
curl http://localhost:5000

Você deve ver a mensagem "Hello from production environment!"

Se tiver problemas com o curl, você também pode abrir uma nova aba no navegador e visitar http://localhost:5000. Você deverá ver a mesma mensagem.

Se encontrar algum problema, você pode verificar os logs do contêiner usando:

docker logs flask-container

Isso mostrará quaisquer mensagens de erro ou saídas da sua aplicação Flask.

Builds de Múltiplos Estágios

Agora que entendemos as instruções básicas e as camadas do Dockerfile, vamos explorar uma técnica mais avançada: builds de múltiplos estágios (multi-stage builds). Eles permitem que você use várias instruções FROM em seu Dockerfile. Isso é particularmente útil para criar imagens finais menores, copiando apenas os artefatos necessários de um estágio para outro.

Vamos modificar nosso Dockerfile para usar um build de múltiplos estágios que resulte em uma imagem menor:

  1. No WebIDE, abra o Dockerfile que criamos anteriormente.
  2. Substitua todo o conteúdo pelo seguinte:
## Build stage
FROM python:3.9-slim AS builder

WORKDIR /app

COPY requirements.txt .

RUN pip install --user --no-cache-dir -r requirements.txt

## Final stage
FROM python:3.9-slim

WORKDIR /app

## Copy only the installed packages from the builder stage
COPY --from=builder /root/.local /root/.local
COPY app.py .

ENV PATH=/root/.local/bin:$PATH
ENV ENVIRONMENT=production

CMD ["python", "app.py"]

EXPOSE 5000

LABEL maintainer="Your Name <your.email@example.com>"
LABEL version="1.0"
LABEL description="Flask app demo with multi-stage build"

Vamos analisar o que está acontecendo neste Dockerfile de múltiplos estágios:

  1. Começamos com um estágio builder:

    • Usamos a imagem Python 3.9-slim como base para manter as coisas pequenas desde o início.
    • Instalamos nossas dependências Python neste estágio usando pip install --user. Isso instala os pacotes no diretório home do usuário.
  2. Depois, temos o nosso estágio final:

    • Começamos do zero com outra imagem Python 3.9-slim.
    • Copiamos apenas os pacotes instalados do estágio builder, especificamente de /root/.local, onde o pip install --user os colocou.
    • Copiamos o código da nossa aplicação.
    • Adicionamos o diretório bin local ao PATH para que o Python possa encontrar os pacotes instalados.
    • Configuramos o restante do nosso contêiner (ENV, CMD, EXPOSE, LABEL) como antes.

A principal vantagem aqui é que nossa imagem final não inclui nenhuma das ferramentas de build ou caches do processo de instalação do pip. Ela contém apenas os artefatos finais necessários. Isso deve resultar em uma imagem menor.

  1. Vamos construir esta nova imagem de múltiplos estágios. No terminal, execute:
docker build -t multi-stage-flask-app .
  1. Assim que o build for concluído, vamos comparar os tamanhos das nossas duas imagens. Execute:
docker images | grep flask-app
multi-stage-flask-app         latest     7bdd1be2d1fb   10 seconds ago   129MB
advanced-flask-app            latest     c59d6fa303cc   10 minutes ago   136MB

Você deve notar que a multi-stage-flask-app é menor que a advanced-flask-app que construímos anteriormente.

  1. Agora, vamos rodar um contêiner com nossa nova imagem mais enxuta:
docker run -d -p 5001:5000 --name multi-stage-container multi-stage-flask-app

Note que estamos usando uma porta de host diferente (5001) para evitar conflitos com nosso contêiner anterior.

  1. Teste a aplicação:
curl http://localhost:5001

Você ainda deve ver a mensagem "Hello from production environment!"

  1. Para entender melhor as diferenças entre nossas imagens de estágio único e múltiplos estágios, podemos usar o comando docker history. Execute estes comandos:
docker history advanced-flask-app
docker history multi-stage-flask-app

Compare as saídas. Você deve notar que o build de múltiplos estágios tem menos camadas e tamanhos menores para algumas delas.

Builds de múltiplos estágios são uma técnica poderosa para criar imagens Docker eficientes. Eles permitem que você use ferramentas e arquivos em seu processo de build sem inflar sua imagem final. Isso é particularmente útil para linguagens compiladas ou aplicações com processos de build complexos.

Neste caso, usamos para criar uma imagem de aplicação Python menor, copiando apenas os pacotes instalados e o código da aplicação necessários, deixando para trás quaisquer artefatos de build ou caches.

Usando o Arquivo .dockerignore

Ao construir uma imagem Docker, o Docker envia todos os arquivos do diretório para o daemon do Docker. Se você tiver arquivos grandes que não são necessários para a construção da sua imagem, isso pode tornar o processo de build mais lento. O arquivo .dockerignore permite especificar arquivos e diretórios que devem ser excluídos ao construir uma imagem Docker.

Vamos criar um arquivo .dockerignore e ver como ele funciona:

  1. No WebIDE, crie um novo arquivo no diretório advanced-dockerfile e nomeie-o como .dockerignore.
  2. Adicione o seguinte conteúdo ao arquivo .dockerignore:
**/.git
**/.gitignore
**/__pycache__
**/*.pyc
**/*.pyo
**/*.pyd
**/.Python
**/env
**/venv
**/ENV
**/env.bak
**/venv.bak

Vamos detalhar o que esses padrões significam:

  • **/.git: Ignora o diretório .git e todo o seu conteúdo, onde quer que ele apareça na estrutura de diretórios.
  • **/.gitignore: Ignora arquivos .gitignore.
  • **/__pycache__: Ignora os diretórios de cache do Python.
  • **/*.pyc, **/*.pyo, **/*.pyd: Ignora arquivos Python compilados.
  • **/.Python: Ignora arquivos .Python (frequentemente criados por ambientes virtuais).
  • **/env, **/venv, **/ENV: Ignora diretórios de ambiente virtual.
  • **/env.bak, **/venv.bak: Ignora cópias de backup de diretórios de ambiente virtual.

O ** no início de cada linha significa "em qualquer diretório".

  1. Para demonstrar o efeito do arquivo .dockerignore, vamos criar alguns arquivos que queremos ignorar. No terminal, execute:
mkdir venv
touch venv/ignore_me.txt
touch .gitignore

Esses comandos criam um diretório venv com um arquivo dentro e um arquivo .gitignore. Esses são elementos comuns em projetos Python que normalmente não queremos em nossas imagens Docker.

  1. Agora, vamos construir nossa imagem novamente:
docker build -t ignored-flask-app .
  1. Para verificar se os arquivos ignorados não foram incluídos no contexto de build, podemos usar o comando docker history:
docker history ignored-flask-app

Você não deve ver nenhuma etapa que copie o diretório venv ou o arquivo .gitignore.

O arquivo .dockerignore é uma ferramenta poderosa para manter suas imagens Docker limpas e seu processo de build eficiente. É especialmente útil para projetos maiores, onde você pode ter muitos arquivos que não são necessários na imagem final.

Instruções Avançadas de Dockerfile

Nesta etapa final, exploraremos algumas instruções adicionais de Dockerfile e boas práticas que podem ajudar a tornar suas imagens Docker mais seguras, fáceis de manter e de usar. Também focaremos na solução de problemas e na verificação de cada etapa do processo.

  1. No WebIDE, abra o Dockerfile novamente.

  2. Substitua o conteúdo pelo seguinte:

## Build stage
FROM python:3.9-slim AS builder

WORKDIR /app

COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

## Final stage
FROM python:3.9-slim

## Create a non-root user
RUN useradd -m appuser

## Install curl for healthcheck
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

WORKDIR /app

## Dynamically determine Python version and site-packages path
RUN PYTHON_VERSION=$(python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') && \
    SITE_PACKAGES_PATH="/home/appuser/.local/lib/python${PYTHON_VERSION}/site-packages" && \
    mkdir -p "${SITE_PACKAGES_PATH}" && \
    chown -R appuser:appuser /home/appuser/.local

## Copy site-packages and binaries using the variable
COPY --from=builder /root/.local/lib/python3.9/site-packages "${SITE_PACKAGES_PATH}"
COPY --from=builder /root/.local/bin /home/appuser/.local/bin
COPY app.py .

ENV PATH=/home/appuser/.local/bin:$PATH
ENV ENVIRONMENT=production

## Set the user to run the application
USER appuser

## Use ENTRYPOINT with CMD
ENTRYPOINT ["python"]
CMD ["app.py"]

EXPOSE 5000

HEALTHCHECK --interval=30s --timeout=3s \
  CMD curl -f http://localhost:5000/ || exit 1

ARG BUILD_VERSION
LABEL maintainer="Your Name <your.email@example.com>"
LABEL version="${BUILD_VERSION:-1.0}"
LABEL description="Flask app demo with advanced Dockerfile techniques"

Vamos analisar os novos conceitos introduzidos neste Dockerfile:

  • RUN useradd -m appuser: Cria um novo usuário chamado appuser no contêiner. Executar aplicações como um usuário não-root é uma boa prática de segurança, pois limita os danos potenciais se a aplicação for comprometida. A flag -m cria um diretório home para o usuário.
  • RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*: Instala o pacote curl, necessário para que nossa instrução HEALTHCHECK funcione. Também limpamos o cache do apt para reduzir o tamanho da imagem.
  • RUN PYTHON_VERSION=$(python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') && ...: Este conjunto de comandos determina dinamicamente a versão do Python dentro do contêiner e cria o diretório site-packages correto para o appuser. Também define as permissões corretas para o diretório local do usuário.
  • COPY --from=builder /root/.local/lib/python3.9/site-packages "${SITE_PACKAGES_PATH}": Esta instrução copia os pacotes Python instalados do estágio builder para o caminho site-packages determinado dinamicamente na imagem final, garantindo que os pacotes sejam colocados no local correto para o appuser usar.
  • COPY --from=builder /root/.local/bin /home/appuser/.local/bin: Copia os scripts executáveis instalados pelo pip (como a interface de linha de comando do Flask, se houver) do estágio builder para o diretório bin local do appuser.
  • ENTRYPOINT ["python"] com CMD ["app.py"]: Quando usados juntos, o ENTRYPOINT define o executável principal do contêiner (neste caso, python) e o CMD fornece os argumentos padrão para esse executável (app.py). Esse padrão permite flexibilidade: os usuários podem rodar o contêiner e executar o app.py por padrão, ou podem sobrescrever o CMD para rodar outros scripts ou comandos Python.
  • HEALTHCHECK: Esta instrução configura uma verificação de saúde para o contêiner. O Docker executará periodicamente o comando especificado (curl -f http://localhost:5000/) para determinar se o contêiner está saudável. As flags --interval=30s e --timeout=3s definem o intervalo de verificação e o tempo limite, respectivamente. Se o comando curl falhar (retornar um código de saída diferente de zero), o contêiner será considerado não saudável (unhealthy).
  • ARG BUILD_VERSION: Define um argumento de build chamado BUILD_VERSION. Argumentos de build permitem passar valores para a imagem Docker durante o tempo de construção.
  • LABEL version="${BUILD_VERSION:-1.0}": Define um rótulo (label) chamado version na imagem Docker. Ele usa o argumento de build BUILD_VERSION. Se BUILD_VERSION for fornecido durante o build, seu valor será usado; caso contrário, o padrão será 1.0 (usando a sintaxe de valor padrão :-).
  1. Agora, vamos construir esta nova imagem, especificando uma versão de build:
docker build -t advanced-flask-app-v2 --build-arg BUILD_VERSION=2.0 .

A flag --build-arg BUILD_VERSION=2.0 nos permite passar o valor 2.0 para o argumento de build BUILD_VERSION durante o processo de construção da imagem. Esse valor será usado para definir o label version na imagem Docker.

  1. Assim que o build for concluído, vamos verificar se a imagem foi criada com sucesso:
docker images | grep advanced-flask-app-v2

Você deve ver a nova imagem advanced-flask-app-v2 listada na saída do comando docker images, junto com sua tag, ID da imagem, data de criação e tamanho.

  1. Agora, vamos rodar um contêiner com a nova imagem:
docker run -d -p 5002:5000 --name advanced-container-v2 advanced-flask-app-v2

Este comando executa um contêiner em modo desanexado (-d), mapeia a porta 5002 do seu host para a porta 5000 no contêiner (-p 5002:5000), nomeia o contêiner como advanced-container-v2 (--name advanced-container-v2) e usa a imagem advanced-flask-app-v2 para criar o contêiner.

  1. Vamos verificar se o contêiner está rodando:
docker ps | grep advanced-container-v2

Se o contêiner estiver rodando com sucesso, você deverá vê-lo listado na saída do comando docker ps. Se não vir o contêiner listado, ele pode ter encerrado. Vamos verificar se há contêineres parados:

docker ps -a | grep advanced-container-v2

Se você vir o contêiner listado na saída de docker ps -a, mas ele não estiver rodando (o status não é "Up"), podemos verificar seus logs em busca de erros:

docker logs advanced-container-v2

Este comando exibirá os logs do contêiner advanced-container-v2, o que pode ajudar a diagnosticar quaisquer problemas de inicialização ou erros de tempo de execução em sua aplicação Flask.

  1. Assumindo que o contêiner está rodando, após dar um momento para ele iniciar, podemos verificar seu status de saúde:
docker inspect --format='{{.State.Health.Status}}' advanced-container-v2

Após um curto atraso (para permitir que a verificação de saúde seja executada pelo menos uma vez), você deve ver "healthy" como saída. Se vir "unhealthy" inicialmente, aguarde mais 30 segundos (o intervalo da verificação de saúde) e execute o comando novamente. Se permanecer "unhealthy", verifique os logs do contêiner usando docker logs advanced-container-v2 para identificar possíveis problemas com sua aplicação Flask. Se não houver problemas óbvios, você pode ignorar o status "unhealthy".

  1. Também podemos verificar se o nosso label de versão de build foi aplicado corretamente:
docker inspect -f '{{.Config.Labels.version}}' advanced-container-v2

Este comando recupera o valor do label version do contêiner advanced-container-v2 e o exibe. Você deve ver "2.0" como saída, o que confirma que o argumento de build BUILD_VERSION foi usado corretamente para definir o label.

  1. Finalmente, vamos testar nossa aplicação enviando uma requisição para ela:
curl http://localhost:5002

Você deve ver a mensagem "Hello from production environment!" na saída. Isso indica que sua aplicação Flask está rodando corretamente dentro do contêiner Docker e está acessível na porta 5002 do seu host.

Essas técnicas avançadas permitem criar imagens Docker mais seguras, configuráveis e prontas para produção. O usuário não-root melhora a segurança, o HEALTHCHECK ajuda na orquestração e monitoramento de contêineres, e os argumentos de build permitem uma construção de imagem mais flexível e versionada.

Resumo

Neste laboratório, exploramos técnicas avançadas de Dockerfile que ajudarão você a criar imagens Docker mais eficientes, seguras e fáceis de manter. Cobrimos:

  1. Instruções detalhadas de Dockerfile e seu impacto nas camadas da imagem: Aprendemos como cada instrução contribui para a estrutura da nossa imagem Docker e como entender as camadas pode nos ajudar a otimizar nossas imagens.
  2. Builds de múltiplos estágios: Usamos essa técnica para criar imagens finais menores, separando nosso ambiente de build do nosso ambiente de execução.
  3. Uso de arquivos .dockerignore: Aprendemos como excluir arquivos desnecessários do nosso contexto de build, o que pode acelerar as construções e reduzir o tamanho da imagem.
  4. Instruções avançadas de Dockerfile: Exploramos instruções adicionais como USER, ENTRYPOINT, HEALTHCHECK e ARG, que nos permitem criar imagens mais seguras e flexíveis.

Essas técnicas permitem que você:

  • Crie imagens Docker mais otimizadas e menores
  • Melhore a segurança executando aplicações como usuários não-root
  • Implemente verificações de saúde para uma melhor orquestração de contêineres
  • Use variáveis em tempo de build para uma construção de imagem mais flexível

Ao longo deste laboratório, usamos o WebIDE (VS Code) para editar nossos arquivos, facilitando a criação e modificação de Dockerfiles e do código da aplicação diretamente no navegador. Essa abordagem permite uma experiência de desenvolvimento contínua ao trabalhar com Docker.