Docker 17.05版本之后,新增了Dockerfile多阶段构建。所谓多阶段构建,其实是容许一个Dockerfile 中出现多个 FROM
指令。这样作有什么意义呢?html
在17.05版本以前的Docker,只容许Dockerfile中出现一个FROM
指令,这得从镜像的本质提及。linux
在《Docker概念简介》 中咱们提到,你能够简单理解Docker的镜像是一个压缩文件,其中包含了你须要的程序和一个文件系统。其实这样说是不严谨的,Docker镜像并不是只是一个文件,而是由一堆文件组成,最主要的文件是 层。nginx
Dockerfile 中,大多数指令会生成一个层,好比下方的两个例子:git
# 示例一,foo 镜像的Dockerfile # 基础镜像中已经存在若干个层了 FROM ubuntu:16.04 # RUN指令会增长一层,在这一层中,安装了 git 软件 RUN apt-get update \ && apt-get install -y --no-install-recommends git \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* # 示例二,bar 镜像的Dockerfile FROM foo # RUN指令会增长一层,在这一层中,安装了 nginx RUN apt-get update \ && apt-get install -y --no-install-recommends nginx \ && apt-get clean \ && rm -rf /var/lib/apt/lists/*
假设基础镜像ubuntu:16.04
已经存在5层,使用第一个Dockerfile打包成镜像 foo,则foo有6层,又使用第二个Dockerfile打包成镜像bar,则bar中有7层。golang
若是ubuntu:16.04
等其余镜像不算,若是系统中只存在 foo 和 bar 两个镜像,那么系统中一共保存了多少层呢?ubuntu
是7层,并不是13层,这是由于,foo和bar共享了6层。层的共享机制能够节约大量的磁盘空间和传输带宽,好比你本地已经有了foo镜像,又从镜像仓库中拉取bar镜像时,只拉取本地所没有的最后一层就能够了,不须要把整个bar镜像连根拉一遍。可是层共享是怎样实现的呢?工具
原来,Docker镜像的每一层只记录文件变动,在容器启动时,Docker会将镜像的各个层进行计算,最后生成一个文件系统,这个被称为 联合挂载。对此感兴趣的话能够进入了解一下 AUFS。ui
Docker的各个层是有相关性的,在联合挂载的过程当中,系统须要知道在什么样的基础上再增长新的文件。那么这就要求一个Docker镜像只能有一个起始层,只能有一个根。因此,Dockerfile中,就只容许一个FROM
指令。由于多个FROM
指令会形成多根,则是没法实现的。但为何 Docker 17.05 版本之后容许 Dockerfile支持多个 FROM
指令了呢,莫非已经支持了多根?code
多个 FROM 指令并非为了生成多根的层关系,最后生成的镜像,仍以最后一条 FROM 为准,以前的 FROM 会被抛弃,那么以前的FROM 又有什么意义呢?server
每一条 FROM 指令都是一个构建阶段,多条 FROM 就是多阶段构建,虽然最后生成的镜像只能是最后一个阶段的结果,可是,可以将前置阶段中的文件拷贝到后边的阶段中,这就是多阶段构建的最大意义。
最大的使用场景是将编译环境和运行环境分离,好比,以前咱们须要构建一个Go语言程序,那么就须要用到go命令等编译环境,咱们的Dockerfile多是这样的:
# Go语言环境基础镜像 FROM golang:1.10.3 # 将源码拷贝到镜像中 COPY server.go /build/ # 指定工做目录 WORKDIR /build # 编译镜像时,运行 go build 编译生成 server 程序 RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOARM=6 go build -ldflags '-w -s' -o server # 指定容器运行时入口程序 server ENTRYPOINT ["/build/server"]
基础镜像golang:1.10.3
是很是庞大的,由于其中包含了全部的Go语言编译工具和库,而运行时候咱们仅仅须要编译后的server
程序就好了,不须要编译时的编译工具,最后生成的大致积镜像就是一种浪费。
使用脉冲云的解决办法是将程序编译和镜像打包分开,使用脉冲云的编译构建服务,选择增长构Go语言构建工具,而后在构建步骤中编译。
最后将编译接口拷贝到镜像中就好了,那么Dockerfile的基础镜像并不须要包含Go编译环境:
# 不须要Go语言编译环境 FROM scratch # 将编译结果拷贝到容器中 COPY server /server # 指定容器运行时入口程序 server ENTRYPOINT ["/server"]
提示:scratch
是内置关键词,并非一个真实存在的镜像。FROM scratch
会使用一个彻底干净的文件系统,不包含任何文件。 由于Go语言编译后不须要运行时,也就不须要安装任何的运行库。FROM scratch
可使得最后生成的镜像最小化,其中只包含了server
程序。
在 Docker 17.05版本之后,就有了新的解决方案,直接一个Dockerfile就能够解决:
# 编译阶段 FROM golang:1.10.3 COPY server.go /build/ WORKDIR /build RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOARM=6 go build -ldflags '-w -s' -o server # 运行阶段 FROM scratch # 从编译阶段的中拷贝编译结果到当前镜像中 COPY --from=0 /build/server / ENTRYPOINT ["/server"]
这个 Dockerfile 的玄妙之处就在于 COPY 指令的--from=0
参数,从前边的阶段中拷贝文件到当前阶段中,多个FROM语句时,0表明第一个阶段。除了使用数字,咱们还能够给阶段命名,好比:
# 编译阶段 命名为 builder FROM golang:1.10.3 as builder # ... 省略 # 运行阶段 FROM scratch # 从编译阶段的中拷贝编译结果到当前镜像中 COPY --from=builder /build/server /
更为强大的是,COPY --from
不但能够从前置阶段中拷贝,还能够直接从一个已经存在的镜像中拷贝。好比,
FROM ubuntu:16.04 COPY --from=quay.io/coreos/etcd:v3.3.9 /usr/local/bin/etcd /usr/local/bin/
咱们直接将etcd镜像中的程序拷贝到了咱们的镜像中,这样,在生成咱们的程序镜像时,就不须要源码编译etcd了,直接将官方编译好的程序文件拿过来就好了。
有些程序要么没有apt源,要么apt源中的版本太老,要么干脆只提供源码须要本身编译,使用这些程序时,咱们能够方便地使用已经存在的Docker镜像做为咱们的基础镜像。可是咱们的软件有时候可能须要依赖多个这种文件,咱们并不能同时将 nginx 和 etcd 的镜像同时做为咱们的基础镜像(不支持多根),这种状况下,使用 COPY --from
就很是方便实用了。