Nesta aula, você aprenderá a criar imagens Docker para aplicações Python, um passo essencial para garantir consistência entre ambientes de desenvolvimento, teste e produção. Docker permite empacotar sua aplicação e todas as suas dependências em um contêiner leve e portátil, eliminando o clássico problema de "funciona na minha máquina".

Vamos explorar os principais elementos de um Dockerfile para Python, desde a escolha da imagem base até técnicas avançadas como multi-stage builds, além de boas práticas para criar imagens seguras, eficientes e fáceis de manter.

Imagem base

A imagem base é o alicerce do seu contêiner. Para aplicações Python, a escolha mais comum é a imagem oficial python disponível no Docker Hub. Existem várias variantes: python:3.12-slim (base Debian enxuta), python:3.12-alpine (base Alpine, ainda menor, mas com possíveis incompatibilidades), e python:3.12 (versão completa). A recomendação geral é usar a variante slim para reduzir o tamanho da imagem, mantendo compatibilidade com a maioria das bibliotecas.

Além disso, você pode usar imagens específicas para frameworks (ex.: python:3.12-slim-bullseye) ou imagens com pré-requisitos instalados, como python:3.12-slim já inclui o pip e o venv. Evite usar a tag latest para garantir reprodutibilidade; prefira versões fixas, como python:3.12.2-slim.

Exemplo de Dockerfile básico:

FROM python:3.12-slim

WORKDIR /app

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

COPY . .

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

requirements

O arquivo requirements.txt lista todas as dependências Python da aplicação. É crucial que ele seja copiado para a imagem antes da instalação, para que o Docker possa usar o cache de camadas (layer caching). Se você copiar o código antes de instalar as dependências, qualquer alteração no código invalidará o cache e forçará uma reinstalação completa, tornando o build mais lento.

Prática recomendada: primeiro copie o requirements.txt, instale as dependências e só depois copie o restante do código. Isso aproveita o cache do Docker: enquanto o requirements.txt não mudar, a camada de instalação não será reconstruída.

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"]

Use --no-cache-dir no pip para evitar o cache de pacotes em disco, reduzindo o tamanho da imagem. Para produção, considere usar um arquivo de bloqueio como Pipfile.lock ou poetry.lock, mas requirements.txt é suficiente para a maioria dos projetos.

Multi-stage (visão geral)

Multi-stage builds permitem usar múltiplas imagens base em um único Dockerfile, separando o ambiente de build do ambiente de execução. Isso é útil para reduzir drasticamente o tamanho da imagem final, pois você pode compilar dependências em uma etapa e copiar apenas os artefatos necessários para a etapa final.

No contexto Python, é comum usar uma imagem completa (como python:3.12) para compilar pacotes que exigem compilador (por exemplo, psycopg2 ou numpy) e depois copiar os pacotes instalados para uma imagem slim. No entanto, para a maioria das aplicações Python puras, o multi-stage não é obrigatório, mas é uma boa prática para otimização.

Exemplo de multi-stage build para uma aplicação com dependências compiladas:

# Estágio 1: build
FROM python:3.12 AS builder

WORKDIR /app
COPY requirements.txt .
RUN pip install --prefix=/install -r requirements.txt

# Estágio 2: runtime
FROM python:3.12-slim

WORKDIR /app
COPY --from=builder /install /usr/local
COPY . .

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

Nesse exemplo, as dependências são instaladas no diretório /install na imagem builder, e depois copiadas para a imagem final. Isso evita que ferramentas de build (como gcc) fiquem na imagem final.

Boas práticas

Além dos pontos já mencionados, aqui estão outras boas práticas para Docker com Python:

  • Use .dockerignore para excluir arquivos desnecessários (como .git, __pycache__, .venv) do contexto de build, acelerando o envio e reduzindo o risco de vazamento de dados.
  • Execute como usuário não-root por segurança. Crie um usuário dedicado no Dockerfile e use USER para executar a aplicação.
  • Defina variáveis de ambiente com ENV para configurações como PYTHONDONTWRITEBYTECODE=1 (evita arquivos .pyc) e PYTHONUNBUFFERED=1 (logs em tempo real).
  • Prefira COPY em vez de ADD quando não precisar de extração automática de arquivos.
  • Use CMD em vez de ENTRYPOINT para comandos simples, a menos que precise de um contêiner executável.
  • Mantenha as camadas pequenas agrupando comandos relacionados, mas sem exagerar: cada RUN cria uma camada, e camadas são reutilizadas em cache.

Exemplo de Dockerfile com boas práticas:

FROM python:3.12-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

WORKDIR /app

RUN addgroup --system app && adduser --system --group app

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

COPY --chown=app:app . .

USER app

EXPOSE 8000

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

Referências

Exercícios

  1. Exercício 1: Crie um Dockerfile simples para uma aplicação Flask que escuta na porta 5000. Use a imagem python:3.12-slim, instale as dependências de requirements.txt e defina o comando para rodar app.py.

    ✓ Resposta:
    FROM python:3.12-slim
    WORKDIR /app
    COPY requirements.txt .
    RUN pip install --no-cache-dir -r requirements.txt
    COPY . .
    EXPOSE 5000
    CMD ["python", "app.py"]
    
  2. Exercício 2: Explique por que é importante copiar o requirements.txt antes do código-fonte no Dockerfile e como isso afeta o cache de camadas.

    ✓ Resposta: Copiar o requirements.txt antes do código permite que o Docker reutilize a camada de instalação das dependências enquanto o arquivo não mudar. Se o código for copiado primeiro, qualquer alteração no código invalida a camada e força a reinstalação de todas as dependências, tornando o build mais lento. Essa prática melhora a eficiência do processo de build.
  3. Exercício 3: Escreva um Dockerfile multi-stage para uma aplicação que requer compilação de uma dependência (por exemplo, psycopg2). Use a imagem python:3.12 para build e python:3.12-slim para runtime.

    ✓ Resposta:
    # Estágio 1: build
    FROM python:3.12 AS builder
    WORKDIR /app
    COPY requirements.txt .
    RUN pip install --prefix=/install -r requirements.txt
    
    # Estágio 2: runtime
    FROM python:3.12-slim
    WORKDIR /app
    COPY --from=builder /install /usr/local
    COPY . .
    CMD ["python", "app.py"]
    
  4. Exercício 4: Liste três boas práticas de segurança e três de performance em Dockerfiles Python.

    ✓ Resposta: Segurança: 1) Executar como usuário não-root; 2) Usar imagens oficiais e atualizadas; 3) Evitar expor portas desnecessárias. Performance: 1) Copiar requirements antes do código para aproveitar cache; 2) Usar imagens slim; 3) Combinar comandos RUN para reduzir camadas.
  5. Exercício 5: Crie um .dockerignore para um projeto Python que inclua os arquivos: .git, __pycache__, .venv, .env e *.pyc.

    ✓ Resposta:
    .git
    __pycache__/
    .venv
    .env
    *.pyc
    

Observações finais

Dominar Docker com Python é uma habilidade valiosa para o desenvolvimento moderno. Lembre-se de sempre testar suas imagens localmente e usar ferramentas como docker compose para orquestrar múltiplos serviços. À medida que seus projetos crescem, revise os Dockerfiles periodicamente para incorporar novas boas práticas e melhorar a eficiência.