¿Cómo solucionar 'ModuleNotFoundError' al construir una imagen Docker?

DockerBeginner
Practicar Ahora

Introducción

Al construir imágenes de Docker para aplicaciones Python, los desarrolladores a menudo se encuentran con el mensaje 'ModuleNotFoundError'. Este error ocurre cuando Python no puede localizar un módulo o paquete que su aplicación requiere. Para los principiantes en Docker, esto puede ser particularmente desafiante de solucionar.

En este laboratorio práctico, creará una aplicación Python simple, la contendrá con Docker, se encontrará con el ModuleNotFoundError y aprenderá formas prácticas de resolverlo. Al final, comprenderá cómo gestionar correctamente las dependencias de Python en las imágenes de Docker y evitar este problema común en sus proyectos.

Creación de una Aplicación Python Simple

Creemos una aplicación Python básica y configuremos Docker para ejecutarla. Esto nos ayudará a comprender cómo ocurre el ModuleNotFoundError en un entorno Docker.

Entendiendo la Estructura de la Aplicación Python

Primero, creemos un directorio de proyecto y naveguemos a él:

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

Ahora, creemos una aplicación Python simple que importe un módulo de terceros. Crearemos dos archivos:

  1. Un archivo de aplicación principal
  2. Un archivo de requisitos para listar las dependencias

Cree el archivo de aplicación principal:

nano app.py

Agregue el siguiente 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 simple utiliza la biblioteca requests para hacer una solicitud HTTP a example.com e imprimir información básica sobre la respuesta.

Ahora, creemos un archivo de requisitos:

nano requirements.txt

Agregue la siguiente línea a requirements.txt:

requests==2.28.1

Creación de un Dockerfile Básico

Ahora, creemos un Dockerfile simple que demostrará el ModuleNotFoundError:

nano Dockerfile

Agregue el siguiente contenido al 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:

  • Utiliza la imagen Python 3.9 slim como base
  • Establece el directorio de trabajo en /app
  • Copia nuestro archivo de aplicación
  • Especifica el comando para ejecutar nuestra aplicación

Observe que deliberadamente no copiamos el archivo requirements.txt ni instalamos ninguna dependencia. Esto causará el ModuleNotFoundError cuando intentemos ejecutar el contenedor.

Construyendo y Ejecutando la Imagen Docker

Construyamos la imagen Docker:

docker build -t python-app-error .

Debería ver una salida similar 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

Ahora, ejecutemos el contenedor Docker:

docker run python-app-error

Debería ver un mensaje de error similar a este:

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

Este es el ModuleNotFoundError en el que nos estamos enfocando en este laboratorio. El error ocurre porque no incluimos el módulo requests requerido en nuestra imagen Docker.

Entendiendo y Solucionando el ModuleNotFoundError

Ahora que nos hemos encontrado con el ModuleNotFoundError, entendamos por qué ocurrió y cómo solucionarlo.

¿Por qué ocurre el ModuleNotFoundError en Docker?

El ModuleNotFoundError ocurre en Docker por varias razones comunes:

  1. Instalación de dependencias faltante: No instalamos los paquetes Python requeridos en la imagen Docker.
  2. PYTHONPATH incorrecto: El intérprete de Python no puede encontrar los módulos en las ubicaciones esperadas.
  3. Problemas de estructura de archivos: La estructura del código de la aplicación no coincide con la forma en que se están haciendo las importaciones.

En nuestro caso, el error ocurrió porque no instalamos el paquete requests en nuestra imagen Docker. A diferencia de nuestro entorno de desarrollo local donde podríamos tener este paquete instalado globalmente, los contenedores Docker son entornos aislados.

Método 1: Instalación de Dependencias Usando pip en el Dockerfile

Modifiquemos nuestro Dockerfile para instalar las dependencias requeridas:

nano Dockerfile

Actualice el Dockerfile con el siguiente contenido:

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

Construyamos y ejecutemos esta imagen actualizada:

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

Debería ver una salida que incluye la instalación del paquete:

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

Ahora ejecutemos el contenedor corregido:

docker run python-app-fixed-1

Debería ver una salida similar a esta:

Status code: 200
Content length: 1256 characters

¡Genial! La aplicación ahora se ejecuta con éxito porque instalamos la dependencia requerida.

Método 2: Usando requirements.txt para la Gestión de Dependencias

Si bien la instalación directa de paquetes funciona, es mejor práctica usar un archivo requirements.txt para una gestión de dependencias más organizada. Actualicemos nuestro Dockerfile:

nano Dockerfile

Actualice el Dockerfile con el siguiente contenido:

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

Este enfoque tiene varias ventajas:

  • Separa la gestión de dependencias del código
  • Facilita la actualización de dependencias
  • Sigue las mejores prácticas para el almacenamiento en caché de capas de imágenes de Docker

Construyamos y ejecutemos esta imagen actualizada:

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

Debería ver una salida similar a la compilación anterior, pero esta 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

