likes
comments
collection
share

Docker多阶段构建:让你的镜像尽可能小

作者站长头像
站长
· 阅读数 14

我正在参加「掘金·启航计划」

前言

说到Docker,熟悉的人都知道用Docker容器来快速构建应用非常方便。但docker的使用有很多窍门,初学者肯定遇到过测试一个小项目而镜像文件非常大的问题,你估计纳闷了,我代码就两三行,为啥镜像这么大。今天就以一个简单的Python项目来说说Docker的多阶段构建知识。

首先,我们对镜像不做处理,来看看构建过程。

构建过程都放入一个Dockerfile

将所有构建过程都放在一个Dockerfile中,包含项目及其依赖库的编译、测试、打包等过程。这就会带来一个问题:

  • 镜像层次多,镜像体积大,部署时间长;
  • 源代码存在泄露的风险。

以一个Python flask项目为例,主要代码文件manage.py如下:

from flask import Flask
# import redis

app = Flask(__name__)

@app.route('/')
def index_func():
    return f"hello world"


if __name__ == '__main__':
    app.run()

代码的运行我们通过gunicorn来实现,gunicorn的配置代码如下:

worker = 4
worker_class = "gevent"
bind = "0.0.0.0:5000"

编写依赖文件requirements.txt:

Flask==2.0.3
gunicorn==20.1.0
gevent==21.12.0

最后编写Dockerfile:

FROM python:3.7
WORKDIR /myapp/

COPY requirements.txt ./
RUN python -m pip install --upgrade pip \
    && pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple

COPY . .

CMD [ "gunicorn", "manage:app", "-c", "./gunicorn_conf.py" ]

构建镜像:

docker build -t myapp .

构建后我们来看看镜像的信息,镜像的体积达到了900MB。也就是说你自己的代码只有不到1MB,但镜像达到了900MB。

$ docker images            
REPOSITORY               TAG       IMAGE ID       CREATED          SIZE
myapp                    latest    cd03a8d7cc8e   13 minutes ago   900MB

那有没有办法把镜像缩小呢?答案肯定是有的,那就是多阶段构建。我们先来看看Docker镜像分层原理。

Docker镜像分层原理

docker镜像其实是一层层的文件系统组成,文件系统基于UnionFS,每一层都在上一层的基础上构建。

使用docker image inspect <镜像名>可看出镜像的分层结构。

"RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:dd5b48ca5196c48f1a866eec959e88eb41d9aaf47b708e343e1b12802a421264",
                "sha256:fe09b9981fd21fd1aaa81e504536f550ce9d9f5c415f97cba5434de181481505",
                "sha256:cba7a92f211b56f4f2f1e7d2549bf3d913abfaecfae5b3b0cf96af17abcb5238",
                "sha256:ffd50287b468e69a2042645381db5c5650912ca89edb1ed89ee6ac7c4f2636ea",
                "sha256:fef6f293382ecc6a9ab8545dfc728d6edc56463921d7fda32a091aa6a891f65e",
                "sha256:dbf3773c055c4ec795c8c9b34bc352ae3f3d5c0cf3349913d212b92d0382bff3",
                "sha256:2fbc97bf885e6e66ecc91e63e648ffb81065b8056961749d21f134bd1c9517fd",
                "sha256:7bbc35c590816dc5d49b50abfa422a474510a931454ccff0303abdf75b2b8138",
                "sha256:5e35b3a4c86f78964922969cecad65dc548d1103b02d9136fbf703e307e52240",
                "sha256:5daa7c7d95ca7696ce462ffbd1821db2df119a1aede6ba244941569b0be6ef90",
                "sha256:04c818a2b1ea8c5c5305cbe513af28fae24f972549af6bbe25f1b5ecc589d0b2",
                "sha256:a52fd73441a31f615d60e9ba176df76b8680e6124e9bd76f459b86c36482aa53",
                "sha256:c99fdb7acd9fef32e25df9faf24b0a5557fb2ce625d6f752421c5ad30b49882c"
            ]
        },

