如何修复构建 Docker 镜像时出现的 'ModuleNotFoundError'

DockerBeginner
立即练习

介绍

在为 Python 应用构建 Docker 镜像时,开发者经常会遇到 'ModuleNotFoundError' 消息。当 Python 无法找到你的应用所需的模块或包时,就会发生此错误。对于 Docker 初学者来说,这可能是一个特别具有挑战性的问题。

在这个实践实验(Lab)中,你将创建一个简单的 Python 应用,使用 Docker 将其容器化,遇到 ModuleNotFoundError,并学习解决它的实用方法。到最后,你将了解如何在 Docker 镜像中正确管理 Python 依赖项,并避免在你的项目中出现这个常见问题。

创建一个简单的 Python 应用

让我们创建一个基本的 Python 应用,并设置 Docker 来运行它。这将帮助你理解 ModuleNotFoundError 是如何在 Docker 环境中发生的。

理解 Python 应用结构

首先,让我们创建一个项目目录并导航到它:

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

现在,让我们创建一个导入第三方模块的简单 Python 应用。我们将创建两个文件:

  1. 一个主应用文件
  2. 一个 requirements 文件,用于列出依赖项

创建主应用文件:

nano app.py

将以下代码添加到 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()

这个简单的脚本使用 requests 库向 example.com 发送一个 HTTP 请求,并打印关于响应的一些基本信息。

现在,让我们创建一个 requirements 文件:

nano requirements.txt

将以下行添加到 requirements.txt 中:

requests==2.28.1

创建一个基本的 Dockerfile

现在,让我们创建一个简单的 Dockerfile,它将演示 ModuleNotFoundError:

nano Dockerfile

将以下内容添加到 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"]

这个 Dockerfile:

  • 使用 Python 3.9 slim 镜像作为基础
  • 将工作目录设置为 /app
  • 复制我们的应用文件
  • 指定运行我们应用的命令

请注意,我们故意没有复制 requirements.txt 文件或安装任何依赖项。这将导致我们在尝试运行容器时出现 ModuleNotFoundError。

构建和运行 Docker 镜像

让我们构建 Docker 镜像:

docker build -t python-app-error .

你应该看到类似这样的输出:

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

现在,让我们运行 Docker 容器:

docker run python-app-error

你应该看到类似这样的错误消息:

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

这就是我们在本实验(Lab)中关注的 ModuleNotFoundError。发生此错误的原因是我们没有在 Docker 镜像中包含所需的 requests 模块。

理解并修复 ModuleNotFoundError

现在我们已经遇到了 ModuleNotFoundError,让我们来理解它为什么发生以及如何修复它。

为什么 ModuleNotFoundError 会在 Docker 中发生?

ModuleNotFoundError 在 Docker 中发生的原因有几个常见原因:

  1. 缺少依赖项安装:我们没有在 Docker 镜像中安装所需的 Python 包。
  2. 错误的 PYTHONPATH:Python 解释器无法在预期位置找到模块。
  3. 文件结构问题:应用代码结构与导入方式不匹配。

在我们的例子中,错误发生的原因是我们没有在 Docker 镜像中安装 requests 包。与我们在本地开发环境中可能全局安装此包不同,Docker 容器是隔离的环境。

方法 1:在 Dockerfile 中使用 pip 安装依赖项

让我们修改我们的 Dockerfile 以安装所需的依赖项:

nano Dockerfile

使用以下内容更新 Dockerfile:

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

让我们构建并运行这个更新后的镜像:

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

你应该看到包含包安装的输出:

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

现在让我们运行修复后的容器:

docker run python-app-fixed-1

你应该看到类似这样的输出:

Status code: 200
Content length: 1256 characters

太棒了!该应用现在成功运行,因为我们安装了所需的依赖项。

方法 2:使用 requirements.txt 进行依赖项管理

虽然直接安装包有效,但使用 requirements.txt 文件进行更有组织的依赖项管理是更好的做法。让我们更新我们的 Dockerfile:

nano Dockerfile

使用以下内容更新 Dockerfile:

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

这种方法有几个优点:

  • 它将依赖项管理与代码分离
  • 它使更新依赖项更容易
  • 它遵循 Docker 镜像层缓存的最佳实践

