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

前言

多阶段构建指在Dockerfile中使用多个FROM语句,每个指令都可使用不同的基础镜像且是一个独立的子构建阶段。使用多阶段构建打包应用具有构建安全、构建速度快、镜像文件体积小等优点。

【云原生丨Docker系列13】Docker 的多阶段构建详解_第1张图片


目录

1️⃣引入

2️⃣示例

3️⃣解决方案

4️⃣多阶段构建


1️⃣引入

Docker 的⼝号是 Build,Ship,and Run Any App,Anywhere(一次封装,到处运行),在我们使⽤ Docker 的⼤部分时候,的确能感觉到其优越性,但是往往在我们 Build ⼀个应⽤的时候,是将我们的源代码也构建进去的,这对于类似于 golang 这样的编译型语⾔肯定是不⾏的,因为实际运⾏的时候我只需要把最终构建的⼆进制包 给你就⾏,把源码也⼀起打包在镜像中,需要承担很多⻛险,即使是脚本语⾔,在构建的时候也可能 需要使⽤到⼀些上线的⼯具,这样⽆疑也增⼤了我们的镜像体积。

【云原生丨Docker系列13】Docker 的多阶段构建详解_第2张图片

构建镜像时最具挑战性的事情之一就是缩小镜像大小。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 的多阶段构建详解_第3张图片

有没有⼀种更加简单的⽅式来实现上⾯的镜像构建过程呢?

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 的多阶段构建详解_第4张图片


多阶段构建是一个新特性,需要 Docker 17.05 或更高版本的守护进程和客户端。对于那些努力优化 Dockerfiles 并使其易于阅读和维护的人来说,多阶段构建非常有用。 


【云原生丨Docker系列13】Docker 的多阶段构建详解_第5张图片

你可能感兴趣的:(云原生,从入门到进阶,docker,java,ubuntu)