用 Docker Multi-Stage 編譯出 Go 語言最小 Image

docker 之前應該沒寫過用 Docker 結合 Go 語言編譯出最小 Image 的文章,剛好趁這機會來介紹。其實網路上可以直接找到文章,像是這篇『Building Minimal Docker Containers for Go Applications』,那本文來介紹 Docker 新功能 multi-stage builds,此功能只有在 17.05.0-ce 才支援,看起來是 2017/05/03 號會 release 出來。我們拿 Go 語言的 Hello World 來介紹 Single build 及 Multiple build。

Single build

底下是 Go 語言 Hello World 範例:
package main

import "fmt"

func main() {
    fmt.Println("Hello World!")
}
接著用 alpine 的 Go 語言 Image 來編譯出執行檔。
FROM golang:alpine
WORKDIR /app
ADD . /app
RUN cd /app && go build -o app
ENTRYPOINT ./app
接著執行底下編譯指令:
$ docker build -t appleboy/go-app .
$ docker run --rm appleboy/go-app
最後檢查看看編譯出來的 Image 大小,使用 docker images | grep go-app,會發現 Image 大小為 258 MB

Multiple build

Multiple build 則是可以在 Dockerfile 使用多個不同的 Image 來源,請看看底下範例
# build stage
FROM golang:alpine AS build-env
ADD . /src
RUN cd /src && go build -o app

# final stage
FROM alpine
WORKDIR /app
COPY --from=build-env /src/app /app/
ENTRYPOINT ./app
從上面可以看到透過 AS--from 互相溝通,以前需要寫兩個 Dockerfile,現在只要一個就可以搞定。最後一樣執行編譯指令:
$ docker build -t appleboy/go-app .
$ docker run --rm appleboy/go-app
會發現最後大小為 6.35 MB,比上面是不是小了很多。

最小 Image?

6.35 MB 是最小的 Image 了嗎?才單單一個 Hello World 執行檔,用 Docker 包起來竟然要 6.35,其實不用這麼大,我們可以透過 Dokcer 所提供的最小 Image: scratch,將執行檔直接包進去即可,在編譯執行檔需加入特定參數才可以:
$ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app
再透過 Docker 包起來
FROM centurylink/ca-certs

ADD app /

ENTRYPOINT ["/app"]
編譯出來大小為: 1.81MB,相信這是最小的 Image 了。最後用 Docker 來包
# build stage
FROM golang:alpine AS build-env
ADD . /src
RUN cd /src && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app

# final stage
FROM centurylink/ca-certs
COPY --from=build-env /src/app /
ENTRYPOINT ["/app"]

結論

Multiple build 非常方便,這樣就可以將多個步驟全部合併在一個 Dockerfile 處理掉,像是底下例子
from debian as build-essential
arg APT_MIRROR
run apt-get update
run apt-get install -y make gcc
workdir /src

from build-essential as foo
copy src1 .
run make

from build-essential as bar
copy src2 .
run make

from alpine
copy --from=foo bin1 .
copy --from=bar bin2 .
cmd ...
用一個 Dockerfile 產生多個執行檔,最後再用 alpine 打包成 Image。

附上本篇程式碼範例