Ahora ejecutemos el contenedor:

docker run python-app-fixed-2

Debería ver la misma salida exitosa:

Status code: 200
Content length: 1256 characters

¡Ha solucionado con éxito el ModuleNotFoundError utilizando dos métodos diferentes!

Mejores Prácticas para Evitar el ModuleNotFoundError

Ahora que hemos solucionado el problema inmediato, veamos algunas de las mejores prácticas para evitar el ModuleNotFoundError en las imágenes Docker.

Entendiendo el Caching de Docker para Construcciones Eficientes

Docker utiliza un enfoque en capas para construir imágenes. Cada instrucción en un Dockerfile crea una nueva capa. Cuando reconstruye una imagen, Docker reutiliza las capas almacenadas en caché si es posible, lo que puede acelerar significativamente el proceso de construcción.

Para las aplicaciones Python, puede optimizar el almacenamiento en caché al:

  1. Copiar e instalar los requisitos antes de copiar el código de la aplicación
  2. Mantener los archivos que cambian con frecuencia (como el código de la aplicación) en las capas posteriores

Actualicemos nuestro Dockerfile para seguir estas mejores prácticas:

nano Dockerfile

Actualice el Dockerfile con el siguiente contenido optimizado:

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

Construyamos esta imagen optimizada:

docker build -t python-app-optimized .

Y ejecútela para verificar que funciona:

docker run python-app-optimized

Debería ver la misma salida exitosa:

Status code: 200
Content length: 1256 characters

Usando un Archivo .dockerignore

Para que sus construcciones de Docker sean más eficientes, es una buena práctica usar un archivo .dockerignore para excluir archivos y directorios que no se necesitan en la imagen Docker. Esto reduce el tamaño del contexto de construcción y mejora el rendimiento de la construcción.

Creemos un archivo .dockerignore:

nano .dockerignore

Agregue el siguiente contenido:

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

Creando una Estructura de Aplicación Más Compleja

Para aplicaciones más grandes con múltiples módulos, es importante estructurar su proyecto correctamente. Creemos un ejemplo un poco más complejo:

mkdir -p myapp

Cree un archivo de módulo:

nano myapp/__init__.py

Deje este archivo vacío (simplemente marca el directorio como un paquete Python).

Ahora cree un archivo de módulo con alguna funcionalidad:

nano myapp/utils.py

Agregue el siguiente código:

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

Ahora actualice nuestra aplicación principal para usar este módulo:

nano app.py

Reemplace el contenido con:

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

Construya y ejecute la aplicación actualizada:

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

Debería ver una salida que incluye nuestro mensaje personalizado:

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

Prácticas Adicionales Recomendadas

Aquí hay algunas prácticas adicionales recomendadas para evitar el ModuleNotFoundError en Docker:

  1. Entornos virtuales: Si bien no es estrictamente necesario en Docker (ya que los contenedores están aislados), el uso de entornos virtuales puede ayudar a garantizar la consistencia entre el desarrollo y la producción.

  2. Dependencias fijadas: Especifique siempre las versiones exactas de las dependencias para garantizar la consistencia en diferentes entornos.

  3. Construcciones de múltiples etapas: Para las imágenes de producción, considere usar construcciones de múltiples etapas para crear imágenes más pequeñas con solo las dependencias necesarias.

  4. Actualizaciones periódicas de dependencias: Actualice periódicamente sus dependencias para obtener correcciones de seguridad y mejoras.

Siguiendo estas mejores prácticas, minimizará las posibilidades de encontrar el ModuleNotFoundError en sus contenedores Docker y creará imágenes Docker más eficientes y mantenibles.

Resumen

En este laboratorio, aprendió a identificar, solucionar problemas y corregir el ModuleNotFoundError al trabajar con imágenes Docker para aplicaciones Python. Ha ganado experiencia práctica en:

  • Creación de una aplicación Python básica y su contenedorización con Docker
  • Comprensión de por qué ocurre el ModuleNotFoundError en entornos Docker
  • Solución de problemas de dependencias utilizando la instalación directa de paquetes y requirements.txt
  • Implementación de las mejores prácticas de Docker, como el almacenamiento en caché de capas adecuado y la estructura de archivos
  • Creación de una estructura de aplicación más compleja con múltiples módulos
  • Uso de .dockerignore para optimizar las construcciones de Docker

Estas habilidades le ayudarán a crear imágenes Docker más confiables y mantenibles para sus aplicaciones Python. Siguiendo las mejores prácticas cubiertas en este laboratorio, puede evitar errores comunes como ModuleNotFoundError y optimizar su flujo de trabajo de desarrollo de Docker.

Recuerde que la gestión adecuada de las dependencias es crucial cuando se trabaja con aplicaciones en contenedores. Asegúrese siempre de que sus imágenes Docker incluyan todas las dependencias necesarias, código estructurado correctamente y sigan las mejores prácticas para la eficiencia y el mantenimiento.