Dockerfile多阶段构建(multi-stage builds)

多阶段构建是Docker17.05级以上版本的新特性。多阶段构建对优化Dockerfile很有用,同时还保持了Dockerfile的可读性和可维护性。

致谢:特别感谢Alex Ellis允许使用他的博客文章Builder模式与Docker中的多阶段构建作为以下示例的基础。

多阶构建之前

保持镜像尽可能的小是构建镜像最重要的一件事情,Dockerfile中每个指令都会在镜像中创建一个层。我们每创建完一层都要把不再需要的东西清除掉,想要写出高效的Dockerfile,您除了需要掌握shell技巧,还需要从逻辑上保待层尽可能的小,并且只保留下一个层必需的目标。

有一种常见的做法,一个Dockerfile用于开发(包含构建您的应用的所有东西),一个简化的用于生产,只包含你的应用和运行它需要的东西。这通常被称为构建模式。维护两个Dockerfile并不是个好主意。

下面的例子使用了上面讲的构建模式,两个Dockerfile.build和Dockerfile

Dockerfile.build:

FROM golang:1.7.3
WORKDIR /go/src/github.com/alexellis/href-counter/
COPY app.go .
RUN go get -d -v golang.org/x/net/html   && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

这个例子人工的把两个RUN命令用Bush&&操作压缩,避免添加更多的层到镜像中。然而这样容易发生故障且难以维护。例如,很容易插入另一个命令而忘记使用\字符继续行。

Dockerfile:

FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY app .
CMD ["./app"] 

build.sh:

#!/bin/sh
echo Building alexellis2/href-counter:build

docker build --build-arg https_proxy=$https_proxy --build-arg http_proxy=$http_proxy \  
    -t alexellis2/href-counter:build . -f Dockerfile.build

docker container create --name extract alexellis2/href-counter:build  
docker container cp extract:/go/src/github.com/alexellis/href-counter/app ./app  
docker container rm -f extract

echo Building alexellis2/href-counter:latest

docker build --no-cache -t alexellis2/href-counter:latest .
rm ./app

当您运行build.sh脚本时,它需要构建第一个镜像,并从中创建一个容器来复制中间物,然后构建第二个镜像。这两个镜像都占用了系统上的空间,您的本地磁盘上仍然有app中间物。

多阶段构建极大地简化了这种情况!

使用多阶段构建

对于多阶段构建,您可以在Dockerfile中使用多个FROM语句。每个FROM指令都可以使用不同的基镜像,并且它们都开始了构建的新阶段。您可以有选择地将中间物从一个阶段复制到另一个阶段,舍弃在最后的镜像中您不想要的所有内容。为了演示这是如何工作的,让我们修改前一节中的Dockerfile,以使用多阶段构建。

Dockerfile:

FROM golang:1.7.3
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html  
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"]  

您只需要一个Dockerfile。也不需要单独的构建脚本。只需运行docker构建。

$ docker build -t alexellis2/href-counter:latest .

最终的结果是与以前一样小的生产镜像,并且显著降低了复杂性。您不需要创建任何中间镜像,也不需要将任何中间物提取到本地系统中。

它是如何工作的?第二个FROM指令以alpine:latest image为基础,开始了一个新的构建阶段。COPY --from=0行只将前一个阶段构建的中间物复制到这个新阶段。Go SDK和任何中间构件都被遗留下来,没有保存在最终的镜像中。

命名你的构建阶段

默认情况下,这些阶段没有命名,您可以通过它们的整数索引引用它们,第一个FROM指令为0。但是,您可以通过在FROM指令中添加AS <NAME>来为阶段命名。这个例子通过命名阶段和在COPY指令中使用名称来改进前面的例子。这意味着即使Dockerfile中的指令稍后被调整顺序,COPY也不会出错。

FROM golang:1.7.3 AS builder
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html  
COPY app.go    .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"]  

在特定的构建阶段停止

构建镜像时,不需要构建整个Dockerfile中包括的每个阶段。 您可以指定目标构建阶段。 以下命令假定您正在使用以前的Dockerfile,但在名为builder的阶段停止:

$ docker build --target builder -t alexellis2/href-counter:latest .

一些场景中这可能非常强大:

  • 调试特定的构建阶段。
  • 使用debug阶段——启用了所有调试符号或工具,和干净的production阶段。
  • 使用testing阶段,在该阶段中,您的应用填充了测试数据,但生产构建使用另一个使用真实数据的阶段。

将外部镜像用作“阶段”

使用多阶段构建时,您不仅限于从Dockerfile中之前创建的阶段进行复制。 您可以使用COPY --from指令从单独的镜像中复制,可以是本地镜像名称,本地或Docker注册中心上可用的标签,或一个标签ID。 Docker客户端在必要时拉取镜像并从中复制构件。 语法为:

COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf

将上一个阶段用作新阶段

在使用FROM指令时,可以通过引用前一个阶段结束的地方来进行选择。 例如:

FROM alpine:latest as builder
RUN apk --no-cache add build-base

FROM builder as build1
COPY source1.cpp source.cpp
RUN g++ -o /binary source.cpp

FROM builder as build2
COPY source2.cpp source.cpp
RUN g++ -o /binary source.cpp