Como corrigir 'ModuleNotFoundError' ao construir uma imagem Docker

DockerBeginner
Pratique Agora

Introdução

Ao construir imagens Docker para aplicações Python, os desenvolvedores frequentemente se deparam com a mensagem 'ModuleNotFoundError'. Este erro ocorre quando o Python não consegue localizar um módulo ou pacote que sua aplicação requer. Para iniciantes no Docker, isso pode ser particularmente desafiador de solucionar.

Neste laboratório prático, você criará uma aplicação Python simples, a containerizará com Docker, encontrará o ModuleNotFoundError e aprenderá maneiras práticas de resolvê-lo. Ao final, você entenderá como gerenciar corretamente as dependências Python em imagens Docker e evitar este problema comum em seus projetos.

Criando uma Aplicação Python Simples

Vamos criar uma aplicação Python básica e configurar o Docker para executá-la. Isso nos ajudará a entender como o ModuleNotFoundError ocorre em um ambiente Docker.

Entendendo a Estrutura da Aplicação Python

Primeiro, vamos criar um diretório de projeto e navegar até ele:

mkdir -p ~/project/docker-python-app
cd ~/project/docker-python-app

Agora, vamos criar uma aplicação Python simples que importa um módulo de terceiros. Criaremos dois arquivos:

  1. Um arquivo de aplicação principal
  2. Um arquivo de requisitos para listar as dependências

Crie o arquivo de aplicação principal:

nano app.py

Adicione o seguinte código a app.py:

import requests

def main():
    response = requests.get("https://www.example.com")
    print(f"Status code: {response.status_code}")
    print(f"Content length: {len(response.text)} characters")

if __name__ == "__main__":
    main()

Este script simples usa a biblioteca requests para fazer uma requisição HTTP para example.com e imprimir algumas informações básicas sobre a resposta.

Agora, vamos criar um arquivo de requisitos:

nano requirements.txt

Adicione a seguinte linha a requirements.txt:

requests==2.28.1

Criando um Dockerfile Básico

Agora, vamos criar um Dockerfile simples que demonstrará o ModuleNotFoundError:

nano Dockerfile

Adicione o seguinte conteúdo ao Dockerfile:

FROM python:3.9-slim

WORKDIR /app

COPY app.py .

## We're intentionally NOT copying or installing requirements
## to demonstrate the ModuleNotFoundError

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

Este Dockerfile:

  • Usa a imagem Python 3.9 slim como base
  • Define o diretório de trabalho como /app
  • Copia nosso arquivo de aplicação
  • Especifica o comando para executar nossa aplicação

Observe que deliberadamente não copiamos o arquivo requirements.txt nem instalamos nenhuma dependência. Isso causará o ModuleNotFoundError quando tentarmos executar o container.

Construindo e Executando a Imagem Docker

Vamos construir a imagem Docker:

docker build -t python-app-error .

Você deve ver uma saída semelhante a esta:

Sending build context to Docker daemon  3.072kB
Step 1/4 : FROM python:3.9-slim
 ---> 3a4bac80b3ea
Step 2/4 : WORKDIR /app
 ---> Using cache
 ---> a8a4f574dbf5
Step 3/4 : COPY app.py .
 ---> Using cache
 ---> 7d5ae315f84b
Step 4/4 : CMD ["python", "app.py"]
 ---> Using cache
 ---> f5a9b09d7d8e
Successfully built f5a9b09d7d8e
Successfully tagged python-app-error:latest

Agora, vamos executar o container Docker:

docker run python-app-error

Você deve ver uma mensagem de erro semelhante a esta:

Traceback (most recent call last):
  File "/app/app.py", line 1, in <module>
    import requests
ModuleNotFoundError: No module named 'requests'

Este é o ModuleNotFoundError em que estamos focando neste laboratório. O erro ocorre porque não incluímos o módulo requests requerido em nossa imagem Docker.

Entendendo e Corrigindo o ModuleNotFoundError

