云原生|【云原生丨Docker系列13】Docker 的多阶段构建详解

前言

多阶段构建指在Dockerfile中使用多个FROM语句,每个指令都可使用不同的基础镜像且是一个独立的子构建阶段。使用多阶段构建打包应用具有构建安全、构建速度快、镜像文件体积小等优点。
云原生|【云原生丨Docker系列13】Docker 的多阶段构建详解
文章图片

目录
1??引入
2??示例
3??解决方案
4??多阶段构建
1??引入 Docker 的?号是 Build,Ship,and Run Any App,Anywhere(一次封装,到处运行),在我们使? Docker 的?部分时候,的确能感觉到其优越性,但是往往在我们 Build ?个应?的时候,是将我们的源代码也构建进去的,这对于类似于 golang 这样的编译型语?肯定是不?的,因为实际运?的时候我只需要把最终构建的?进制包 给你就?,把源码也?起打包在镜像中,需要承担很多?险,即使是脚本语?,在构建的时候也可能 需要使?到?些上线的?具,这样?疑也增?了我们的镜像体积。
云原生|【云原生丨Docker系列13】Docker 的多阶段构建详解
文章图片


【云原生|【云原生丨Docker系列13】Docker 的多阶段构建详解】构建镜像时最具挑战性的事情之一就是缩小镜像大小。Dockerfile 中的每一条指令都会在镜像中添加一个层,在进入下一层之前,您需要记住清除所有不需要的工件。要编写一个真正高效的 Dockerfile,传统上需要使用 shell 技巧和其他逻辑来保持层尽可能小,并确保每一层都有它需要的来自前一层的工件,而没有其他东西。
实际上,有一个 Dockerfile 用于开发环境(包含构建应用程序所需的所有内容),同时有一个精简的 Dockerfile 用于生产环境(仅包含应用程序和运行应用程序所需的内容)是非常常见的。这被称为“建造者模式”。
2??示例 ?如我们现在有?个最简单的 golang 服务,需要构建?个最?的 Docker 镜像,源码如下:
package main import ( "github.com/gin-gonic/gin" "net/http" ) func main() { router := gin.Default() router.GET("/ping", func(c *gin.Context) { c.String(http.StatusOK, "PONG") }) router.Run(":8080") }

3??解决方案 我们最终的?的都是将最终的可执??件放到?个最?的镜像(?如 alpine )中去执?,怎样得到最终 的编译好的?件呢?基于 Docker 的指导思想,我们需要在?个标准的容器中编译,?如在?个 Ubuntu 镜像中先安装编译的环境,然后编译,最后也在该容器中执?即可。
但是如果我们想把编译后的?件放置到 alpine 镜像中执?呢?我们就得通过上?的 Ubuntu 镜像将 编译完成的?件通过 volume 挂载到我们的主机上,然后我们再将这个?件挂载到 alpine 镜像中 去。
这种解决?案理论上肯定是可?的,但是这样的话在构建镜像的时候我们就得定义两步了,第?步是先??个通?的镜像编译镜像,第?步是将编译后的?件复制到 alpine 镜像中执?,?且通?镜像编译后的?件在 alpine 镜像中不?定能执?。
定义编译阶段的 Dockerfile :(保存为Dockerfile.build)
FROM golang WORKDIR /go/src/app ADD . /go/src/app RUN go get -u -v github.com/kardianos/govendor RUN govendor sync RUN GOOS=linux GOARCH=386 go build -v -o /go/src/app/app-server

定义 alpine 镜像:(保存为Dockerfile.old)
FROM alpine:latest RUN apk add -U tzdata RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime WORKDIR /root/ COPY app-server . CMD ["./app-server"]

根据我们的执?步骤,我们还可以简单定义成?个脚本:(保存为build.sh)
#!/bin/sh echo Building cnych/docker-multi-stage-demo:build docker build -t cnych/docker-multi-stage-demo:build . -f Dockerfile.build docker create --name extract cnych/docker-multi-stage-demo:build docker cp extract:/go/src/app/app-server ./app-server docker rm -f extract echo Building cnych/docker-multi-stage-demo:old docker build --no-cache -t cnych/docker-multi-stage-demo:old . -f Dockerfile.old rm ./app-server

当我们执?完上?的构建脚本后,就实现了我们的?标。
4??多阶段构建 云原生|【云原生丨Docker系列13】Docker 的多阶段构建详解
文章图片

有没有?种更加简单的?式来实现上?的镜像构建过程呢?
Docker 17.05版本以后,官?就提供了? 个新的特性: Multi-stage builds (多阶段构建)。 使?多阶段构建,你可以在?个 Dockerfile 中使?多个 FROM 语句。每个 FROM 指令都可以使?不同的基础镜像,并表示开始?个新的构建 段。你可以很?便的将?个阶段的?件复制到另外?个阶段,在最终的镜像中保留下你需要的内容即可。
我们可以调整前??节的 Dockerfile 来使?多阶段构建:(保存为Dockerfile)
FROM golang AS build-env ADD . /go/src/app WORKDIR /go/src/app RUN go get -u -v github.com/kardianos/govendor RUN govendor sync RUN GOOS=linux GOARCH=386 go build -v -o /go/src/app/app-server FROM alpine RUN apk add -U tzdata RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime COPY --from=build-env /go/src/app/app-server /usr/local/bin/app-server EXPOSE 8080 CMD [ "app-server" ]

现在我们只需要?个 Dockerfile ?件即可,也不需要拆分构建脚本了,只需要执? build 命令即可:
$ docker build -t cnych/docker-multi-stage-demo:latest

默认情况下,构建阶段是没有命令的,我们可以通过它们的索引来引?它们,第?个 FROM 指令 从 0 开始,我们也可以? AS 指令为阶段命令,?如我们这?的将第?阶段命名为 build-env ,然后 在其他阶段需要引?的时候使? --from=build-env 参数即可。
最后我们简单的运?下该容器测试:
$ docker run --rm -p 8080:8080 cnych/docker-multi-stage-demo:latest

运?成功后,我们可以在浏览器中打开 http://127.0.0.1:8080/ping 地址,可以看到PONG返回。 现在我们就把两个镜像的?件最终合并到?个镜像??了。
云原生|【云原生丨Docker系列13】Docker 的多阶段构建详解
文章图片



多阶段构建是一个新特性,需要 Docker 17.05 或更高版本的守护进程和客户端。对于那些努力优化 Dockerfiles 并使其易于阅读和维护的人来说,多阶段构建非常有用。
云原生|【云原生丨Docker系列13】Docker 的多阶段构建详解
文章图片

    推荐阅读