使用多阶段构建
多阶段构建从Docker 17.05及更高版本的守护进程与客户端的新功能, 对于那些努力优化Dockerfile同时保持可阅读性和可维护性的人来说,多阶段构建是非常有用的。
多阶段构建之前
构建镜像最有挑战性之一的就是使用镜像尽可能小。Dockerfile中的每一个指令都会向镜像添加一个新的层, 在移动到下一个图层之前,你需要记得清理所有不再需要的历史遗留。要编写一个非常高效的Dockerfile, 传统思维是采用shell技巧或其他方法使层尽可能小,并确保每个层都能从上一层拿到需要的数据,并且不会多拿。
一个Dockerfile用于开发环境,其中包含构建应用程序所需的一切, 另一个精简版的Dockerfile,只包含你的应用程序及运行所需的内容,用于生产环境, 这种情况实际上非常普遍,这被称为”构建器模式”。维护两个Dockerfile并不理想。
下面是一个Dockerfile.build与Dockerfile的示例,采用上面的构建器模式:
Dockerfile.build
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 go get -d -v golang.org/x/net/html \
&& CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
注意:此例还使用Bash的&&操作符人为的将两个RUN命令合在一起,以避免在镜像中多出一个层。
这种方式容易出错而且难以维护,比如你很可能插入一个命令,但是忘记写\字符。
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 create --name extract alexellis2/href-counter:build
docker cp extract:/go/src/github.com/alexellis/href-counter/app ./app
docker rm -f extract
echo Building alexellis2/href-counter:latest
docker build --no-cache -t alexellis2/href-counter:latest .
rm ./app
运行build.sh时,你需要先构建第一个镜像,创建一个容器以便将结果复制出来,然后构建第二个镜像。
两个镜像都会占用你的系统空间,并且在你的本地磁盘上依然有应用程序工件(artifact,译注:这词真难翻译)。
多阶段构建极大的简化了这种情况!
使用多阶段构建
在多阶段构建下,你可以在Dockerfile中使用多个FROM声明,每个FROM声明可以使用不同的基础镜像,
并且每个FROM都使用一个新的构建阶段。你可以选择性的将工单(artifacts)从一个阶段复制到另一个阶段,
删除你不想保留在最终镜像中的一切。为了说明它是如何工作的,我们来调整上一节的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 build。
$ docker build -t alexellis2/href-counter:latest .
最终的结果是与前面一样的极小的结果,但是复杂性大大降低,你不需要创建任何中间镜像, 也根本不需要将任何工件(artifacts)提取到本地系统。
它是如何工作的?第二个FROM指令使用alpine:latest镜像作为基础开始一个新的构建阶段,
COPY --from=0的行将前一个阶段的结果复制到新的阶段,GO SDK及所有中间产物被抛弃,并没有保存在最终镜像中。
命名构建阶段
默认情况下,构建阶段没有命名,使用它们的整数编号引用它们,从第一个FORM以0开始计数。
但是你可以使用给FORM指令添加一个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"]