如何减小 Docker 镜像的大小
问题
首先来看一个例子,构建一个 C
语言版的 hello world
镜像:
1 | /* hello.c */ |
对应的 Dockerfile
为:
1 | FROM gcc |
然后执行 docker build -t hello-world .
构建一个名为 hello-world
的镜像,然而以这种方式构建的镜像的大小竟然有1.19 GB:
1 | REPOSITORY TAG IMAGE ID CREATED SIZE |
因为这种构建方式生成的镜像会同时包含 gcc
镜像的内容,查看 gcc
镜像大小发现达到了1.19 GB:
1 | REPOSITORY TAG IMAGE ID CREATED SIZE |
如果我们把基础镜像换成 Ubuntu
并安装 gcc
编译 hello.c
重新构建镜像,最后的镜像大小为213 MB:
1 | FROM ubuntu |
1 | REPOSITORY TAG IMAGE ID CREATED SIZE |
虽然新镜像相比1.19 GB有大幅减少,但相比于 hello-world
程序本身的大小(17k)来说,213 MB依然是个庞大的数字:
1 | $ ls hello -hl |
解决
Multi-stage
对于 hello-world
这个镜像来说,我们真正需要的只是最终的可执行程序,而并不关心中间的编译过程,如果能将编译阶段作为一个临时阶段而并不包含在最终的镜像中,则可有效减少最终的镜像大小。针对此,Docker
在 17.05 版本开始提供了名为 multi-stage
构建的功能。我们将原来的 Dockerfile
稍作修改,将原来的编译阶段抽取为一个 stage
,然后将编译好的可执行文件复制到最终的 stage
中:
1 | FROM gcc AS mybuildstage |
最终的镜像大小只有73.9 MB:
1 | REPOSITORY TAG IMAGE ID CREATED SIZE |
FROM scratch
在上一步中,我们使用 Ubuntu
作为基础镜像来运行 hello-world
,相比于一个可执行程序,Ubuntu
依然过于庞大,有没有比 Ubuntu
更轻量的镜像呢?有,那就是 scratch
,这表示一个空的镜像,继续将 Dockerfile
稍作修改:
1 | FROM gcc AS mybuildstage |
最终的镜像大小只有16.4 KB:
1 | REPOSITORY TAG IMAGE ID CREATED SIZE |
不过在运行该镜像时却提示错误:
1 | standard_init_linux.go:211: exec user process caused "no such file or directory" |
这是因为这种方式构建出的镜像缺少 hello-world
运行时依赖的库。我们可以在编译 hello-world
时通过指定 -static
参数将依赖的库包含到最后的可执行文件中来解决这个问题:
1 | FROM gcc AS mybuildstage |
不过包含了依赖的库后最终镜像的大小也上涨为945 KB:
1 | REPOSITORY TAG IMAGE ID CREATED SIZE |
另外,如果不想将依赖的库包含到最终的镜像中,可以使用 busybox:glibc
这个基础镜像,该镜像包含了 C
语言的标准库,有了这个镜像在编译 hello-world
时则无需指定 -static
参数:
1 | FROM gcc AS mybuildstage |
不过由于该镜像本身有一定大小,最终镜像的大小达到了5.22 MB:
1 | REPOSITORY TAG IMAGE ID CREATED SIZE |
总结
通过 multi-stage
构建可以有效的减少 Docker
镜像的大小,而基础镜像的选择则要具体情况分析,在满足需求的情况下选择合理的基础镜像。