Docker 最佳实践之多阶段构建

by Mike

运维之美 / 2018-09-03 12:11

Docker 目前在容器市场可以说是占领了大部分市场,Docker 掀起了容器革命,同时也改变了现代化云平台的构建方式。尽管 Docker 很强大,但使用过程当中也遇到了一些问题。比如:我想要构建一个编译型语言镜像,需要先在一个 Dockerfile 中编译,然后再使用另外一个 Dockerfile 把编译好的文件放到镜像中。这样无形当中就增大了 CI/CD 的复杂度。

Docker 多阶段构建是 17.05 以后引入的新特性,旨在解决编译和构建复杂的问题。因此要使用多阶段构建特性必须使用高于或等于 17.05 的 Docker。

多阶段构建出现之前

  • 构建镜像最具挑战性的一点是使镜像大小尽可能的小。Dockerfile 中的每条指令都为镜像添加了一个镜像层,您需要记住在移动到下一个镜像层之前清理任何不需要的组件。
  • 为了编写一个真正高效的 Dockerfile,传统上需要使用 Shell 技巧和其它逻辑来保持镜像层尽可能小,并确保每个镜像层都具有前一层所需的组件而不是其它任何东西。

很多时候我们用一个 Dockerfile 维护开发环境(包含构建应用程序所需的所有内容),用另一个 Dockerfile 维护生产环境(只包含您的应用程序以及运行它所需的内容)。实际上同时维护两个 Dockerfiles 并不是一种理想的构建模式,这种模式被称为建造者模式。

我们来看一个例子,这是一个 Dockerfile.build 和 Dockerfile 的例子,它遵循上面的模式。

Dockerfile.build

1
2
3
4
5
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 .

请注意,此示例使用 Bash && 运算符人为合并两个 RUN 命令,以避免在 Image 中创建出其它层。这种方法很容易出错并且难以维护。

Dockerfile

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

build.sh

1
2
3
4
5
6
7
8
9
10
#!/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 脚本时,它首先会构建第一个 Image 并从创建容器中复制编译好的程序到本地。然后在第二个 Image 中将构建好的程序运行起来。

使用多阶段构建

从上面的过程中可以看到过程是非常复杂且容易出错的,多阶段构建的出现就大大简化了这种情况!

对于多阶段构建,您可以在 Dockerfile 中使用多个 FROM 语句。每个 FROM 指令可以使用不同的基础镜像,并且每个 FROM 指令都会开始一个新的构建阶段。您可以选择性地将各构建阶段中的内容从一个阶段复制到另一个阶段,从而在最终 Image 中只留下您想要的内容。

为了说明这是如何工作的,我们调整上述示例中的 Dockerfile 使用多阶段方式来构建。

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
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 并且不需要任何的单独构建脚本,就可以构建出上面相同的 Image。

1
$ docker build -t app:latest .

通过 docker build 构建后,最终结果是产生与之前相同大小的 Image,但复杂性显著降低。您不需要创建任何中间 Image,也不需要将任何编译结果临时提取到本地系统。

哪它是如何工作的呢?关键就在 COPY --from=0 这个指令上。Dockerfile 中第二个 FROM 指令以 alpine:latest 为基础镜像开始了一个新的构建阶段,并通过 COPY --from=0 仅将前一阶段的构建文件复制到此阶段。前一构建阶段中产生的 Go SDK 和任何中间层都会在此阶段中被舍弃,而不是保存在最终 Image 中。

为多构建阶段命名

默认情况下,构建阶段是未命名的。您可以通过一个整数值来引用它们,默认是从第 0 个 FROM 指令开始的。 为了方便管理,您也可以通过向 FROM 指令添加 as NAME 来命名您的各个构建阶段。下面的示例就通过命名各个构建阶段并在 COPY 指令中使用名称来访问指定的构建阶段。

这样做的好处就是即使稍后重新排序 Dockerfile 中的指令,COPY 指令一样能找到对应的构建阶段。

1
2
3
4
5
6
7
8
9
10
11
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 的阶段:

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

此功能适合以下场景:

  • 调试特定的构建阶段。
  • 在 Debug 阶段,启用所有程序调试模式或调试工具,而在生产阶段尽量精简。
  • 在 Testing 阶段,您的应用程序使用测试数据,但在生产阶段则使用生产数据。

使用外部镜像作为构建阶段

使用多阶段构建时,您不仅可以从 Dockerfile 中创建的镜像中进行复制。您还可以使用 COPY --from 指令从单独的 Image 中复制,支持使用本地 Image 名称、本地或 Docker 注册中心可用的标记或标记 ID。

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

本文在 「Docker 最佳实践之多阶段构建 」的基础上整理和修改,原文地址:http://t.cn/RFx6ML2

Shared via Inoreader