为 Python 项目构建 Docker 映像时如何避免重新安装包?

Posted

技术标签:

【中文标题】为 Python 项目构建 Docker 映像时如何避免重新安装包?【英文标题】:How to avoid reinstalling packages when building Docker image for Python projects? 【发布时间】:2014-10-07 23:18:47 【问题描述】:

我的 Dockerfile 类似于

FROM my/base

ADD . /srv
RUN pip install -r requirements.txt
RUN python setup.py install

ENTRYPOINT ["run_server"]

每次构建新映像时,都必须重新安装依赖项,这在我所在的地区可能会非常慢。

对于已安装的cache 软件包,我想到的一种方法是用更新的图像覆盖my/base 图像,如下所示:

docker build -t new_image_1 .
docker tag new_image_1 my/base

所以下次我用这个 Dockerfile 构建时,my/base 已经安装了一些包。

但是这个解决方案有两个问题:

    并非总是可以覆盖基本映像 随着新图像的叠加,基础图像会变得越来越大

那么我可以使用什么更好的解决方案来解决这个问题?

编辑:

关于我机器上 docker 的一些信息:

☁  test  docker version
Client version: 1.1.2
Client API version: 1.13
Go version (client): go1.2.1
Git commit (client): d84a070
Server version: 1.1.2
Server API version: 1.13
Go version (server): go1.2.1
Git commit (server): d84a070
☁  test  docker info
Containers: 0
Images: 56
Storage Driver: aufs
 Root Dir: /var/lib/docker/aufs
 Dirs: 56
Execution Driver: native-0.2
Kernel Version: 3.13.0-29-generic
WARNING: No swap limit support

【问题讨论】:

构建完镜像后是否删除中间镜像? 当然不是,但这无关紧要,因为我重建镜像的时候,还是基于原来的my/base 【参考方案1】:

我知道这个问题已经有一些流行的答案。但是有一种更新的方法可以为包管理器缓存文件。我认为将来当 BuildKit 变得更加标准时,这可能是一个很好的答案。

从 Docker 18.09 开始,对 BuildKit 提供实验性支持。 BuildKit 增加了对 Dockerfile 中一些新特性的支持,包括 experimental support for mounting external volumes 到 RUN 步骤中。这使我们可以为 $HOME/.cache/pip/ 之类的内容创建缓存。

我们将使用以下requirements.txt 文件作为示例:

Click==7.0
Django==2.2.3
django-appconf==1.0.3
django-compressor==2.3
django-debug-toolbar==2.0
django-filter==2.2.0
django-reversion==3.0.4
django-rq==2.1.0
pytz==2019.1
rcssmin==1.0.6
redis==3.3.4
rjsmin==1.1.0
rq==1.1.0
six==1.12.0
sqlparse==0.3.0

Python Dockerfile 的典型示例可能如下所示:

FROM python:3.7
WORKDIR /usr/src/app
COPY requirements.txt /usr/src/app/
RUN pip install -r requirements.txt
COPY . /usr/src/app

使用 DOCKER_BUILDKIT 环境变量启用 BuildKit 后,我​​们可以在大约 65 秒内构建未缓存的 pip 步骤:

$ export DOCKER_BUILDKIT=1
$ docker build -t test .
[+] Building 65.6s (10/10) FINISHED                                                                                                                                             
 => [internal] load .dockerignore                                                                                                                                          0.0s
 => => transferring context: 2B                                                                                                                                            0.0s
 => [internal] load build definition from Dockerfile                                                                                                                       0.0s
 => => transferring dockerfile: 120B                                                                                                                                       0.0s
 => [internal] load metadata for docker.io/library/python:3.7                                                                                                              0.5s
 => CACHED [1/4] FROM docker.io/library/python:3.7@sha256:6eaf19442c358afc24834a6b17a3728a45c129de7703d8583392a138ecbdb092                                                 0.0s
 => [internal] load build context                                                                                                                                          0.6s
 => => transferring context: 899.99kB                                                                                                                                      0.6s
 => CACHED [internal] helper image for file operations                                                                                                                     0.0s
 => [2/4] COPY requirements.txt /usr/src/app/                                                                                                                              0.5s
 => [3/4] RUN pip install -r requirements.txt                                                                                                                             61.3s
 => [4/4] COPY . /usr/src/app                                                                                                                                              1.3s
 => exporting to image                                                                                                                                                     1.2s
 => => exporting layers                                                                                                                                                    1.2s
 => => writing image sha256:d66a2720e81530029bf1c2cb98fb3aee0cffc2f4ea2aa2a0760a30fb718d7f83                                                                               0.0s
 => => naming to docker.io/library/test                                                                                                                                    0.0s

