Docker multi-stage builds
今天来介绍一个 Docker build 很有用的特性:Multi-stage builds,即多阶段打包。
前言
这个特性很早之前,在 2017 年初的时候就可以使用了。如果你没有用到,很有可能你不需要编译语言,就比如 C/C++/Golang/Java 之类的语言。
现在网上有非常多的教程告诉我们,打包 Docker 镜像的时候,我们需要把镜像缩减到最小,因此我们可以看到最佳实践是类似于这样的:
1 | RUN apt-get update && apt-get install -y \ |
又或者这样的:
1 | RUN npm install --production \ |
还有这样的:
1 | RUN pip install -r requirements --no-cache-dir |
这些技巧的目的都在于把改动的内容缩减到最小,因为没必要把缓存之类的内容也放到镜像中去,这样就可以把镜像尽可能缩小。
而今天介绍的 Multi-Stage builds 就是达到这个目的的另一个重要手段。
很早之前
对于编译型的语言,我们可能需要编译完成后,将依赖与编译好的二进制文件(artifacts,即制品)拷贝到一个新的小镜像中来进行打包了,比如我们会把镜像的打包过程放到 CI 中去,大概的内容包括:
- 测试
- 安装测试依赖
- 静态检查
- 单元测试
- 编译
- 安装编译依赖
- 编译
- 打包镜像
- 安装依赖
- 将编译制品拷贝到空镜像
- 将镜像推送到镜像库中
我们可以看到,编译与打包的步骤需要分开,因为我们需要:
- 解耦,简化整体的过程;
- 两者如果是基于不同 CI 运行镜像的话,我们还需要用到不同的镜像;
- 需要用到 CI 提供的并行执行能力;
于是我们的问题出现了:如何将编译制品传送至打包步骤?用缓存是比较简单的,但是需要保证缓存只能被当前的编译步骤使用,不然会导致编译的版本错误。
Gitlab 提供的 dependencies
也能做到这一点,自动将你制定步骤的 artifacts 下载下来,只不过这样的步骤会显得多余:有时候你只是想用来打包镜像而已,而如果 artifacts 上传至 Gitlab 服务器,还有那缓慢速度的限制,拖慢整体打包步骤。
所以,我们的救星来了,Multi-stage builds。
现在
现在这个步骤就很简单了,它允许你把所有的步骤放到一个 Dockerfile 中去完成,拿官网1, 2上的例子来说:
1 | FROM golang:1.7.3 AS builder |
它将我之前说的 CI 编译与打包步骤合并了,可以注意到的是它有两个 FROM
关键字,这就是这个多阶段的关键所在了,它允许多个 FROM
来达到多阶段的目的,并且,最后一个 FROM
才是你真正的打包步骤,利用这个特性,你可以将最后打包完成的镜像做到最小。
这样还有一个好处,你可以更方便地迁移到其它 CI 平台了,毕竟两个步骤合并成一个了。
另外,这样的改变,在不注意的情况下,会让你的编译过程变慢,因为没有缓存。
解决方案也是有的,就是官网上说的是 Stop at a specific build stage,我们在编译的时候,需要多加一行镜像编译命令,就拿上面的例子来说:
1 | docker build -t . example-registry.com/app:latest |
我们可以利用 --target
以及 --cache-from
来实现:
1 | # 将最新已有的 builder 镜像拉下来 |
Ref
- Builder pattern vs. Multi-stage builds in Docker
- Use multi-stage builds
- Caching Docker layers on serverless build hosts with multi-stage builds, —target, and —cache-from
首发于 Github issues: https://github.com/xizhibei/blog/issues/124 ,欢迎 Star 以及 Watch
本文采用 署名-非商业性使用-相同方式共享(BY-NC-SA)进行许可作者:习之北 (@xizhibei)
原链接:https://blog.xizhibei.me/zh-cn/2019/11/04/use-docker-multi-stage-builds/