1. 概述

在构建 Python 应用的 Docker 镜像时,如果每次构建都要重新安装依赖包,不仅浪费时间,还会影响开发效率,特别是在网络较慢的地区。要解决这个问题,关键在于理解 Docker 的层缓存机制

本文将从一个简单项目出发,演示 Docker 是如何构建镜像的,为什么在代码未变时仍会重复安装依赖,并最终优化 Dockerfile,避免不必要的 pip install 操作。

2. 问题描述

目标: 当依赖包未发生变化时,避免每次构建 Docker 镜像时重新安装 Python 包。

2.1. 示例 Dockerfile(存在问题)

以下是一个典型的 Dockerfile:

FROM python:3.10-slim

WORKDIR /app
ADD . /app
RUN pip install -r requirements.txt

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

如果我们在本地修改了代码文件(比如修改了 app.py),再次构建镜像时,Docker 会重新执行 pip install,即使 requirements.txt 没有变化。

2.2. Docker 层缓存机制简介

Docker 构建镜像是按层进行的。每一行命令(如 COPY, RUN, ADD)都会生成一个新层。如果某一层的内容未变,Docker 会复用缓存,跳过执行。

问题出在 ADD . /app 这一步。它会将整个目录下的文件复制到镜像中,包括 requirements.txtapp.py。只要任意文件有变化,这一层就会失效,从而导致后续所有层(如 pip install)也被重新执行。

3. 重现问题

我们来构建一个简单的 Flask 项目来演示这个问题。

项目结构

flask-demo/
├── app.py
├── Dockerfile
└── requirements.txt

app.py

from flask import Flask

app = Flask(__name__)

@app.route("/")
def home():
    return "Hello, Docker!"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

requirements.txt

flask==2.3.2

Dockerfile(原始版本)

FROM python:3.10-slim

WORKDIR /app
ADD . /app
RUN pip install -r requirements.txt

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

构建镜像

docker build -t flask-demo .

首次构建时,pip install 会正常执行。

修改 app.py 中的返回值后再次构建:

docker build -t flask-demo .

你会发现 pip install 仍然被执行,即使 requirements.txt 没有任何变化。这就是缓存失效的问题。

4. 优化 Dockerfile:利用层缓存

优化后的 Dockerfile

FROM python:3.10-slim

WORKDIR /app

# 先复制依赖文件,以便利用缓存
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

# 再复制其余代码
COPY . .

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

优化说明

  • COPY requirements.txt ./:只复制依赖文件,确保只有当 requirements.txt 变化时才会触发 pip install
  • RUN pip install --no-cache-dir:禁用 pip 本地缓存,减少镜像体积
  • COPY . .:最后复制其余代码,不影响 pip install 的缓存

4.1. 验证优化效果

初始构建

docker build -t flask-demo .

输出显示 pip install 被执行。

修改 app.py 后再次构建

docker build -t flask-demo .

输出显示 pip install 被跳过,缓存生效 ✅

修改 requirements.txt 后构建

echo "requests==2.31.0" >> requirements.txt
docker build -t flask-demo .

此时 pip install 重新执行 ❗️

4.2. 使用 .dockerignore 文件

构建时,Docker 会将整个目录作为构建上下文发送给引擎,包括 .git.pyc 等无用文件。我们可以使用 .dockerignore 排除这些文件,提高构建效率。

# .dockerignore
__pycache__/
*.pyc
*.pyo
*.pyd
.env
.git

4.3. 固定依赖版本(推荐)

确保 requirements.txt 中的每个依赖都指定了版本号:

✅ 推荐写法:

flask==2.3.2
requests==2.31.0

❌ 不推荐写法:

flask
requests

如果依赖版本不固定,即使 requirements.txt 没变,远程包更新后也可能导致缓存失效 ❌

5. 总结

✅ 通过将 requirements.txt 的复制和安装步骤提前,我们可以有效利用 Docker 的层缓存机制,避免不必要的 pip install
✅ 使用 .dockerignore 减少构建上下文大小,提升构建速度。
✅ 固定依赖版本,确保构建结果可预测。

通过这些优化,可以显著提升 Python 项目在使用 Docker 构建镜像时的效率,尤其适用于频繁修改代码的开发场景。


原始标题:Skip Package Reinstallation for a Python Docker Image Build