上一节我们通过更改基础镜像的方式优化了镜像体积,选择一些精简的镜像作为基础镜像比其他优化方式更立竿见影,但是基础镜像的选择只是镜像优化的开始。接下来我们使用多阶段构建进一步优化镜像体积。
通俗来讲,多阶段构建就是在Dockerfile中定义多个FROM,每个FROM下有多个不同的指令,一般可简单分为构建步骤和生成业务应用镜像步骤,也就是说前面一个或多个阶段用于构建,产生业务应用的包或其他产物,之后的阶段将上述阶段产生的包或其他产物再次构建成镜像。这样一来,最后一步就没有了构建时产生的缓存文件,也起到了优化镜像体积的作用。
假如有一个用Go语言开发的应用,此处用Hello World代替:
首先使用单个步骤构建,看一下制作出来的镜像大小,此时的Dockerfile如下:
# build step FROM golang:1.14.4-alpine WORKDIR /opt COPY hw.go /opt RUN go build /opt/hw.go CMD "./hw"
执行构建并运行测试:
# docker build -t hw:one . Successfully built e036098fbb2e Successfully tagged hw:one # docker run --rm hw:one Hello World!
查看此时由一个阶段构建的镜像大小:
可以看到此时镜像大小为372MB,但是上述代码我们只需要构建步骤产生的二进制文件hw即可,这个文件大小可以进入容器内部看一下:
真正需要的二进制文件只有2MB,如果使用上述方法执行构建并生成业务镜像,那么生成的镜像将会多用370MB空间,所以我们需要使用多阶段构建,将构建步骤和生成业务镜像的步骤拆分,之后将二进制包放置在某个可以运行该二进制包的基础镜像中即可。假如我们使用上一节提到的Scratch镜像制作业务应用镜像,此时的多阶段Dockerfile如下:
# cat Dockerfile # 构建过程 FROM golang:1.14.4-alpine as builder WORKDIR /opt COPY hw.go /opt RUN go build /opt/hw.go # CMD "./hw" # 生成应用镜像过程 FROM scratch COPY --from=builder /opt/hw .
ROM xxx as xxx用于给某个阶段起一个别名,在其他阶段使用--from=别名进行引用,如果不用别名,可以从上往下用0/1/2/x代替。
Scratch镜像为空镜像,里面没有任何东西,不能被拉取和推送,一般可以用在静态语言的应用中,比如这次举例的Hello World没有用到任何动态库,就可以使用这个镜像,但是在生产环境中,一般很少用该镜像直接作为生成业务镜像的基础镜像,因为该镜像没有任何可供使用的工具包,出了问题很难排查。
再次构建后,查看此时的镜像大小和运行镜像的效果:
使用多阶段构建不仅实现了相同的效果,而且还节省了300多兆字节(MB)的磁盘空间,所以在生产环境中构建自己的业务镜像时,可以根据不同的语言制定不同的构建过程,拆分代码编译过程和生成业务应用镜像过程。我们在本书第18章持续集成持续部署部分也会根据这个原理将代码编译和docker build步骤拆开,每个步骤各司其职,不仅可以优化我们的流水线,也可以降低因为构建步骤设计不合理造成的磁盘空间的浪费。