回到Dockerfile上,比如我们的Python代码镜像。首先第一层是操作系统,第二层是Python环境和依赖,第三层为具体的Python代码。将这几个层打包起来就是一个镜像。

Dockerfile多阶段构建

在Docker 17.05版本前,如果你不想将所有构建过程都放在一个Dockerfile中,就得将镜像拆分,分散到多个Dockerfile中,最后在一个shell中构建多个Dockerfile。这种方式部署起来较复杂,这里不多介绍。主要来讲多阶段构建。

Docker 17.05版本之后支持多阶段构建,也就是可以在一个Dockerfile中将多个镜像的构建都放在一个Dockerfile中。

举个例子,在一个Dockerfile中构建:

  • 第一个镜像:安装编译器,并安装所需的虚拟环境。
  • 第二个镜像:复制第一个镜像已构建的环境,运行Python代码。

通过多阶段构建,可以得到更小的镜像文件。

先说说简单项目的多阶段构建方法,以Python项目为例,镜像的构建无非就是编译环境镜像+运行代码镜像。 其中运行代码的镜像大同小异,主要在于编译环境镜像的构建上。

Python项目的多阶段构建

方法一:用pip install --user构建编译环境

当使用pip install --user来安装依赖包时,安装的文件都会放在当前用户主目录的.local下,因此使用多阶段构建时,第一个镜像将环境构建在.local下,第二个镜像直接将.local复制过来用就行了。

FROM python:3.7-slim AS venv_image

COPY requirements.txt .

RUN pip install --user -r requirements.txt


FROM python:3.7-slim AS code_image

COPY --from=venv_image /root/.local /root/.local

ENV PATH=/root/.local/bin:$PATH

WORKDIR /myapp

COPY . /myapp

CMD [ "gunicorn", "manage:app", "-c", "./gunicorn_conf.py" ]

备注:

  • python:3.7-slim:使用-slim镜像,因Python3.7镜像包含了完整的构建工具,这里用不到,可用slim镜像。
  • --FROM:可从上一阶段的镜像复制文件,这里将.local复制过来了.

构建镜像:

docker build -t myapp_0 .

方法二:用virtualenv构建编译环境

注意看FROM ASCOPY --from在Dockerfile中的使用,直接看例子:

FROM python:3.7-slim AS venv_image

WORKDIR /py_venv

COPY requirements.txt .

ENV PATH="/py_venv/bin:$PATH"

RUN python -m venv --copies /py_venv \
    && pip install -r requirements.txt


FROM python:3.7-slim as code_image
ENV PATH=/py_venv/bin:$PATH
WORKDIR /myapp

COPY --from=venv_image /py_venv /py_venv

COPY . /myapp

CMD [ "gunicorn", "manage:app", "-c", "./gunicorn_conf.py" ]

备注:

  • --copies:加上这个参数, venv会将py_venv文件复制到虚拟环境.
  • --FROM:可从上一阶段的镜像复制文件,这里将虚拟环境文件/py_venv复制过来了.

这个Dockerfile很简单,分为2个镜像。第一个镜像是构建Python编译环境以及安装依赖包。第二个镜像是将第一个镜像的虚拟环境复制过来,然后运行代码。

docker build -t myapp_1 .
两种构建方式的对比

构建镜像后,我们来看看多阶段构建Dockerfile和普通Dockerfile的大小区别:

# ethans @ ethan-mac in ~ [15:27:24]
$ docker images
REPOSITORY               TAG       IMAGE ID       CREATED             SIZE
myapp                    latest    cd03a8d7cc8e   About an hour ago   900MB
myapp_0                  latest    91fdb92da04e   26 seconds ago      138MB
myapp_1                  latest    337010eb02e6   About an hour ago   149MB

可以看出,通过多阶段构建的操作后,镜像大小从900MB降到了149MB和138MB,差距非常大。

考量到用pip install --user的方式会和系统安装的Python包共存,环境包不是一个隔离目录,推荐使用virtualenv方式。

小结

本文讲述了普通构建和多阶段构建的区别,在我们用Docker部署项目时,功能需求完成后,可用多阶段构建的方法,把我们的镜像尽可能缩小,节省空间资源。