问题
首先来看一个例子,构建一个 C
语言版的 hello world
镜像:
1 2 3 4 5 int main () { puts ("Hello, world!" ); return 0 ; }
对应的 Dockerfile
为:
1 2 3 4 FROM gcc COPY hello.c . RUN gcc -o hello hello.c CMD ["./hello"]
然后执行 docker build -t hello-world .
构建一个名为 hello-world
的镜像,然而以这种方式构建的镜像的大小竟然有1.19 GB:
1 2 REPOSITORY TAG IMAGE ID CREATED SIZE hello-world latest b11e170bd1d2 6 minutes ago 1.19GB
因为这种构建方式生成的镜像会同时包含 gcc
镜像的内容,查看 gcc
镜像大小发现达到了1.19 GB:
1 2 REPOSITORY TAG IMAGE ID CREATED SIZE gcc latest 21f378ba43ec 11 days ago 1.19GB
如果我们把基础镜像换成 Ubuntu
并安装 gcc
编译 hello.c
重新构建镜像,最后的镜像大小为213 MB:
1 2 3 4 5 FROM ubuntu COPY hello.c . RUN apt-get update && apt-get install gcc -y RUN gcc -o hello hello.c CMD ["./hello"]
1 2 REPOSITORY TAG IMAGE ID CREATED SIZE hello-world latest 42f17d1d12a5 About a minute ago 213MB
虽然新镜像相比1.19 GB有大幅减少,但相比于 hello-world
程序本身的大小(17k)来说,213 MB依然是个庞大的数字:
1 2 $ ls hello -hl -rwxr-xr-x 1 root root 17K Aug 4 13:54 hello
解决
Multi-stage
对于 hello-world
这个镜像来说,我们真正需要的只是最终的可执行程序,而并不关心中间的编译过程,如果能将编译阶段作为一个临时阶段而并不包含在最终的镜像中,则可有效减少最终的镜像大小。针对此,Docker
在 17.05 版本开始提供了名为 multi-stage
构建的功能。我们将原来的 Dockerfile
稍作修改,将原来的编译阶段抽取为一个 stage
,然后将编译好的可执行文件复制到最终的 stage
中:
1 2 3 4 5 6 FROM gcc AS mybuildstage COPY hello.c . RUN gcc -o hello hello.c FROM ubuntu COPY --from=mybuildstage hello . CMD ["./hello"]
最终的镜像大小只有73.9 MB:
1 2 REPOSITORY TAG IMAGE ID CREATED SIZE hello-world latest 7dd2b51c53b2 7 minutes ago 73.9MB
FROM scratch
在上一步中,我们使用 Ubuntu
作为基础镜像来运行 hello-world
,相比于一个可执行程序,Ubuntu
依然过于庞大,有没有比 Ubuntu
更轻量的镜像呢?有,那就是 scratch
,这表示一个空的镜像,继续将 Dockerfile
稍作修改:
1 2 3 4 5 6 FROM gcc AS mybuildstage COPY hello.c . RUN gcc -o hello hello.c FROM scratch COPY --from=mybuildstage hello . CMD ["./hello"]
最终的镜像大小只有16.4 KB:
1 2 REPOSITORY TAG IMAGE ID CREATED SIZE hello-world latest 676253b0e9c4 31 minutes ago 16.4kB
不过在运行该镜像时却提示错误:
1 standard_init_linux.go:211: exec user process caused "no such file or directory"
这是因为这种方式构建出的镜像缺少 hello-world
运行时依赖的库。我们可以在编译 hello-world
时通过指定 -static
参数将依赖的库包含到最后的可执行文件中来解决这个问题:
1 2 3 4 5 6 FROM gcc AS mybuildstage COPY hello.c . RUN gcc -o hello hello.c -static FROM scratch COPY --from=mybuildstage hello . CMD ["./hello"]
不过包含了依赖的库后最终镜像的大小也上涨为945 KB:
1 2 REPOSITORY TAG IMAGE ID CREATED SIZE hello-world latest e6a1fccc2de7 9 seconds ago 945kB
另外,如果不想将依赖的库包含到最终的镜像中,可以使用 busybox:glibc
这个基础镜像,该镜像包含了 C
语言的标准库,有了这个镜像在编译 hello-world
时则无需指定 -static
参数:
1 2 3 4 5 6 FROM gcc AS mybuildstage COPY hello.c . RUN gcc -o hello hello.c FROM busybox:glibc COPY --from=mybuildstage hello . CMD ["./hello"]
不过由于该镜像本身有一定大小,最终镜像的大小达到了5.22 MB:
1 2 REPOSITORY TAG IMAGE ID CREATED SIZE hello-world latest e2f2c0544800 7 seconds ago 5.22MB
总结
通过 multi-stage
构建可以有效的减少 Docker
镜像的大小,而基础镜像的选择则要具体情况分析,在满足需求的情况下选择合理的基础镜像。
参考