Dockerfile 中,把多个 RUN 合并在一起,能减少镜像尺寸吗?

时间:2024-10-17 07:01:32

先说结论:
有些时候能,有些时候不能,但你要明白原理

问题

看到一个 Dockerfile:

FROM python:3.17.7-alpine3.20
RUN pip3 install pillow
RUN pip3 install django
RUN pip3 install jieba
RUN pip3 install nltk
RUN pip3 install colormap

有人建议,把这几个 pip3 install 合并成一个 pip3 install -r requirements.txt,可以减小最终打包出来的镜像尺寸,真得是这样吗?

实验一:一次 pip 安装 vs 多次 pip 安装

多次 pip 安装

我们把上面这个最初始的 Dockerfile 打包出来的镜像起名为 temp:multi。
通过 docker image ls temp:multi 看到,这个包的大小为 396MB
然后,通过 docker history temp:multi 看到这个包的层级如下:

IMAGE          CREATED       CREATED BY                                      SIZE      COMMENT
a934e19243c7   2 hours ago   RUN /bin/sh -c pip3 install colormap   -i ht…   177MB     buildkit.dockerfile.v0
<missing>      2 hours ago   RUN /bin/sh -c pip3 install nltk       -i ht…   22.8MB    buildkit.dockerfile.v0
<missing>      2 hours ago   RUN /bin/sh -c pip3 install django     -i ht…   39.1MB    buildkit.dockerfile.v0
<missing>      2 hours ago   RUN /bin/sh -c pip3 install jieba      -i ht…   83.5MB    buildkit.dockerfile.v0
<missing>      2 hours ago   RUN /bin/sh -c pip3 install pillow     -i ht…   27.1MB    buildkit.dockerfile.v0
<missing>      2 weeks ago   CMD ["python3"]                                 0B        buildkit.dockerfile.v0
<missing>      2 weeks ago   RUN /bin/sh -c set -eux;  for src in idle3 p…   36B       buildkit.dockerfile.v0
<missing>      2 weeks ago   RUN /bin/sh -c set -eux;   apk add --no-cach…   38.1MB    buildkit.dockerfile.v0
<missing>      2 weeks ago   ENV PYTHON_VERSION=3.12.7                       0B        buildkit.dockerfile.v0
<missing>      2 weeks ago   ENV GPG_KEY=7169605F62C751356D054A26A821E680…   0B        buildkit.dockerfile.v0
<missing>      2 weeks ago   RUN /bin/sh -c set -eux;  apk add --no-cache…   999kB     buildkit.dockerfile.v0
<missing>      2 weeks ago   ENV LANG=C.UTF-8                                0B        buildkit.dockerfile.v0
<missing>      2 weeks ago   ENV PATH=/usr/local/bin:/usr/local/sbin:/usr…   0B        buildkit.dockerfile.v0
<missing>      5 weeks ago   /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B
<missing>      5 weeks ago   /bin/sh -c #(nop) ADD file:5758b97d8301c84a2…   7.8MB

可以看到,pip 安装的这些包,总大小应该在 350MB 左右,加上 python-alpine 原来46MB 的大小,整好是在 396 MB

一次 pip 安装

创建 requirements.txt 文件,用于 pip 集中安装:

-i https://pypi.tuna.tsinghua.edu.cn/simple
pillow
django
jieba
nltk
colormap

修改 Dockerfile

FROM python:3.12.7-alpine3.20
COPY requirements.txt /root/
RUN pip install -r /root/requirements.txt

使用 docker build 打包镜像 temp:one。 可以看到,temp:one 的大注也是 396 MB。
使用 docerk history temp:one 查看:

