在应用了容器技术的软件开发过程中,控制容器镜像的大小可是一件费时费力的事情。如果我们构建的镜像既是编译软件的环境,又是软件最终的运行环境,这是很难控制镜像大小的。所以常见的配置模式为:分别为软件的编译环境和运行环境提供不同的容器镜像。比如为编译环境提供一个 Dockerfile.build,用它构建的镜像包含了编译软件需要的所有内容,比如代码、SDK、工具等等。同时为软件的运行环境提供另外一个单独的 Dockerfile,它从 Dockerfile.build 中获得编译好的软件,用它构建的镜像只包含运行软件所必须的内容。这种情况被称为构造者模式(builder pattern),本文将介绍如何通过 Dockerfile 中的 multi-stage 来解决构造者模式带来的问题。
常见的容器镜像构建过程比如我们创建了一个 GO 语言编写了一个检查页面中超级链接的程序 app.go(请从 sparkdev 获取本文相关的代码):
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/"
}
resp, err := http.Get(urlIn)
scrapeData := &scrapeDataStore{}
tokenizer := html.NewTokenizer(resp.Body)
end := false for {
tt := tokenizer.Next()
switch {
case tt == html.StartTagToken:
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))
}
下面我们通过容器来构建它,并把它部署到生产型的容器镜像中。
首先构建编译应用程序的镜像:
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 文件中。
接着把构建好的应用程序部署到生产环境用的镜像中:
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY app .
CMD ["./app"]
把上面的内容保存到 Dockerfile 文件中。
最后需要使用一个脚本把整个构建过程整合起来: