Dockerfile 中的 multi-stage(多階段構建)

在應用了容器技術的軟件開發過程當中,控制容器鏡像的大小但是一件費時費力的事情。若是咱們構建的鏡像既是編譯軟件的環境,又是軟件最終的運行環境,這是很難控制鏡像大小的。因此常見的配置模式爲:分別爲軟件的編譯環境和運行環境提供不一樣的容器鏡像。好比爲編譯環境提供一個 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。

在 Dockerfile 中使用 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 文件就能夠了!

使用命名的 stage

在上面的例子中咱們經過 --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

相關文章
相關標籤/搜索