让我们构建并运行这个更新后的镜像:

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

你应该看到与之前的构建类似的输出,但这次它使用 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

现在让我们运行容器:

docker run python-app-fixed-2

你应该看到相同的成功输出:

Status code: 200
Content length: 1256 characters

你已经成功地使用两种不同的方法修复了 ModuleNotFoundError!

避免 ModuleNotFoundError 的最佳实践

现在我们已经解决了眼前的问题,让我们看看一些最佳实践,以避免在 Docker 镜像中出现 ModuleNotFoundError。

理解 Docker 缓存以实现高效构建

Docker 使用分层方法来构建镜像。Dockerfile 中的每个指令都会创建一个新层。当你重建一个镜像时,Docker 会尽可能重用缓存层,这可以显著加快构建过程。

对于 Python 应用,你可以通过以下方式优化缓存:

  1. 在复制应用代码之前复制并安装 requirements
  2. 将经常更改的文件(如应用代码)保留在后面的层中

让我们更新我们的 Dockerfile 以遵循这些最佳实践:

nano Dockerfile

使用以下优化的内容更新 Dockerfile:

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

让我们构建这个优化的镜像:

docker build -t python-app-optimized .

并运行它以验证它是否有效:

docker run python-app-optimized

你应该看到相同的成功输出:

Status code: 200
Content length: 1256 characters

使用 .dockerignore 文件

为了使你的 Docker 构建更有效,使用 .dockerignore 文件来排除 Docker 镜像中不需要的文件和目录是一个好习惯。这可以减少构建上下文的大小并提高构建性能。

让我们创建一个 .dockerignore 文件:

nano .dockerignore

添加以下内容:

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

创建更复杂的应用结构

对于具有多个模块的大型应用,正确地构建你的项目非常重要。让我们创建一个稍微复杂一点的例子:

mkdir -p myapp

创建一个模块文件:

nano myapp/__init__.py

将此文件留空(它只是将目录标记为 Python 包)。

现在创建一个具有一些功能的模块文件:

nano myapp/utils.py

添加以下代码:

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

现在更新我们的主应用以使用此模块:

nano app.py

替换内容为:

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

构建并运行更新后的应用:

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

你应该看到包含我们自定义消息的输出:

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

额外的最佳实践

以下是一些避免在 Docker 中出现 ModuleNotFoundError 的额外最佳实践:

  1. 虚拟环境:虽然在 Docker 中并非绝对必要(因为容器是隔离的),但使用虚拟环境可以帮助确保开发和生产之间的一致性。

  2. 固定依赖项:始终指定依赖项的确切版本,以确保在不同环境中的一致性。

  3. 多阶段构建:对于生产镜像,考虑使用多阶段构建来创建仅包含必要依赖项的更小的镜像。

  4. 定期依赖项更新:定期更新你的依赖项以获取安全修复和改进。

通过遵循这些最佳实践,你将最大限度地减少在 Docker 容器中遇到 ModuleNotFoundError 的机会,并创建更高效、更易于维护的 Docker 镜像。

总结

在这个实验中,你学习了如何识别、排除故障和修复在使用 Docker 镜像进行 Python 应用开发时遇到的 ModuleNotFoundError。你获得了以下方面的实践经验:

  • 创建一个基本的 Python 应用并使用 Docker 进行容器化
  • 理解 ModuleNotFoundError 为什么会在 Docker 环境中发生
  • 使用直接包安装和 requirements.txt 修复依赖项问题
  • 实施 Docker 最佳实践,如正确的层缓存和文件结构
  • 创建具有多个模块的更复杂的应用结构
  • 使用 .dockerignore 来优化 Docker 构建

这些技能将帮助你为你的 Python 应用创建更可靠且易于维护的 Docker 镜像。通过遵循本实验中介绍的最佳实践,你可以避免常见的陷阱,如 ModuleNotFoundError,并优化你的 Docker 开发工作流程。

请记住,在使用容器化应用时,正确的依赖项管理至关重要。始终确保你的 Docker 镜像包含所有必要的依赖项、结构正确的代码,并遵循效率和可维护性的最佳实践。