Comment corriger 'ModuleNotFoundError' lors de la construction d'une image Docker

DockerBeginner
Pratiquer maintenant

Introduction

Lors de la construction d'images Docker pour des applications Python, les développeurs rencontrent souvent le message 'ModuleNotFoundError'. Cette erreur se produit lorsque Python ne parvient pas à localiser un module ou un package requis par votre application. Pour les débutants de Docker, cela peut être particulièrement difficile à dépanner.

Dans ce lab pratique, vous allez créer une application Python simple, la conteneuriser avec Docker, rencontrer le ModuleNotFoundError, et apprendre des moyens pratiques de le résoudre. À la fin, vous comprendrez comment gérer correctement les dépendances Python dans les images Docker et éviter ce problème courant dans vos projets.

Création d'une application Python simple

Créons une application Python de base et configurons Docker pour l'exécuter. Cela nous aidera à comprendre comment le ModuleNotFoundError se produit dans un environnement Docker.

Comprendre la structure de l'application Python

Tout d'abord, créons un répertoire de projet et naviguons-y :

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

Maintenant, créons une application Python simple qui importe un module tiers. Nous allons créer deux fichiers :

  1. Un fichier d'application principal
  2. Un fichier de requirements pour lister les dépendances

Créez le fichier d'application principal :

nano app.py

Ajoutez le code suivant à 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()

Ce script simple utilise la bibliothèque requests pour effectuer une requête HTTP vers example.com et afficher des informations de base sur la réponse.

Maintenant, créons un fichier de requirements :

nano requirements.txt

Ajoutez la ligne suivante à requirements.txt :

requests==2.28.1

Création d'un Dockerfile de base

Maintenant, créons un Dockerfile simple qui démontrera le ModuleNotFoundError :

nano Dockerfile

Ajoutez le contenu suivant au 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"]

Ce Dockerfile :

  • Utilise l'image Python 3.9 slim comme base
  • Définit le répertoire de travail sur /app
  • Copie notre fichier d'application
  • Spécifie la commande pour exécuter notre application

Remarquez que nous n'avons délibérément pas copié le fichier requirements.txt ni installé de dépendances. Cela provoquera le ModuleNotFoundError lorsque nous essaierons d'exécuter le conteneur.

Construction et exécution de l'image Docker

Construisons l'image Docker :

docker build -t python-app-error .

Vous devriez voir une sortie similaire à celle-ci :

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

Maintenant, exécutons le conteneur Docker :

docker run python-app-error

Vous devriez voir un message d'erreur similaire à celui-ci :

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

C'est le ModuleNotFoundError sur lequel nous nous concentrons dans ce lab. L'erreur se produit parce que nous n'avons pas inclus le module requests requis dans notre image Docker.

Comprendre et corriger le ModuleNotFoundError

Maintenant que nous avons rencontré le ModuleNotFoundError, comprenons pourquoi il s'est produit et comment le corriger.

Pourquoi le ModuleNotFoundError se produit-il dans Docker ?

Le ModuleNotFoundError se produit dans Docker pour plusieurs raisons courantes :

  1. Installation de dépendances manquantes : Nous n'avons pas installé les packages Python requis dans l'image Docker.
  2. PYTHONPATH incorrect : L'interpréteur Python ne peut pas trouver les modules aux emplacements attendus.
  3. Problèmes de structure de fichiers : La structure du code de l'application ne correspond pas à la façon dont les importations sont effectuées.

Dans notre cas, l'erreur s'est produite parce que nous n'avons pas installé le package requests dans notre image Docker. Contrairement à notre environnement de développement local où nous pourrions avoir ce package installé globalement, les conteneurs Docker sont des environnements isolés.

Méthode 1 : Installation des dépendances à l'aide de pip dans le Dockerfile

Modifions notre Dockerfile pour installer les dépendances requises :

nano Dockerfile

Mettez à jour le Dockerfile avec le contenu suivant :

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

Construisons et exécutons cette image mise à jour :

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

Vous devriez voir une sortie qui inclut l'installation du package :

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

Maintenant, exécutons le conteneur corrigé :

docker run python-app-fixed-1

Vous devriez voir une sortie similaire à celle-ci :

Status code: 200
Content length: 1256 characters

Génial ! L'application s'exécute maintenant avec succès car nous avons installé la dépendance requise.

Méthode 2 : Utilisation de requirements.txt pour la gestion des dépendances

Bien que l'installation directe des packages fonctionne, il est préférable d'utiliser un fichier requirements.txt pour une gestion des dépendances plus organisée. Mettons à jour notre Dockerfile :

nano Dockerfile

Mettez à jour le Dockerfile avec le contenu suivant :

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

Cette approche présente plusieurs avantages :

  • Elle sépare la gestion des dépendances du code
  • Elle facilite la mise à jour des dépendances
  • Elle suit les meilleures pratiques pour la mise en cache des couches d'images Docker

Construisons et exécutons cette image mise à jour :

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

Vous devriez voir une sortie similaire à la construction précédente, mais cette fois, elle utilise 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

