如何使用多阶段构建来减小Python(Docker)镜像大小?

49

我正在寻找一种使用Python和Dockerfile创建多阶段构建的方法:

例如,使用以下镜像:

第一个镜像:安装所有编译时依赖项,并安装所有需要的Python模块

第二个镜像:将从第一个镜像编译/构建的所有软件包复制到第二个镜像中,但不包括编译器本身(gcc,postgers-dev,python-dev等)

最终目标是获得更小的镜像,运行我所需的Python和Python软件包。

简而言之:我如何"包装"所有已在第一个镜像中创建的编译模块(site-packages /外部库),并以"清洁的方式"将它们复制到第二个镜像中。


你解决了这个问题吗?我也需要做同样的事情。 - ealeon
4个回答

50

好的,我的解决方案是使用wheel,它让我们可以在第一张镜像上进行编译,为所有依赖项创建wheel文件并在第二张镜像中安装它们,而无需安装编译器。

FROM python:2.7-alpine as base

RUN mkdir /svc
COPY . /svc
WORKDIR /svc

RUN apk add --update \
    postgresql-dev \
    gcc \
    musl-dev \
    linux-headers

RUN pip install wheel && pip wheel . --wheel-dir=/svc/wheels

FROM python:2.7-alpine

COPY --from=base /svc /svc

WORKDIR /svc

RUN pip install --no-index --find-links=/svc/wheels -r requirements.txt

您可以在以下博客文章中查看我关于此问题的答案:

https://www.blogfoobar.com/post/2018/02/10/python-and-docker-multistage-build


4
非常有帮助!不过结果中仍然包含了轮子文件,除了它们已经安装好的形式。我猜使用Rocker是正确的解决方案,因为Docker在构建过程中拒绝给我们挂载。 - remram
1
重要提示 - 请使用/srv/wheels或您复制wheels到的任何其他目录,而不是/shadow_reporting/wheels。 - zeldigas
@gCoh 抱歉如果我的问题很简单,但requirements.txt文件是什么? - philippos
1
@philippos https://learnpython.com/blog/python-requirements-file/@philippos https://learnpython.com/blog/python-requirements-file/ - gCoh
这可能是比使用 venv 方法更好的解决方案,后者需要更新/重新创建环境变量。 - ReenigneArcher
在你执行 pip install ... requirements.txt 之后,是否安全执行 rm -rf /svc/wheels/ 呢? - Christopher Markieta

17
我推荐阅读这篇文章中详细的方法(第二部分)。作者使用virtualenv,让pip install把所有的Python代码、二进制文件等存储在一个文件夹下,而不是分散在整个文件系统中。这样可以很容易地将这个文件夹复制到最终的“生产”镜像中。总结如下:
编译镜像
  • 在您选择的某个路径中激活virtualenv。
  • 将该路径添加到docker的ENV中。这是virtualenv为未来的docker RUN和CMD操作提供所需的全部功能。
  • 像往常一样安装系统开发包和pip install xyz
生产镜像
  • 从编译镜像中复制virtualenv文件夹。
  • 将virtualenv文件夹添加到docker的PATH中。

我一直在尝试您提到的完全相同的示例,但是我一直收到以下错误:调用远程方法“docker-start-container”时出错:错误:(HTTP代码400)意外-无法创建shim任务:OCI运行时创建失败:runc创建失败:无法启动容器进程:exec:“myapp”:在$PATH中找不到可执行文件:未知。 - philippos
@philippos 我不确定是什么原因导致了这个问题,但我建议你开一个新的SO帖子并提供更多细节,假设你在搜索中找不到任何有用的信息。我认为这与我们在此页面上尝试解决的特定问题无关。 - mpoisot
这种方法有一个需要注意的地方。如果您之前已经部署了该镜像并尝试更新它,则需要删除 PATH 环境变量并重新创建镜像。我使用的是 portainer,它允许您仅删除该变量。我猜在 Docker 中,您需要删除并重新创建整个容器(不确定)。 - ReenigneArcher

5
这是一个使用 Python 虚拟环境在 Docker 中的有用场所。通常复制虚拟环境比较棘手,因为它需要与完全相同的 Python 构建和文件系统路径匹配,但在 Docker 中可以保证这一点。
(这与 @mpoisot 在 他们的回答 中描述的基本配方相同,在其他 SO 答案中也有出现。)
假设您正在安装 PostgreSQL 客户端库 psycopg。其扩展形式需要 Python C 开发库以及 PostgreSQL C 客户端库头文件;但要运行它,您只需要 PostgreSQL C 运行时库。因此,您可以使用 多阶段构建:第一阶段使用完整的 C 工具链安装虚拟环境,而最终阶段则复制已构建的虚拟环境,但仅包括最少所需的库。
典型的 Dockerfile 可以如下所示:
# Name the single Python image we're using everywhere.
ARG python=python:3.10-slim

# Build stage:
FROM ${python} AS build

# Install a full C toolchain and C build-time dependencies for
# everything we're going to need.
RUN apt-get update \
 && DEBIAN_FRONTEND=noninteractive \
    apt-get install --no-install-recommends --assume-yes \
      build-essential \
      libpq-dev

# Create the virtual environment.
RUN python3 -m venv /venv
ENV PATH=/venv/bin:$PATH

# Install the Python library dependencies, including those with
# C extensions.  They'll get installed into the virtual environment.
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt

# Final stage:
FROM ${python}

# Install the runtime-only C library dependencies we need.
RUN apt-get update \
 && DEBIAN_FRONTEND=noninteractive \
    apt-get install --no-install-recommends --assume-yes \
      libpq5

# Copy the virtual environment from the first stage.
COPY --from=build /venv /venv
ENV PATH=/venv/bin:$PATH

# Copy the application in.
COPY . .
CMD ["./main.py"]

如果您的应用程序使用Python入口点脚本,那么您可以在第一阶段完成所有操作:RUN pip install .将应用程序复制到虚拟环境中,并为您创建一个包装器脚本/venv/bin。在最后阶段,您无需再次COPY应用程序。将CMD设置为从虚拟环境运行包装器脚本,该虚拟环境已经位于$PATH的前面。

再次注意,这种方法之所以有效,是因为两个阶段都使用了相同的Python基础镜像,并且虚拟环境位于完全相同的路径上。如果是不同的Python或不同的容器路径,则移植的虚拟环境可能无法正常工作。


谢谢,我通过删除gcc和build-essential减少了300MB。我的镜像从866MB减少到了555MB。 - nsantana

-19

这份文档详细解释了如何完成此操作。

https://docs.docker.com/engine/userguide/eng-image/multistage-build/#before-multi-stage-builds

基本上你就像你说的那样做。然而,多阶段构建功能的神奇之处在于,你可以从一个Dockerfile中完成所有这些。
比如:
FROM golang:1.7.3
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html  
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"]  

这将构建一个 Go 二进制文件,然后下一个镜像运行该二进制文件。第一个镜像具有所有构建工具,而第二个镜像只是一个可以运行二进制文件的基本 Linux 机器。


6
这是一个通用解决方案,而不是针对Python的特定解决方案。 - gCoh
将其适应您的需求...获取Linux映像,在其中安装PostgreSQL或任何构建工具等等。这将为您提供所请求的轻量级最终映像,然后您需要使用适合您的打包系统。https://docs.python.org/3/library/zipapp.html - Pandelis
11
那么你需要从第一张图片复制什么内容到第二张图片中呢?这就是问题所在。"根据你的需求进行调整"并没有起到任何帮助。 - ealeon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接