前言
多阶段构建指在Dockerfile中使用多个FROM语句,每个指令都可使用不同的基础镜像且是一个独立的子构建阶段。使用多阶段构建打包应用具有构建安全、构建速度快、镜像文件体积小等优点。
目录
1️⃣引入
2️⃣示例
3️⃣解决方案
4️⃣多阶段构建
Docker 的⼝号是 Build,Ship,and Run Any App,Anywhere(一次封装,到处运行),在我们使⽤ Docker 的⼤部分时候,的确能感觉到其优越性,但是往往在我们 Build ⼀个应⽤的时候,是将我们的源代码也构建进去的,这对于类似于 golang 这样的编译型语⾔肯定是不⾏的,因为实际运⾏的时候我只需要把最终构建的⼆进制包 给你就⾏,把源码也⼀起打包在镜像中,需要承担很多⻛险,即使是脚本语⾔,在构建的时候也可能 需要使⽤到⼀些上线的⼯具,这样⽆疑也增⼤了我们的镜像体积。
构建镜像时最具挑战性的事情之一就是缩小镜像大小。Dockerfile 中的每一条指令都会在镜像中添加一个层,在进入下一层之前,您需要记住清除所有不需要的工件。要编写一个真正高效的 Dockerfile,传统上需要使用 shell 技巧和其他逻辑来保持层尽可能小,并确保每一层都有它需要的来自前一层的工件,而没有其他东西。
实际上,有一个 Dockerfile 用于开发环境(包含构建应用程序所需的所有内容),同时有一个精简的 Dockerfile 用于生产环境(仅包含应用程序和运行应用程序所需的内容)是非常常见的。这被称为“建造者模式”。
⽐如我们现在有⼀个最简单的 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")
}
我们最终的⽬的都是将最终的可执⾏⽂件放到⼀个最⼩的镜像(⽐如 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
当我们执⾏完上⾯的构建脚本后,就实现了我们的⽬标。
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 17.05 或更高版本的守护进程和客户端。对于那些努力优化 Dockerfiles 并使其易于阅读和维护的人来说,多阶段构建非常有用。