Maintenant, exécutons le conteneur :

docker run python-app-fixed-2

Vous devriez voir la même sortie réussie :

Status code: 200
Content length: 1256 characters

Vous avez réussi à corriger le ModuleNotFoundError en utilisant deux méthodes différentes !

Bonnes pratiques pour éviter le ModuleNotFoundError

Maintenant que nous avons corrigé le problème immédiat, examinons quelques bonnes pratiques pour éviter le ModuleNotFoundError dans les images Docker.

Comprendre la mise en cache Docker pour des builds efficaces

Docker utilise une approche en couches pour construire des images. Chaque instruction dans un Dockerfile crée une nouvelle couche. Lorsque vous reconstruisez une image, Docker réutilise les couches mises en cache si possible, ce qui peut accélérer considérablement le processus de construction.

Pour les applications Python, vous pouvez optimiser la mise en cache en :

  1. Copiant et installant les requirements avant de copier le code de l'application
  2. Conservant les fichiers qui changent fréquemment (comme le code de l'application) dans les couches ultérieures

Mettons à jour notre Dockerfile pour suivre ces bonnes pratiques :

nano Dockerfile

Mettez à jour le Dockerfile avec le contenu optimisé suivant :

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

Construisons cette image optimisée :

docker build -t python-app-optimized .

Et exécutons-la pour vérifier qu'elle fonctionne :

docker run python-app-optimized

Vous devriez voir la même sortie réussie :

Status code: 200
Content length: 1256 characters

Utilisation d'un fichier .dockerignore

Pour rendre vos builds Docker plus efficaces, il est de bonne pratique d'utiliser un fichier .dockerignore pour exclure les fichiers et répertoires qui ne sont pas nécessaires dans l'image Docker. Cela réduit la taille du contexte de construction et améliore les performances de construction.

Créons un fichier .dockerignore :

nano .dockerignore

Ajoutez le contenu suivant :

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

Création d'une structure d'application plus complexe

Pour les applications plus volumineuses avec plusieurs modules, il est important de structurer correctement votre projet. Créons un exemple légèrement plus complexe :

mkdir -p myapp

Créez un fichier de module :

nano myapp/__init__.py

Laissez ce fichier vide (il marque simplement le répertoire comme un package Python).

Maintenant, créez un fichier de module avec quelques fonctionnalités :

nano myapp/utils.py

Ajoutez le code suivant :

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

Maintenant, mettez à jour notre application principale pour utiliser ce module :

nano app.py

Remplacez le contenu par :

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()

Construisez et exécutez l'application mise à jour :

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

Vous devriez voir une sortie qui inclut notre message personnalisé :

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

Bonnes pratiques supplémentaires

Voici quelques bonnes pratiques supplémentaires pour éviter le ModuleNotFoundError dans Docker :

  1. Environnements virtuels : Bien que non strictement nécessaires dans Docker (puisque les conteneurs sont isolés), l'utilisation d'environnements virtuels peut aider à garantir la cohérence entre le développement et la production.

  2. Dépendances épinglées : Spécifiez toujours les versions exactes des dépendances pour garantir la cohérence entre les différents environnements.

  3. Constructions multi-étapes : Pour les images de production, envisagez d'utiliser des constructions multi-étapes pour créer des images plus petites avec uniquement les dépendances nécessaires.

  4. Mises à jour régulières des dépendances : Mettez régulièrement à jour vos dépendances pour obtenir des correctifs de sécurité et des améliorations.

En suivant ces bonnes pratiques, vous minimiserez les risques de rencontrer le ModuleNotFoundError dans vos conteneurs Docker et créerez des images Docker plus efficaces et maintenables.

Résumé

Dans ce lab, vous avez appris à identifier, dépanner et corriger le ModuleNotFoundError lorsque vous travaillez avec des images Docker pour des applications Python. Vous avez acquis une expérience pratique dans :

  • La création d'une application Python de base et sa conteneurisation avec Docker
  • La compréhension des raisons pour lesquelles le ModuleNotFoundError se produit dans les environnements Docker
  • La résolution des problèmes de dépendances en utilisant l'installation directe de packages et requirements.txt
  • La mise en œuvre des bonnes pratiques Docker telles que la mise en cache correcte des couches et la structure des fichiers
  • La création d'une structure d'application plus complexe avec plusieurs modules
  • L'utilisation de .dockerignore pour optimiser les builds Docker

Ces compétences vous aideront à créer des images Docker plus fiables et maintenables pour vos applications Python. En suivant les bonnes pratiques couvertes dans ce lab, vous pouvez éviter les pièges courants comme le ModuleNotFoundError et optimiser votre flux de travail de développement Docker.

N'oubliez pas qu'une gestion appropriée des dépendances est cruciale lorsque vous travaillez avec des applications conteneurisées. Assurez-vous toujours que vos images Docker incluent toutes les dépendances nécessaires, un code correctement structuré et respectent les bonnes pratiques en matière d'efficacité et de maintenabilité.