现在,让我们添加实验标头并修改RUN 步骤以缓存 Python 包:

# syntax=docker/dockerfile:experimental

FROM python:3.7
WORKDIR /usr/src/app
COPY requirements.txt /usr/src/app/
RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt
COPY . /usr/src/app

现在继续进行另一个构建。它应该花费相同的时间。但这次它在我们的新缓存挂载中缓存 Python 包:

$ docker build -t pythontest .
[+] Building 60.3s (14/14) FINISHED                                                                                                                                             
 => [internal] load build definition from Dockerfile                                                                                                                       0.0s
 => => transferring dockerfile: 120B                                                                                                                                       0.0s
 => [internal] load .dockerignore                                                                                                                                          0.0s
 => => transferring context: 2B                                                                                                                                            0.0s
 => resolve image config for docker.io/docker/dockerfile:experimental                                                                                                      0.5s
 => CACHED docker-image://docker.io/docker/dockerfile:experimental@sha256:9022e911101f01b2854c7a4b2c77f524b998891941da55208e71c0335e6e82c3                                 0.0s
 => [internal] load .dockerignore                                                                                                                                          0.0s
 => [internal] load build definition from Dockerfile                                                                                                                       0.0s
 => => transferring dockerfile: 120B                                                                                                                                       0.0s
 => [internal] load metadata for docker.io/library/python:3.7                                                                                                              0.5s
 => CACHED [1/4] FROM docker.io/library/python:3.7@sha256:6eaf19442c358afc24834a6b17a3728a45c129de7703d8583392a138ecbdb092                                                 0.0s
 => [internal] load build context                                                                                                                                          0.7s
 => => transferring context: 899.99kB                                                                                                                                      0.6s
 => CACHED [internal] helper image for file operations                                                                                                                     0.0s
 => [2/4] COPY requirements.txt /usr/src/app/                                                                                                                              0.6s
 => [3/4] RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt                                                                                  53.3s
 => [4/4] COPY . /usr/src/app                                                                                                                                              2.6s
 => exporting to image                                                                                                                                                     1.2s
 => => exporting layers                                                                                                                                                    1.2s
 => => writing image sha256:0b035548712c1c9e1c80d4a86169c5c1f9e94437e124ea09e90aea82f45c2afc                                                                               0.0s
 => => naming to docker.io/library/test                                                                                                                                    0.0s

大约 60 秒。类似于我们的第一个构建。

requirements.txt 做一个小改动(例如在两个包之间添加一个新行)以强制缓存失效并再次运行:

$ docker build -t pythontest .
[+] Building 15.9s (14/14) FINISHED                                                                                                                                             
 => [internal] load build definition from Dockerfile                                                                                                                       0.0s
 => => transferring dockerfile: 120B                                                                                                                                       0.0s
 => [internal] load .dockerignore                                                                                                                                          0.0s
 => => transferring context: 2B                                                                                                                                            0.0s
 => resolve image config for docker.io/docker/dockerfile:experimental                                                                                                      1.1s
 => CACHED docker-image://docker.io/docker/dockerfile:experimental@sha256:9022e911101f01b2854c7a4b2c77f524b998891941da55208e71c0335e6e82c3                                 0.0s
 => [internal] load build definition from Dockerfile                                                                                                                       0.0s
 => => transferring dockerfile: 120B                                                                                                                                       0.0s
 => [internal] load .dockerignore                                                                                                                                          0.0s
 => [internal] load metadata for docker.io/library/python:3.7                                                                                                              0.5s
 => CACHED [1/4] FROM docker.io/library/python:3.7@sha256:6eaf19442c358afc24834a6b17a3728a45c129de7703d8583392a138ecbdb092                                                 0.0s
 => CACHED [internal] helper image for file operations                                                                                                                     0.0s
 => [internal] load build context                                                                                                                                          0.7s
 => => transferring context: 899.99kB                                                                                                                                      0.7s
 => [2/4] COPY requirements.txt /usr/src/app/                                                                                                                              0.6s
 => [3/4] RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt                                                                                   8.8s
 => [4/4] COPY . /usr/src/app                                                                                                                                              2.1s
 => exporting to image                                                                                                                                                     1.1s
 => => exporting layers                                                                                                                                                    1.1s
 => => writing image sha256:fc84cd45482a70e8de48bfd6489e5421532c2dd02aaa3e1e49a290a3dfb9df7c                                                                               0.0s
 => => naming to docker.io/library/test                                                                                                                                    0.0s

只有大约 16 秒!

我们正在获得这种加速,因为我们不再下载所有 Python 包。它们被包管理器(在这种情况下为pip)缓存并存储在缓存卷挂载中。卷安装提供给运行步骤,以便pip 可以重用我们已经下载的包。 这发生在任何 Docker 层缓存之外

更大的requirements.txt 的收益应该会更好。

注意事项:

这是实验性的 Dockerfile 语法,应该这样对待。您目前可能不想在生产环境中使用它进行构建。 BuildKit 的东西目前在 Docker Compose 或其他直接使用 Docker API 的工具下不工作。现在 Docker Compose 从 1.25.0 开始支持这一点。见How do you enable BuildKit with docker-compose? 目前没有用于管理缓存的直接接口。当您执行 docker system prune -a 时,它会被清除。

希望这些功能能够在 Docker 中进行构建,并且 BuildKit 将成为默认设置。如果/当这种情况发生时,我会尝试更新这个答案。

【讨论】:

我可以确认这个解决方案效果很好。我的构建时间从 1 多分钟下降到 2.2 秒。谢谢@andy-shinn。 现在也是 Docker-Compose:***.com/questions/58592259/… 注意:如果你使用SUDO来运行docker,你可能需要这样做:sudo DOCKER_BUILDKIT=1 ... 我收到此错误:- 无法使用前端 dockerfile.v0 解决:未能创建 LLB 定义:Dockerfile 解析错误第 10 行:未知标志:挂载 这听起来像是你错过了Dockerfile 顶部的评论,或者 Docker 版本太旧了。我会用你的所有调试信息创建一个新问题。【参考方案2】:
pipenv install

默认情况下会尝试重新锁定。这样做时,不会使用 Docker 构建的缓存层,因为 Pipfile.lock 已更改。 See the docs

对此的解决方案是版本 Pipfile.lock 并使用

RUN pipenv sync

改为。

感谢 JFG Piñeiro。

【讨论】:

【参考方案3】:

尝试构建一个看起来像这样的 Dockerfile:

FROM my/base

WORKDIR /srv
ADD ./requirements.txt /srv/requirements.txt
RUN pip install -r requirements.txt
ADD . /srv
RUN python setup.py install

ENTRYPOINT ["run_server"]

只要您不对requirements.txt 进行任何更改,Docker 将在 pip 安装期间使用缓存,无论. 的其他代码文件是否已更改。这是一个例子。


这是一个简单的Hello, World! 程序:

$ tree
.
├── Dockerfile
├── requirements.txt
└── run.py   

0 directories, 3 file

# Dockerfile

FROM dockerfile/python
WORKDIR /srv
ADD ./requirements.txt /srv/requirements.txt
RUN pip install -r requirements.txt
ADD . /srv
CMD python /srv/run.py

# requirements.txt
pytest==2.3.4

# run.py
print("Hello, World")

docker build 的输出:

