最近更新Docker知识发现了一个新的Linux发行版Alpine Linux,这个发行版仅有几M大小,非常适合作为Docker容器的基础环境。DockerHub上主流的镜像纷纷推出Alpine版本的镜像,由此我决定尝试一下如何制作最小的Docker镜像。

Docker减少大小有以下几种思路:

  • 使用更小的Base镜像
  • 减少Layer层数
  • 合并RUN指令,尽可能的清除中间过程的临时文件
  • 最小化可执行程序

对于极小化Base镜像,我们有三种选择:

  • scratch 空的镜像,这是制作最小镜像的最佳选择
  • busybox 具有单一可执行文件的精简Unix工具集
  • alpine 运行于RAM的Linux 发行版,具有一个完整的包管理工具

我的需求是把一些Go语言程序和Haskell程序Docker化,两个语言都是可以静态编译为二进制程序,因此我选择scratch作为Base镜像,整个镜像仅包含一个可独立运行的压缩后的二进制可执行程序。

最终生成镜像的Dockerfile代码如下,文件保存为Dockerfile.run

  FROM scratch
  COPY main /main
  ENTRYPOINT ["/main"]

请注意,ENTRYPOINT必须指定,并且必须使用[""]格式指定绝对路径,否则会无法执行main程序。

这个Dockerfile无法解决如何得到main程序的问题,合适的方法是把编译过程放到Docker中进行,这样就可以做到不污染宿主环境。因此在最终执行生成目标镜像之前需要做一个预编译。预编译的Dockerfile如下

FROM golang:alpine
COPY make.sh make.sh
COPY Dockerfile.run Dockerfile

RUN chmod +x make.sh \
 && ./make.sh \
 && eval [ -f main ]

CMD tar -cf - main Dockerfile

其中给出了一个make.sh用于编写具体的Go语言程序编译脚本。预编译镜像构建过程会进行目标程序编译,生成后的镜像执行时会输出一个tar包,该包包含我们需要的Dockerfile.run需要的所有文件。

通过如下脚本,可以把上述两个Docker构建过程串联起来,

docker build --rm --no-cache -t icymint/main-builder . \
&& docker run --rm icymint/main-builder | docker build --rm --no-cache -t icymint/main - \
&& docker rmi icymint/main-builder

该脚本首先构建镜像icymint/main-builder,然后执行这个镜像输出构建icymint/main的完整tar包,并且直接构建,最后删除icymint/main-builder。执行结果是创建了一个icymint/main的镜像,其大小基本上就是main程序的大小。预编译环境里面的临时文件不会影响最终镜像的大小。

再进一步的优化

  • Go语言编译是去掉一些Debug信息,使用x86编译
  • Go语言编译后的二进制文件使用upx进行压缩

完整的构建代码可以参考我的Github项目,最后一个项目是编译Haskell的方案。

最终编译的镜像大小,这些镜像都已经发布到DockerHub

icymint/ngrokd        latest     d6a0efe611d7        About an hour ago   1.488 MB
icymint/shadowsocks   latest     d75dcea923af        5 hours ago         753.4 kB
icymint/brainfuck     latest     da2adca648cc        5 days ago          1.273 MB
icymint/http          latest     5081ed56639d        6 days ago          1.081 MB

参考文章