Agora que encontramos o ModuleNotFoundError, vamos entender por que ele aconteceu e como corrigi-lo.

Por que o ModuleNotFoundError Ocorre no Docker?

O ModuleNotFoundError ocorre no Docker por várias razões comuns:

  1. Instalação de dependência ausente: Não instalamos os pacotes Python necessários na imagem Docker.
  2. PYTHONPATH incorreto: O interpretador Python não consegue encontrar os módulos nos locais esperados.
  3. Problemas na estrutura de arquivos: A estrutura do código da aplicação não corresponde à forma como as importações estão sendo feitas.

Em nosso caso, o erro ocorreu porque não instalamos o pacote requests em nossa imagem Docker. Ao contrário do nosso ambiente de desenvolvimento local, onde podemos ter este pacote instalado globalmente, os containers Docker são ambientes isolados.

Método 1: Instalando Dependências Usando pip no Dockerfile

Vamos modificar nosso Dockerfile para instalar as dependências necessárias:

nano Dockerfile

Atualize o Dockerfile com o seguinte conteúdo:

FROM python:3.9-slim

WORKDIR /app

COPY app.py .

## Fix Method 1: Directly install the required package
RUN pip install requests==2.28.1

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

Vamos construir e executar esta imagem atualizada:

docker build -t python-app-fixed-1 .

Você deve ver uma saída que inclui a instalação do pacote:

Sending build context to Docker daemon  3.072kB
Step 1/5 : FROM python:3.9-slim
 ---> 3a4bac80b3ea
Step 2/5 : WORKDIR /app
 ---> Using cache
 ---> a8a4f574dbf5
Step 3/5 : COPY app.py .
 ---> Using cache
 ---> 7d5ae315f84b
Step 4/5 : RUN pip install requests==2.28.1
 ---> Running in 5a6d7e8f9b0c
Collecting requests==2.28.1
  Downloading requests-2.28.1-py3-none-any.whl (62 kB)
Collecting charset-normalizer<3,>=2
  Downloading charset_normalizer-2.1.1-py3-none-any.whl (39 kB)
Collecting certifi>=2017.4.17
  Downloading certifi-2022.9.24-py3-none-any.whl (161 kB)
Collecting idna<4,>=2.5
  Downloading idna-3.4-py3-none-any.whl (61 kB)
Collecting urllib3<1.27,>=1.21.1
  Downloading urllib3-1.26.12-py2.py3-none-any.whl (140 kB)
Installing collected packages: urllib3, idna, charset-normalizer, certifi, requests
Successfully installed certifi-2022.9.24 charset-normalizer-2.1.1 idna-3.4 requests-2.28.1 urllib3-1.26.12
 ---> 2b3c4d5e6f7g
Removing intermediate container 5a6d7e8f9b0c
Step 5/5 : CMD ["python", "app.py"]
 ---> Running in 8h9i0j1k2l3m
 ---> 3n4o5p6q7r8s
Removing intermediate container 8h9i0j1k2l3m
Successfully built 3n4o5p6q7r8s
Successfully tagged python-app-fixed-1:latest

Agora vamos executar o container corrigido:

docker run python-app-fixed-1

Você deve ver uma saída semelhante a esta:

Status code: 200
Content length: 1256 characters

Ótimo! A aplicação agora é executada com sucesso porque instalamos a dependência necessária.

Método 2: Usando requirements.txt para Gerenciamento de Dependências

Embora a instalação direta de pacotes funcione, é melhor prática usar um arquivo requirements.txt para um gerenciamento de dependências mais organizado. Vamos atualizar nosso Dockerfile:

nano Dockerfile

Atualize o Dockerfile com o seguinte conteúdo:

FROM python:3.9-slim

WORKDIR /app

## Copy requirements first to leverage Docker cache
COPY requirements.txt .

## Fix Method 2: Use requirements.txt
RUN pip install -r requirements.txt