Step 1 : WORKDIR /srv
---> Running in 22d725d22e10
---> 55768a00fd94
Removing intermediate container 22d725d22e10
Step 2 : ADD ./requirements.txt /srv/requirements.txt
---> 968a7c3a4483
Removing intermediate container 5f4e01f290fd
Step 3 : RUN pip install -r requirements.txt
---> Running in 08188205e92b
Downloading/unpacking pytest==2.3.4 (from -r requirements.txt (line 1))
  Running setup.py (path:/tmp/pip_build_root/pytest/setup.py) egg_info for package pytest
....
Cleaning up...
---> bf5c154b87c9
Removing intermediate container 08188205e92b
Step 4 : ADD . /srv
---> 3002a3a67e72
Removing intermediate container 83defd1851d0
Step 5 : CMD python /srv/run.py
---> Running in 11e69b887341
---> 5c0e7e3726d6
Removing intermediate container 11e69b887341
Successfully built 5c0e7e3726d6

让我们修改run.py

# run.py
print("Hello, Python")

再次尝试构建,输出如下:

Sending build context to Docker daemon  5.12 kB
Sending build context to Docker daemon 
Step 0 : FROM dockerfile/python
---> f86d6993fc7b
Step 1 : WORKDIR /srv
---> Using cache
---> 55768a00fd94
Step 2 : ADD ./requirements.txt /srv/requirements.txt
---> Using cache
---> 968a7c3a4483
Step 3 : RUN pip install -r requirements.txt
---> Using cache
---> bf5c154b87c9
Step 4 : ADD . /srv
---> 9cc7508034d6
Removing intermediate container 0d7cf71eb05e
Step 5 : CMD python /srv/run.py
---> Running in f25c21135010
---> 4ffab7bc66c7
Removing intermediate container f25c21135010
Successfully built 4ffab7bc66c7

正如你在上面看到的,这一次 docker 在构建过程中使用了缓存。现在,让我们更新requirements.txt

# requirements.txt

pytest==2.3.4
ipython

下面是 docker build 的输出:

Sending build context to Docker daemon  5.12 kB
Sending build context to Docker daemon 
Step 0 : FROM dockerfile/python
---> f86d6993fc7b
Step 1 : WORKDIR /srv
---> Using cache
---> 55768a00fd94
Step 2 : ADD ./requirements.txt /srv/requirements.txt
---> b6c19f0643b5
Removing intermediate container a4d9cb37dff0
Step 3 : RUN pip install -r requirements.txt
---> Running in 4b7a85a64c33
Downloading/unpacking pytest==2.3.4 (from -r requirements.txt (line 1))
  Running setup.py (path:/tmp/pip_build_root/pytest/setup.py) egg_info for package pytest

Downloading/unpacking ipython (from -r requirements.txt (line 2))
Downloading/unpacking py>=1.4.12 (from pytest==2.3.4->-r requirements.txt (line 1))
  Running setup.py (path:/tmp/pip_build_root/py/setup.py) egg_info for package py

Installing collected packages: pytest, ipython, py
  Running setup.py install for pytest

Installing py.test script to /usr/local/bin
Installing py.test-2.7 script to /usr/local/bin
  Running setup.py install for py

Successfully installed pytest ipython py
Cleaning up...
---> 23a1af3df8ed
Removing intermediate container 4b7a85a64c33
Step 4 : ADD . /srv
---> d8ae270eca35
Removing intermediate container 7f003ebc3179
Step 5 : CMD python /srv/run.py
---> Running in 510359cf9e12
---> e42fc9121a77
Removing intermediate container 510359cf9e12
Successfully built e42fc9121a77

请注意 docker 在 pip 安装期间如何不使用缓存。如果它不起作用,请检查您的 docker 版本。

Client version: 1.1.2
Client API version: 1.13
Go version (client): go1.2.1
Git commit (client): d84a070
Server version: 1.1.2
Server API version: 1.13
Go version (server): go1.2.1
Git commit (server): d84a070

【讨论】:

这似乎不起作用,因为每当 docker 看到 ADD 指令时,缓存就会失效。 我不知道为什么它不起作用。但是requirements.txt(ADD ./requirements.txt /srv/requirements.txt上的)没有任何变化,那么docker必须使用缓存。请参阅 Dockerfile 文档中的 add seciton。 是的,如果 requirements.txt 没有改变,它将使用缓存。但是如果 requirements.txt 发生变化,那么所有的需求都会被下载。有什么方法可以将 pip 缓存卷挂载到 docker 容器中以从缓存中加载? 这个答案的关键是在运行 pip (RUN pip install -r requirements.txt) 之前添加 requirements.txt (ADD requirements.txt /srv),并在运行之后添加所有其他文件 pip. 因此,它们应该按以下顺序排列:(1)ADD requirements.txt /srv;(2)RUN pip install -r requirements.txt;(3)ADD . /srv 请注意,当使用 COPY 而不是 ADD 时,这不起作用【参考方案4】:

为了尽量减少网络活动,您可以将pip 指向主机上的缓存目录。

运行 docker 容器,并将主机的 pip 缓存目录绑定安装到容器的 pip 缓存目录中。 docker run 命令应如下所示:

docker run -v $HOME/.cache/pip-docker/:/root/.cache/pip image_1

然后在您的 Dockerfile 中将您的要求安装为ENTRYPOINT 语句(或CMD 语句)的一部分,而不是作为RUN 命令的一部分。这很重要,因为(如 cmets 中所指出的)在构建映像期间(执行 RUN 语句时)安装不可用。 Docker 文件应如下所示:

FROM my/base

ADD . /srv

ENTRYPOINT ["sh", "-c", "pip install -r requirements.txt && python setup.py install && run_server"]

【讨论】:

不是 OP 在他的用例中寻找的东西,但如果您制作构建服务器,这是个好主意 这似乎是一个问题的根源,尤其是指向默认主机缓存的建议。您可能正在混合特定于架构的软件包。 @GiacomoLacava 谢谢,这是一个很好的观点。我调整了答案并删除了建议使用重用主机缓存目录的部分。【参考方案5】:

我发现一个更好的方法是将 Python 站点包目录添加为一个卷。

services:
    web:
        build: .
        command: python manage.py runserver 0.0.0.0:8000
        volumes:
            - .:/code
            -  /usr/local/lib/python2.7/site-packages/

这样我可以直接 pip 安装新库,而无需完全重建。

编辑:忽略这个答案,jkukul's 上面的答案对我有用。我的意图是缓存 site-packages 文件夹。那看起来更像:

volumes:
   - .:/code
   - ./cached-packages:/usr/local/lib/python2.7/site-packages/

缓存下载文件夹更干净。这也缓存了***,因此它可以正确地完成任务。

【讨论】:

当你尝试在另一台机器上构建这个 dockerfile 时会发生什么。这不是一个可持续的解决方案。 真的很困惑,结果证明这是错误的,我真的不知道为什么。能否提供更多细节。 您的 docker 映像取决于主机系统的状态。这使 docker 的大部分实用程序无效。图像需要的所有东西都应该安装在其中。使用 Dockerfile 安装所有依赖项。如果您想避免每次从 jkukul 构建答案以挂载 pip 缓存时都重新下载软件包。 灯泡刚刚熄灭,谢谢。我实际上是在尝试从 VM 而非主机上挂载 site-packages 目录。相当疏忽。我认为在精神上我试图按照 jkulkul 的建议做同样的事情。感谢您的澄清! @AaronMcMillin 他实际上并不依赖于主机上的路径。他正在将容器中的站点包安装到匿名卷。不过还是个坏主意

以上是关于为 Python 项目构建 Docker 映像时如何避免重新安装包?的主要内容,如果未能解决你的问题,请参考以下文章

从多项目点网核心解决方案构建 Docker 映像

如何在具有纱线工作空间的 monorepo 中从 nodejs 项目构建 docker 映像

Docker在映像构建中不接受pyOBDC

使用 AWS CDK 创建用于构建 Docker 映像的 CodeBuild 项目

如何在 docker 映像中安装 python 模块?

如何构建结合了 Angular 和 Spring Boot 应用程序的 Docker 映像?