IMAGE          CREATED       CREATED BY                                      SIZE      COMMENT
a802510b2faa   2 hours ago   RUN /bin/sh -c pip3 install -r /home/require…   349MB     buildkit.dockerfile.v0
<missing>      2 hours ago   COPY requirements.txt /home/ # buildkit         79B       buildkit.dockerfile.v0
<missing>      2 weeks ago   CMD ["python3"]                                 0B        buildkit.dockerfile.v0
<missing>      2 weeks ago   RUN /bin/sh -c set -eux;  for src in idle3 p…   36B       buildkit.dockerfile.v0
<missing>      2 weeks ago   RUN /bin/sh -c set -eux;   apk add --no-cach…   38.1MB    buildkit.dockerfile.v0
<missing>      2 weeks ago   ENV PYTHON_VERSION=3.12.7                       0B        buildkit.dockerfile.v0
<missing>      2 weeks ago   ENV GPG_KEY=7169605F62C751356D054A26A821E680…   0B        buildkit.dockerfile.v0
<missing>      2 weeks ago   RUN /bin/sh -c set -eux;  apk add --no-cache…   999kB     buildkit.dockerfile.v0
<missing>      2 weeks ago   ENV LANG=C.UTF-8                                0B        buildkit.dockerfile.v0
<missing>      2 weeks ago   ENV PATH=/usr/local/bin:/usr/local/sbin:/usr…   0B        buildkit.dockerfile.v0
<missing>      5 weeks ago   /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B
<missing>      5 weeks ago   /bin/sh -c #(nop) ADD file:5758b97d8301c84a2…   7.8MB

可以看到,把所有 pip 安装一个 requirements 里安装,实际上并没有减小镜像包的尺寸

实验二:添加一个文件,然后删除

再看这个 Dockerfile

FROM alpine
COPY bigfile /home
RUN rm -f /home/bigfile

这里,我们基于 alpine 镜像,先往里面拷贝了一个 6.4 MB 的大文件,然后又把它给删除了。相当于什么都没做。
最理想的结果,是打包出来的镜像(起名为 temp:add_remove),大小和 alpine 差不多,也应该是 7.8MB 左右的样子。
但实际结果不是这样。
通过 docker image ls temp:add_remove 可以看到,镜像大小为 14.4MB
而用 docker history temp:add_remove 看,结果如下:

IMAGE          CREATED          CREATED BY                                      SIZE      COMMENT
81de752d816c   55 minutes ago   RUN /bin/sh -c rm -f /home/bigfile # buildkit   0B        buildkit.dockerfile.v0
<missing>      56 minutes ago   COPY bigfile /home/ # buildkit                  6.62MB    buildkit.dockerfile.v0
<missing>      47 hours ago     RUN /bin/sh -c adduser -D dot # buildkit        3.03kB    buildkit.dockerfile.v0
<missing>      5 weeks ago      /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B
<missing>      5 weeks ago      /bin/sh -c #(nop) ADD file:5758b97d8301c84a2…   7.8MB

可以看到,原来 COPY 的内容实际仍然在打包的镜像里面。为什么删除没有效果呢?

原因是 Docker 的 Union FS

Docker 使用 Union FS 来管理文件系统。
它允许将多个目录挂载到同一个挂载点上,这些目录在挂载点处表现为一个连贯的文件系统。
在 Docker 的上下文中,这意味着可以创建含多个只读层的堆叠,并在顶部添加一个可写层。
Union FS支持层叠多个目录,其中每个目录都可以被视为一个独立的层。
这允许 Docker 镜像由多个只读层组成,每个层代表一个 Dockerfile 指令的结果。

Docker 早期使用的是 AUFS,后来改为使用 overlay2。目前的 Linux 内核,决大多数都支持 oerverlay2。

Overlay2 的一个持点就是:写时复制(Copy-on-Write)
当容器尝试修改一个文件时,overlay2 会检查该文件是否存在于下面的只读层中。
如果是,overlay2 会在可写层创建该文件的副本并进行修改,保持原始只读层不变。

所以,每次 RUN 操作,实际上就是对原始记录加了一层。
如果两个动作如果没有重叠,就像用多个 pip install 不同的包,产生的多层和合在一个 pip 安装产生的一层的大小差不多。
但是,对于先添加,又删除,添加的那层文件始终是在的,只是之后又被删除动作在新的一层上标记为删除。

相当于一个本子上先写了一笔,然后又划掉了(而不是用橡皮擦掉),并不能使本子恢复空白。

最根本的原因,是 overlay2,除了最上层的读写层之外,底下的每一层都是只读的。