## Copy the rest of the application
COPY app.py .

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

Esta abordagem tem várias vantagens:

  • Separa o gerenciamento de dependências do código
  • Facilita a atualização das dependências
  • Segue as melhores práticas para o cache de camadas de imagem Docker

Vamos construir e executar esta imagem atualizada:

docker build -t python-app-fixed-2 .

Você deve ver uma saída semelhante à compilação anterior, mas desta vez está usando requirements.txt:

Sending build context to Docker daemon  4.096kB
Step 1/5 : FROM python:3.9-slim
 ---> 3a4bac80b3ea
Step 2/5 : WORKDIR /app
 ---> Using cache
 ---> a8a4f574dbf5
Step 3/5 : COPY requirements.txt .
 ---> Using cache
 ---> b2c3d4e5f6g7
Step 4/5 : RUN pip install -r requirements.txt
 ---> Running in h8i9j0k1l2m3
Collecting requests==2.28.1
  Using cached requests-2.28.1-py3-none-any.whl (62 kB)
Collecting charset-normalizer<3,>=2
  Using cached charset_normalizer-2.1.1-py3-none-any.whl (39 kB)
Collecting idna<4,>=2.5
  Using cached idna-3.4-py3-none-any.whl (61 kB)
Collecting certifi>=2017.4.17
  Using cached certifi-2022.9.24-py3-none-any.whl (161 kB)
Collecting urllib3<1.27,>=1.21.1
  Using cached urllib3-1.26.12-py2.py3-none-any.whl (140 kB)
Installing collected packages: urllib3, idna, charset-normalizer, certifi, requests
Successfully installed certifi-2022.9.24 charset-normalizer-2.1.1 idna-3.4 requests-2.28.1 urllib3-1.26.12
 ---> n4o5p6q7r8s9
Removing intermediate container h8i9j0k1l2m3
Step 5/5 : COPY app.py .
 ---> t0u1v2w3x4y5
Step 6/6 : CMD ["python", "app.py"]
 ---> Running in z5a6b7c8d9e0
 ---> f1g2h3i4j5k6
Removing intermediate container z5a6b7c8d9e0
Successfully built f1g2h3i4j5k6
Successfully tagged python-app-fixed-2:latest

Agora vamos executar o container:

docker run python-app-fixed-2

Você deve ver a mesma saída bem-sucedida:

Status code: 200
Content length: 1256 characters

Você corrigiu com sucesso o ModuleNotFoundError usando dois métodos diferentes!

Melhores Práticas para Evitar ModuleNotFoundError

Agora que corrigimos o problema imediato, vamos analisar algumas melhores práticas para evitar o ModuleNotFoundError em imagens Docker.

Entendendo o Caching do Docker para Builds Eficientes

O Docker usa uma abordagem em camadas para construir imagens. Cada instrução em um Dockerfile cria uma nova camada. Quando você reconstrói uma imagem, o Docker reutiliza as camadas em cache, se possível, o que pode acelerar significativamente o processo de construção.

Para aplicações Python, você pode otimizar o caching por:

  1. Copiar e instalar os requisitos antes de copiar o código da aplicação
  2. Manter arquivos que mudam com frequência (como o código da aplicação) nas camadas posteriores

Vamos atualizar nosso Dockerfile para seguir essas melhores práticas:

nano Dockerfile

Atualize o Dockerfile com o seguinte conteúdo otimizado:

FROM python:3.9-slim

WORKDIR /app

## Copy requirements first for better caching
COPY requirements.txt .

## Install dependencies
RUN pip install --no-cache-dir -r requirements.txt

## Copy application code (changes more frequently)
COPY . .

## Make sure we run the application with Python's unbuffered mode for better logging
CMD ["python", "-u", "app.py"]

Vamos construir esta imagem otimizada:

docker build -t python-app-optimized .

E executá-la para verificar se funciona:

docker run python-app-optimized

Você deve ver a mesma saída bem-sucedida:

Status code: 200
Content length: 1256 characters

Usando um Arquivo .dockerignore

Para tornar suas construções Docker mais eficientes, é uma boa prática usar um arquivo .dockerignore para excluir arquivos e diretórios que não são necessários na imagem Docker. Isso reduz o tamanho do contexto de construção e melhora o desempenho da construção.

Vamos criar um arquivo .dockerignore:

nano .dockerignore

Adicione o seguinte conteúdo:

__pycache__
*.pyc
*.pyo
*.pyd
.Python
.git
.gitignore
*.log
*.pot
*.env

Criando uma Estrutura de Aplicação Mais Complexa

Para aplicações maiores com múltiplos módulos, é importante estruturar seu projeto corretamente. Vamos criar um exemplo um pouco mais complexo:

mkdir -p myapp

Crie um arquivo de módulo:

nano myapp/__init__.py

Deixe este arquivo vazio (ele apenas marca o diretório como um pacote Python).

Agora crie um arquivo de módulo com alguma funcionalidade:

nano myapp/utils.py

Adicione o seguinte código:

def get_message():
    return "Hello from myapp.utils module!"

Agora atualize nossa aplicação principal para usar este módulo:

nano app.py

Substitua o conteúdo por:

import requests
from myapp.utils import get_message

def main():
    response = requests.get("https://www.example.com")
    print(f"Status code: {response.status_code}")
    print(f"Content length: {len(response.text)} characters")
    print(get_message())

if __name__ == "__main__":
    main()

Construa e execute a aplicação atualizada:

docker build -t python-app-modules .
docker run python-app-modules

Você deve ver uma saída que inclui nossa mensagem personalizada:

Status code: 200
Content length: 1256 characters
Hello from myapp.utils module!

Práticas Adicionais Recomendadas

Aqui estão algumas práticas adicionais recomendadas para evitar ModuleNotFoundError no Docker:

  1. Ambientes virtuais: Embora não seja estritamente necessário no Docker (já que os containers são isolados), usar ambientes virtuais pode ajudar a garantir a consistência entre desenvolvimento e produção.

  2. Dependências fixadas: Sempre especifique versões exatas das dependências para garantir a consistência em diferentes ambientes.

  3. Construções de múltiplos estágios: Para imagens de produção, considere usar construções de múltiplos estágios para criar imagens menores com apenas as dependências necessárias.

  4. Atualizações regulares de dependências: Atualize regularmente suas dependências para obter correções de segurança e melhorias.

Ao seguir essas melhores práticas, você minimizará as chances de encontrar o ModuleNotFoundError em seus containers Docker e criará imagens Docker mais eficientes e sustentáveis.

Resumo

Neste laboratório, você aprendeu a identificar, solucionar problemas e corrigir o ModuleNotFoundError ao trabalhar com imagens Docker para aplicações Python. Você adquiriu experiência prática em:

  • Criar uma aplicação Python básica e conteinerizá-la com Docker
  • Entender por que o ModuleNotFoundError ocorre em ambientes Docker
  • Corrigir problemas de dependência usando instalação direta de pacotes e requirements.txt
  • Implementar as melhores práticas do Docker, como o caching adequado de camadas e a estrutura de arquivos
  • Criar uma estrutura de aplicação mais complexa com múltiplos módulos
  • Usar .dockerignore para otimizar as construções Docker

Essas habilidades ajudarão você a criar imagens Docker mais confiáveis e sustentáveis para suas aplicações Python. Ao seguir as melhores práticas abordadas neste laboratório, você pode evitar armadilhas comuns como o ModuleNotFoundError e otimizar seu fluxo de trabalho de desenvolvimento Docker.

Lembre-se que o gerenciamento adequado de dependências é crucial ao trabalhar com aplicações conteinerizadas. Sempre certifique-se de que suas imagens Docker incluam todas as dependências necessárias, código devidamente estruturado e sigam as melhores práticas para eficiência e sustentabilidade.