将项目代码和相关文件引入 Docker 镜像,可以确保容器具备执行机器学习任务所需的一切,例如训练模型或运行推理服务。此步骤通常在 Dockerfile 中定义好基础环境和安装依赖之后进行。完成此操作的主要指令是 COPY 和 ADD。使用 COPY 指令COPY 指令是最直接、最常用的一种方法,用于将文件从本地机器(特别是构建上下文)复制到镜像文件系统中。它的语法很简单:COPY <src>... <dest><src>: 指定您要复制的本地机器上的文件或目录(相对于构建上下文根目录)。您可以指定多个源。支持通配符。<dest>: 指定源文件/目录应复制到容器镜像内部的路径。如果目标路径不存在,Docker 将创建它。如果源是目录,则目标路径必须以 / 结尾或是一个已存在的目录。对于一个典型的机器学习项目,结构上包含一个存放 Python 代码的 src 目录并且根目录下有一个 requirements.txt 文件,您可以像这样使用 COPY:# 定义工作目录(前面已介绍) WORKDIR /app # 先复制 requirements 文件以利用缓存 COPY requirements.txt . # 安装依赖(前面已介绍) RUN pip install --no-cache-dir -r requirements.txt # 复制项目其余代码 COPY ./src ./src在此示例中:我们将容器内的工作目录设置为 /app。我们将 requirements.txt 从构建上下文根目录复制到镜像内的 /app 目录。我们运行 pip install。我们将本地 src 目录的内容复制到镜像内 /app 目录下的一个名为 src 的目录中(即 /app/src)。这种顺序对于构建性能很重要,我们稍后在讨论构建缓存时会提到。理解 ADD 指令Docker 也提供了 ADD 指令,它的语法与 COPY 类似:ADD <src>... <dest>ADD 对于本地文件和目录执行与 COPY 相同的功能,但包含两个额外特性:URL 获取: 如果 <src> 是一个 URL,Docker 将从该 URL 下载文件并将其复制到 <dest>。权限设置为 600。归档文件自动解压: 如果 <src> 是一个识别出的压缩归档格式(例如 tar, gzip, bzip2, xz),Docker 将自动将其解压到 <dest> 中作为一个目录。尽管这些特性可能看起来很便利,但它们也可能使您的构建较难预测。例如,在构建过程中从 URL 下载会将镜像构建过程与网络可用性和远程源的稳定性耦合起来。自动解压有时会根据归档结构产生意外结果。建议: 为清晰和可预测性,对于传输本地文件和目录,优先选择 COPY 而非 ADD。仅在您特别需要其 URL 下载或自动解压功能时才使用 ADD,并理解潜在影响。对于下载文件,通常更好的做法是在 RUN 指令中使用 curl 或 wget 等工具,这样可以对下载过程有更多的控制(例如,错误处理、重试)。使用 .dockerignore 避免不需要的文件当您运行 docker build 时,Docker 客户端首先将指定为构建上下文的目录(通常是 .)打包并发送到 Docker 守护进程。此上下文包含所有文件和子目录。发送大型、不必要的文件(例如数据集、虚拟环境、Git 历史、IDE 配置)会减慢构建过程,如果意外复制,还可能使您的镜像变得臃肿。为防止这种情况,在您的构建上下文根目录(与 Dockerfile 相同的目录)中创建一个名为 .dockerignore 的文件。列出您要排除的文件或目录,使用类似于 .gitignore 的语法。这是一个典型机器学习项目的 .dockerignore 示例:# Git 文件 .git .gitignore # Python 虚拟环境 venv/ *.pyc __pycache__/ # IDE / 编辑器特定文件 .vscode/ .idea/ *.swp # 大型数据文件(稍后通过卷/挂载管理) data/ datasets/ # 模型文件(如果较大或单独管理) models/ *.pt *.h5 *.onnx *.pkl # Docker 文件 Dockerfile .dockerignore # 其他临时或本地文件 *.log notebooks/output/通过使用 .dockerignore,您可以确保只有必要的代码和配置文件被发送到守护进程,并通过 COPY 或 ADD 可用于复制到您的镜像中。这会带来更快的构建速度和更小、更安全的镜像。优化复制操作以实现构建缓存Docker 分层构建镜像。Dockerfile 中的每个指令(如 RUN, COPY, ADD)都会创建一个新层。Docker 会使用构建缓存:如果与 COPY 指令相关的文件自上次构建以来没有更改,并且之前的层也已缓存,Docker 会重用现有层,而不是再次执行该指令。这对于耗时步骤(如依赖安装)尤为重要。考虑以下两种方法:不太理想:WORKDIR /app COPY . . # 一次性复制所有内容 RUN pip install -r requirements.txt # ... Dockerfile 的其余部分如果您的项目中有任何文件发生更改(即使是微小的代码修改),COPY . . 层就会失效,Docker 必须重新运行可能耗时的 pip install 命令,即使 requirements.txt 没有更改。更优:WORKDIR /app # 1. 仅复制 requirements 文件 COPY requirements.txt . # 2. 安装依赖 RUN pip install --no-cache-dir -r requirements.txt # 3. 复制应用程序代码的其余部分 COPY . . # ... Dockerfile 的其余部分在这个改进版本中:如果只有您的 Python 代码(.py 文件)更改,Docker 会重用包括 RUN pip install 步骤在内的所有缓存层(因为 requirements.txt 没有更改)。它只需要重新执行最后的 COPY . . 指令,这非常快。只有当 requirements.txt 文件本身被修改时,依赖安装层才会被重新构建。经过周全考虑地组织 COPY 指令,将依赖定义与应用程序代码分开,可以显著加快迭代开发周期。处理模型文件和其他大型文件您应该将预训练模型、数据集或其他大型文件直接 COPY 到您的镜像中吗?优点: 创建一个自包含的镜像。容器启动时已包含这些文件。适用于运行时所需的小型、相对稳定的文件。缺点: 显著增加镜像大小。更新文件需要重建整个镜像。共享大型镜像效率低下。如果在镜像中分发专有模型/数据,可能违反许可。通常,对于数据集以及较大或频繁更新的模型文件,将它们直接复制到镜像中不是推荐的做法。使用 Docker 卷或绑定挂载的替代策略提供更大的灵活性和更高的效率。这些技术允许您将数据和文件与镜像本身分离,使更新更简便,并保持镜像更小。我们将在第 3 章“容器中的数据和模型管理”中详细介绍这些数据管理技术。目前请您理解,COPY 和 ADD 对于将您的应用程序代码和配置放入镜像不可或缺。使用 .dockerignore 排除不必要的文件,并组织您的 COPY 操作,以最大限度地发挥 Docker 构建缓存的优势。