Dockerfile多阶段构建
老版本的Docker中为什么不支持多个FROM指令,在17.05版本之前的Docker,只允许Dockerfile中出现一个FROM指令,这得从镜像的本质说起,可以简单理解Docker的镜像是一个压缩文件,其中包含了需要的程序和一个文件系统,Docker镜像并非只是一个文件,而是由一堆文件组成,最主要的文件是层
Dockerfile中,大多数指令会生成一个层:
#基础镜像中已经存在若干个层了
FROM centos7
#RUN指令会增加一层,在这一层中,安装了redis软件
RUN yum update \
&& yum install -y redis \
&& yum clean
FROM base
#RUN指令会增加一层,在这一层中,安装了 nginx
RUN yum update \
&& yum install -y nginx \
&& yum clean
假设基础镜像 centos:7 已经存在5层,使用第一个Dockerfile打包成镜像 base,则base有6层,又使用第二个Dockerfile打包成镜像 test,则 test 中有7层
如果 centos:7 等其它镜像不算,系统中只存在base 和 test 两个镜像,那么系统中一共保存了多少层,是7层而并非13层,因为 test 和 base 共享了6层,层的共享机制可以节约大量的磁盘空间和传输带宽,比如你本地已经有了 base 镜像,又从镜像仓库中拉取test 镜像时,只拉取本地所没有的最后一层就可以了,不需要把整个 base 镜像连根拉一遍
Docker镜像的每一层只记录文件变更,在容器启动时,Docker会将镜像的各个层进行计算,最后生成一个文件系统,这个被称为联合挂载,Docker的各个层是有相关性的,在联合挂载的过程中,系统需要知道在什么样的基础上再增加新的文件,那么这就要求一个Docker镜像只能有一个起始层,只能有一个根,所以Dockerfile中,就只允许一个 FROM 指令,因为多个 FROM 指令会造成多根,则是无法实现的
多个FROM指令
Docker 17.05 版本之后允许 Dockerfile 支持多个 FROM 指令了,多个 FROM 指令并不是为了生成多根的层关系,最后生成的镜像,仍以最后一条 FROM 为准,之前的 FROM 会被抛弃,每一条 FROM 指令都是一个构建阶段,多条 FROM 就是多阶段构建,虽然最后生成的镜像只能是最后一个阶段的结果,但是能够将前置阶段中的文件拷贝到后边的阶段中,这就是多阶段构建的最大意义(最大的使用场景是将编译环境和运行环境分离)
示例:(Go语言程序)
#Go环境基础镜像
FROM golang:1.17.6
#将源码拷贝到镜像中
COPY main.go /home/
#指定工作目录
WORKDIR /home
#编译镜像时,运行go build编译生成helloworld程序
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOARM=6 go build -ldflags '-w -s' -o helloworld
#指定容器运行时入口程序 helloworld
ENTRYPOINT ["/home/helloworld"]
基础镜像golang:1.17.6是非常庞大的,因为其中包含了所有的Go语言编译工具和库,而运行时候我们仅仅需要编译后的 helloworld 程序就行了,不需要编译时的编译工具,将程序编译和镜像打包分开,选择增加构Go语言构建工具,然后在构建步骤中编译,最后将编译好的可执行文件拷贝到镜像中就行了,那么Dockerfile的基础镜像并不需要包含Go编译环境
#基础可运行镜像
FROM base
#将编译后可执行文件拷贝到容器中
COPY helloworld /home/helloworld
#指定容器运行时入口程序 helloworld
ENTRYPOINT ["/home/helloworld"]
Docker 17.05版本以后,就有了新的解决方案,直接一个Dockerfile就可以解决
#编译阶段
FROM golang:1.17.6
COPY main.go /home/
WORKDIR /home
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOARM=6 go build -ldflags '-w -s' -o helloworld
# 运行阶段
FROM base
#将编译阶段的中编译后可执行文件拷贝到当前镜像中
COPY --from=0 /home/helloworld /home
ENTRYPOINT ["/home/helloworld"]
Dockerfile的玄妙之处就在于COPY指令的--from=0 参数,从前边的阶段中拷贝文件到当前阶段中,多个FROM语句时,0代表第一个阶段,除了使用数字,还可以给阶段命名
#编译阶段命名为build
FROM golang:1.17.6 as build
...
#运行阶段
FROM base
...
COPY --from=build /home/helloworld /home
...
COPY --from不但可以从前置阶段中拷贝,还可以直接从一个已经存在的镜像中拷贝
FROM base
COPY --from=quay.io/coreos/etcd:v3.3.9 /usr/local/bin/etcd /usr/local/bin/
直接将etcd镜像中的程序拷贝到了 base 镜像中,这样在生成新的程序镜像时,就不需要源码编译etcd了,直接将官方编译好的程序文件拿过来就可以了