先说结论:
有些时候能,有些时候不能,但你要明白原理 – Docker 使用 UnionFS,镜像尺寸随着层数增多,是单调非减的。
问题
看到一个 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 里安装,实际上并没有减小镜像包的尺寸。
使用 pip --no-cache-dir 实际能减少大小
在一次性 pip 安装的基础上,给 Dockerfile 的 RUN pip 命令加上 --no-cache-dir 的参数
FROM python:3.12.7-alpine3.20
COPY requirements.txt /root/
RUN pip install -r /root/requirements.txt
添加这个参数之后,最后生成的镜像大小为 300MB。
实验二:添加一个文件,然后删除
再看这个 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,除了最上层的读写层之外,底下的每一层都是只读的。
打个比方 – 就像钢笔写字,不能擦除
每一次 RUN 操作执行完成后,就生成一层。这层生成之后,就是只读的,不可修改的。
对于下一个操作,以前 RUN 的结果生成的各层,就象是一份份由钢笔书写的文件。
就算是我想修改以前的内容,也只能是做一个标记(就像用钢笔把以前某个文件上的某句话划掉),然后在最新的文件上加上要修改的内容。
总而言之,每新做一个操作,会添加一层。总体的镜像尺寸随着层数增多,是单调非减的。