当前位置: 首页 > 后端技术 > Node.js

Dockerfile多阶段构建原理及使用场景

时间:2023-04-04 01:09:22 Node.js

Docker17.05版本之后,新增了Dockerfile多阶段构建。所谓多阶段构建,其实就是让多个FROM指令出现在一个Dockerfile中。这样做有什么意义?为什么老版本的Docker不支持多条FROM指令?在Docker17.05版本之前,Dockerfile中只允许有一条FROM指令,从镜像本质开始。我们在中提到过,你可以简单的理解Docker镜像就是一个压缩文件,里面包含了你需要的程序和一个文件系统。其实这样说是不准确的。Docker镜像不仅仅是一个文件,而是一堆文件。最重要的文件是图层。在Dockerfile中,大部分指令都会生成一层,比如下面两个例子:#例1,foo镜像的Dockerfile#基础镜像中已经有好几层FROMubuntu:16.04#RUN指令会增加一层,这里在一层,安装git软件RUNapt-getupdate\&&apt-getinstall-y--no-install-recommendsgit\&&apt-getclean\&&rm-rf/var/lib/apt/lists/*#例2,bar镜像的DockerfileFROMfoo#RUN命令会增加一层,在这一层中,nginxRUNapt-getupdate\&&apt-getinstall-y--no-install-recommendsnginx\&&apt-getclean\&&rm-rf/var/lib/apt/lists/*假设基础镜像ubuntu:16.04已经有5层,使用第一个Dockerfile打包镜像foo,然后foo有6层,使用第二个Dockerfile如果打包成镜像吧,吧里有7层。如果不算ubuntu:16.04等其他镜像,如果系统中只有foo和bar两个镜像,系统中保存了多少层?是7层,不是13层,因为foo和bar共用6层。层共享机制可以节省大量的磁盘空间和传输带宽。比如你本地已经有foo镜像,从镜像仓库拉取bar镜像,只需要拉取本地没有的最后一层即可。将整个条形图拉到根。但是层共享是如何实现的呢?原来Docker镜像的每一层都只记录文件变化。容器启动时,Docker会计算镜像的每一层,最后生成一个文件系统,称为联合挂载。如果你对此感兴趣,可以进入了解AUFS。Docker的每一层都是相关的。在联合挂载过程中,系统需要知道在什么基础上添加新文件。那么这就要求一个Docker镜像只能有一个起始层,并且只有一个根。因此,在Dockerfile中,只允许有一条FROM指令。因为多条FROM指令会造成多根,所以无法实现。但是为什么Docker在17.05版本之后允许Dockerfile支持多条FROM指令呢?它是否已经支持多根?多FROM指令的含义多FROM指令不是为了生成多根层关系。最终生成的镜像还是基于上次的FROM,之前的FROM会被丢弃。那么前面的FROM有什么意义呢?每条FROM指令为一个构造阶段,多个FROM为多阶段构造。虽然最终生成的图像只能是上一阶段的结果,但是可以将前一阶段的文件复制到后一阶段。这是多阶段建设的最大意义。最大的使用场景就是把编译环境和运行环境分开。比如我们之前需要构建一个Go语言程序,就需要用到go命令等编译环境。我们的Dockerfile可能是这样的:#Go语言环境基础镜像FROMgolang:1.10.3#复制源码到镜像COPYserver.go/build/#指定工作目录WORKDIR/build#编译镜像时,运行gobuild编译生成服务器程序RUNCGO_ENABLED=0GOOS=linuxGOARCH=amd64GOARM=6gobuild-ldflags'-w-s'-oserver#指定容器运行时入口程序serverENTRYPOINT["/build/server"]基础镜像golang:1.10.3非常大,因为它包含了Go语言所有的编译工具和库,而我们只需要在运行时编译的服务器程序。我们在编译时不需要编译工具,最后生成的大体积镜像是一种浪费。使用PulseCloud的解决方案是将程序编译和镜像打包分开,使用PulseCloud的编译构建服务,选择添加Go语言构建工具,然后在构建步骤进行编译。最后将编译接口复制到镜像中即可,所以Dockerfile的基础镜像不需要包含Go编译环境:#不需要Go语言编译环境FROMscratch#将编译结果复制到容器中COPYserver/server#指定容器运行时入口程序serverENTRYPOINT["/server"]提示:scratch是内置关键字,不是真实镜像。FROMscratch将使用完全干净的文件系统,不包含任何文件。因为Go语言编译后不需要运行时,所以不需要安装任何运行时库。FROMscratch可以最小化最终生成的镜像,只包含服务器程序。Docker17.05版本之后,有了新的解决方案,直接用一个Dockerfile就可以解决:#CompilephaseFROMgolang:1.10.3COPYserver.go/build/WORKDIR/buildRUNCGO_ENABLED=0GOOS=linuxGOARCH=amd64GOARM=6gobuild-ldflags'-w-s'-oserver#RunningstageFROMscratch#将编译阶段的编译结果复制到当前镜像COPY--from=0/build/server/ENTRYPOINT["/server"]的这个Dockerfile的奥秘就在于COPY命令的--from=0参数,将前一阶段的文件复制到当前阶段。当有多个FROM语句时,0代表第一阶段。除了用数字,我们还可以给stage命名,比如:#编译阶段命名为builderFROMgolang:1.10.3asbuilder#...omitted#TherunningstageFROMscratch#从编译阶段复制编译结果到当前镜像COPY--from=builder/build/server/更强大的是,COPY--from不仅可以从pre-stage复制,还可以直接从已有镜像复制。比如FROMubuntu:16.04COPY--from=quay.io/coreos/etcd:v3.3.9/usr/local/bin/etcd/usr/local/bin/我们直接把etcd镜像里的程序复制到我们的镜像里这样在生成我们的程序镜像的时候,就不需要从源码编译etcd了,直接拿官方编译好的程序文件就可以了。有些程序要么没有apt源,要么apt源里的版本太旧,要么干脆提供源码,需要自己编译。在使用这些程序的时候,我们可以很方便的使用已有的Docker镜像作为我们的基础镜像。但是,我们的软件有时可能需要依赖多个此类文件。我们不能同时使用nginx和etcd的镜像作为我们的基础镜像(不支持多根)。这种情况下,使用COPY--from就很重要了,方便实用。