为 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 映像时如何避免重新安装包?的主要内容,如果未能解决你的问题,请参考以下文章