在应用了容器技术的软件开发过程当中,控制容器镜像的大小但是一件费时费力的事情。若是咱们构建的镜像既是编译软件的环境,又是软件最终的运行环境,这是很难控制镜像大小的。因此常见的配置模式为:分别为软件的编译环境和运行环境提供不一样的容器镜像。好比为编译环境提供一个 Dockerfile.build,用它构建的镜像包含了编译软件须要的全部内容,好比代码、SDK、工具等等。同时为软件的运行环境提供另一个单独的 Dockerfile,它从 Dockerfile.build 中得到编译好的软件,用它构建的镜像只包含运行软件所必须的内容。这种状况被称为构造者模式(builder pattern),本文将介绍如何经过 Dockerfile 中的 multi-stage 来解决构造者模式带来的问题。html
好比咱们建立了一个 GO 语言编写了一个检查页面中超级连接的程序 app.go(请从 sparkdev 获取本文相关的代码):linux
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"os"
"strings"
"golang.org/x/net/html"
)
type scrapeDataStore struct {
Internal int `json:"internal"`
External int `json:"external"`
}
func isInternal(parsedLink *url.URL, siteUrl *url.URL, link string) bool {
return parsedLink.Host == siteUrl.Host || strings.Index(link, "#") == 0 || len(parsedLink.Host) == 0
}
func main() {
urlIn := os.Getenv("url")
if len(urlIn) == 0 {
urlIn = "https://www.cnblogs.com/"
log.Fatalln("Need a valid url as an env-var.")
}
siteUrl, parseErr := url.Parse(urlIn)
if parseErr != nil {
log.Fatalln(parseErr)
}
resp, err := http.Get(urlIn)
if err != nil {
log.Fatalln(err)
}
scrapeData := &scrapeDataStore{}
tokenizer := html.NewTokenizer(resp.Body)
end := false
for {
tt := tokenizer.Next()
switch {
case tt == html.StartTagToken:
// fmt.Println(tt)
token := tokenizer.Token()
switch token.Data {
case "a":
for _, attr := range token.Attr {
if attr.Key == "href" {
link := attr.Val
parsedLink, parseLinkErr := url.Parse(link)
if parseLinkErr == nil {
if isInternal(parsedLink, siteUrl, link) {
scrapeData.Internal++
} else {
scrapeData.External++
}
}
if parseLinkErr != nil {
fmt.Println("Can't parse: " + token.Data)
}
}
}
break
}
case tt == html.ErrorToken:
end = true
break
}
if end {
break
}
}
data, _ := json.Marshal(&scrapeData)
fmt.Println(string(data))
}
下面咱们经过容器来构建它,并把它部署到生产型的容器镜像中。
首先构建编译应用程序的镜像:git
FROM golang:1.7.3 WORKDIR /go/src/github.com/sparkdevo/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 .
把上面的内容保存到 Dockerfile.build 文件中。github
接着把构建好的应用程序部署到生产环境用的镜像中:golang
FROM alpine:latest RUN apk --no-cache add ca-certificates WORKDIR /root/ COPY app . CMD ["./app"]
把上面的内容保存到 Dockerfile 文件中。docker
最后须要使用一个脚本把整个构建过程整合起来:json
#!/bin/sh echo Building sparkdevo/href-counter:build # 构建编译应用程序的镜像 docker build --no-cache -t sparkdevo/href-counter:build . -f Dockerfile.build # 建立应用程序 docker create --name extract sparkdevo/href-counter:build
# 拷贝编译好的应用程序 docker cp extract:/go/src/github.com/sparkdevo/href-counter/app ./app docker rm -f extract echo Building sparkdevo/href-counter:latest # 构建运行应用程序的镜像 docker build --no-cache -t sparkdevo/href-counter:latest .
把上面的内容保存到 build.sh 文件中。这个脚本会先建立出一个容器来构建应用程序,而后再建立最终运行应用程序的镜像。
把 app.go、Dockerfile.build、Dockerfile 和 build.sh 放在同一个目录下,而后进入这个目录执行 build.sh 脚本进行构建。构建后的容器镜像大小:网络
从上图中咱们能够观察到,用于编译应用程序的容器镜像大小接近 700M,而用于生产环境的容器镜像只有 10.3 M,这样的大小在网络间传输的效率是很高的。app
运行下面的命令能够检查咱们构建的容器是否能够正常的工做:ide
$ docker run -e url=https://www.cnblogs.com/ sparkdevo/href-counter:latest $ docker run -e url=http://www.cnblogs.com/sparkdev/ sparkdevo/href-counter:latest
OK,咱们写的程序正确的统计了博客园首页和笔者的首页中超级连接的状况。
采用上面的构建过程,咱们须要维护两个 Dockerfile 文件和一个脚本文件 build.sh。能不能简化一些呢? 下面咱们看看 docker 针对这种状况提供的解决方案:multi-stage。
multi-stage 容许咱们在 Dockerfile 中完成相似前面 build.sh 脚本中的功能,每一个 stage 能够理解为构建一个容器镜像,后面的 stage 能够引用前面 stage 中建立的镜像。因此咱们可使用下面单个的 Dockerfile 文件实现前面的需求:
FROM golang:1.7.3 WORKDIR /go/src/github.com/sparkdevo/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/sparkdevo/href-counter/app . CMD ["./app"]
把上面的内容保存到文件 Dockerfile.multi 中。这个 Dockerfile 文件的特色是同时存在多个 FROM 指令,每一个 FROM 指令表明一个 stage 的开始部分。咱们能够把一个 stage 的产物拷贝到另外一个 stage 中。本例中的第一个 stage 完成了应用程序的构建,内容和前面的 Dockerfile.build 是同样的。第二个 stage 中的 COPY 指令经过 --from=0 引用了第一个 stage ,并把应用程序拷贝到了当前 stage 中。接下来让咱们编译新的镜像:
$ docker build --no-cache -t sparkdevo/href-counter:multi . -f Dockerfile.multi
此次使用 href-counter:multi 镜像运行应用:
$ docker run -e url=https://www.cnblogs.com/ sparkdevo/href-counter:multi $ docker run -e url=http://www.cnblogs.com/sparkdev/ sparkdevo/href-counter:multi
结果和以前是同样的。那么新生成的镜像有没有特别之处呢:
好吧,从上图咱们能够看到,除了 sparkdevo/href-counter:multi 镜像,还生成了一个匿名的镜像。所以,所谓的 multi-stage 不过期多个 Dockerfile 的语法糖罢了。可是这个语法糖还好很诱人的,如今咱们维护一个结构简洁的 Dockerfile 文件就能够了!
在上面的例子中咱们经过 --from=0 引用了 Dockerfile 中第一个 stage,这样的作法会让 Dockerfile 变得不容易阅读。其实咱们是能够为 stage 命名的,而后就能够经过名称来引用 stage 了。下面是改造后的 Dockerfile.mult 文件:
FROM golang:1.7.3 as builder WORKDIR /go/src/github.com/sparkdevo/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/sparkdevo/href-counter/app . CMD ["./app"]
咱们把第一个 stage 使用 as 语法命名为 builder,而后在后面的 stage 中经过名称 builder 进行引用 --from=builder。经过使用命名的 stage, Dockerfile 更容易阅读了。
Dockerfile 中的 multi-stage 虽然只是些语法糖,但它确实为咱们带来了不少便利。尤为是减轻了 Dockerfile 维护者的负担(要知道实际生产中的 Dockerfile 可不像 demo 中的这么简单)。须要注意的是旧版本的 docker 是不支持 multi-stage 的,只有 17.05 以及以后的版本才开始支持。好了,是否是该去升级你的 docker 版本了?
参考:
Use multi-stage builds
Builder pattern vs. Multi-stage builds in Docker