针对如何制作尽量小的镜像问题,笔者在这里介绍一种用” 空镜像 “制作自定义最小镜像的方案。
1. 从石头缝里蹦出来的基础镜像?
这里请大家思考一个问题:docker hub 上的那些基础镜像,如 nginx、alpine、ubuntu,都是怎么来的?
事实上这些镜像都来源与一个空镜像,scratch。关于 scratch 的官方解释如下:
This image is most useful in the context of building base images (such as debian and busybox) or super minimal images (that contain only a single binary and whatever it requires, such as hello-world).
As of Docker 1.5.0 (specifically, docker/docker#8827), FROM scratch is a no-op in the Dockerfile, and will not create an extra layer in your image (so a previously 2-layer image will be a 1-layer image instead).
官方在这里给出了测试用的hello-world镜像 Dockerfile:
FROM scratc
COPY hello /
CMD ["/hello"]
该 Dockerfile 制作出来的镜像只有 1.84k。
使用 scratch 空镜像的本质是让你的程序只调用 host 主机的 Linux 内核部分的功能,而不依赖容器内的操作环境功能。
由于 host 主机的 Linux 内核部分对 Docker 容器是共享的,因此其 scratch 空镜像的大小可以认为近似为 0。
2. 以 Go 语言为例做一个最小镜像
做最小镜像的关键在于你要完全了解你的程序需要什么,而针对不同的语言,需要到的背景知识和技巧也不同。
这里笔者用较为熟悉的 Go 语言为例子做一个最小镜像,程序如下:
package main
import (
"fmt"
)
func main() {
("Hello")
}
2.1. 编译中技巧
如果希望你程序编译出来最小,并保证能够在 scratch 空镜像内运行,则需要在编译中用一些技巧:
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '-w -s'
下面笔者将一一解析这条命令中的各个部分含义。
2.1.1. GOOS=linux GOARCH=amd64 含义
确保编译出来的程序可以运行在你的容器运行环境。笔者的容器运行环境为 amd64 linux 环境。
2.1.2 CGO_ENABLED=0 含义
该参数是确保你用到的 C 函数库包含到你的 Go run-time 中,程序运行时以静态方式内部调用
否则由于 scratch 空镜像内没有 C 函数库,Go 程序动态调用时会出错。
PS:Go 语言调用 C 函数库出错的现象也会出现在 alpine 中,这是因为 alpine 的 C 函数库是精简版的。
2.1.3. -ldflags ‘-w -s’ 含义
这部分参数是精简掉 Debug 信息,而让编译出来的程序更小。
-w 是精简掉DWARF。
-s 是精简掉 debug symbol
2.2. Dockerfile 书写技巧
Dockerfile 的书写也需要一定的技巧,这里先给出笔者的 Dockerfile:
FROM scratch
LABEL authors="LEO"
ADD hello /
CMD [ "/hello" ]
2.2.1. RUN 命令失效
由于 scratch 空镜像中没有 sh 或 bash,想 mkdir、mv 等 shell 命令是无效的。
因此请在镜像外部把文件结构建立好,然后通过 ADD 或 COPY 命令拷贝到容器内。
2.2.2. 单层一次到位原则
尽管在使用 scratch 空镜像时无法使用 RUN,不大会违反这一原则,但笔者认为大家有必要知道这一原则。
Dokcer 建立镜像时会保留每一层的状态,这就导致当我们在 Dockerfile 写下下列命令时:
RUN mv /here/{1M文件} /there/{1M文件}
制作出来的镜像会大出 1M,这是因为/here 下的文件并没有因为 mv 命令而消失,它永远保存在 RUN 命令之前的那一层中了,RUN 命令这一层只是把它藏了起来。
2.3. 制作镜像查看成果
在包含 Dockerfile 和 hello 程序的位置执行下面的命令制作镜像:
docker build -t hello:latest .
执行下面的命令验证容器是否能正常运行:
docker run hello:latest
现在看一下我们镜像的大小和可执行程序的大小:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
hello latest 116f651aa3b5 6 seconds ago 356.2 kB
$ ll hello
-rwxr-xr-x 1 ... ... ... 348K Nov 12 18:51 hello
可以看到我们最终得到的镜像的大小就接近与可执行文件的大小。
3. 总结
使用 scratch 空镜像的优点:
- 真正意义上的最小镜像,镜像大小约等于执行文件大小
- 安全稳定,只需要关注维护程序本身和 Linux 内核安全更新即可
- 最高效的资源利用,容器内没有任何多余程序或服务占用资源
- 制作镜像方便快捷,由于 scratch 空镜像不需要 load 或 pull 就能使用,流水线上制作镜像更加方便快捷
缺点:
- 由于没有 sh 或 bash,无法进入容器内进行交互式调试
PS:该问题实际可以通过构建自定义基础镜像解决。但笔者个人认为生产环境应该通过日志分析问题,而不是进入到容器内进行调试。