Docker 映像檔(image)大小在 production 環境中也是相當重要的一環。
如果 Docker 映像檔太大,不僅佔用傳輸頻寬,也會拉長部署(deployment)的時間,因此如何優化 Docker 映像檔案大小是一門重要課題。
優化 Docker 映像檔大小有若干種方法,其中一種為 multi-stage build, 不過 Docker 官方文件所提供的 multi-stage builds 範例卻無法正常運作,本篇將修正該範例並實際體驗 multi-stage builds 的效果。
本文環境
- macOS 11.6
- Docker Desktop 3.6.0
Multi-stage builds
談 multi-stage builds 之前先看以下的 Dockerfile 範例:
FROM golang:1.16
WORKDIR /go/src/github.com/alexellis/href-counter/
COPY ./app.go ./
RUN GO111MODULE=off go get -d -v golang.org/x/net/html && \
GO111MODULE=auto CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . && \
apt-get install ca-certificates
CMD ["./app"]
上述 Dockerfile 嘗試以 golang 1.16 的 Docker 映像檔編譯 alexellis/href-counter, 從範例中可以看到 RUN
指令下載 go module 以及執行編譯 go 執行檔,更透過 apt-get
安裝 ca-certificates
套件,最後產出 1 個執行檔(executable) app
。
如欲編譯請先下載 alexellis/href-counter, 並修改其 Dockerfile 成上述範例,指令如下:
$ git clone https://github.com/alexellis/href-counter
$ cd href-counter
$ vim Dockerfile
該範例編譯完成後約 989MB, 相對於編譯完成的可執行檔 app 來說,可說是相當龐大:
$ docker build -t alexellis2/href-counter:latest .
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
alexellis2/href-counter latest 2013ff215d52 29 seconds ago 989MB
之所以造成如此大的映像檔,其原因在於 golang 1.16 的映像檔本身就佔據至少 900MB 的空間,如果能夠使用更小映像檔就肯定能夠有效將映像檔變小。
如果我們更進一步檢視編譯流程,其實可以發現整個過程只有最後產出的可執行檔以及安裝 ca-certificates 套件是必須的,要是能夠把最終編譯完成的可執行檔以及安裝 ca-certificates 的步驟放進去 1 個最小的 Docker 映像檔內的話,那麼映像檔將可以變得更小。
要達成這個目標,可以將步驟拆解成以下 2 個步驟:
- 編譯可執行檔
- 搬移步驟 1 產出的可執行檔至合適的 docker 映像檔內
這就是 multi-stage builds 的思路。
Multi-stage builds
Multi-stage builds 是 Docker Engine 17.05 之後支援的 1 項新功能,除了能夠讓開發者能夠優化 Docker 映像檔之外,也能夠讓 Dockerfile 的管理更結構化以及更好閱讀,這是得益於能夠透過 multi-stage builds 將 Docker 映像檔拆解成多個步驟的原因。
multi-stage builds are useful to anyone who has struggled to optimize Dockerfiles while keeping them easy to read and maintain.
不過實際執行 Docker 官方文件所提供的範例將會出現以下錯誤:
=> [internal] load build definition from Dockerfile ...(略)...
=> CACHED [builder 4/5] COPY app.go ./ 0.0s
=> ERROR [builder 5/5] RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . 0.5s
------
> [builder 5/5] RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .:
#13 0.485 go: go.mod file not found in current directory or any parent directory; see 'go help modules'
------
executor failed running [/bin/sh -c CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .]: exit code: 1
上述錯誤是由於 golang 1.16 之後 go module 預設為開啟的原因,因此我們需要稍作修改將 go module 的功能關掉,再進行編譯方可成功。
接下來的 Dockerfile 範例同樣需先下載 alexellis/href-counter, 並修改其 Dockerfile 成下方 Docker multi-stage builds 範例,指令如下:
$ git clone https://github.com/alexellis/href-counter
$ cd href-counter
Docker multi-stage builds 範例如下:
FROM golang:1.16 AS builder
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN GO111MODULE=off go get -d -v golang.org/x/net/html
COPY app.go ./
RUN GO111MODULE=auto 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/alexellis/href-counter/app ./
CMD ["./app"]
上述 Dockerfile 範例中 AS builder
即是 multi-stage builds 所支援的功能,讓開發者能夠為映像檔編譯步驟命名, AS builder
為將該映像檔命名為 builder, 然後該映像檔只負責編譯 go 的可執行欓 app, 也就是 RUN GO111MODULE=auto CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
的部分。
緊接著, COPY --from=builder /go/src/github.com/alexellis/href-counter/
也是 multi-stage builds 所支援的功能,讓我們從 builder 映像檔(也就是 --from=builder
的部分)中將可執行檔 /go/src/github.com/alexellis/href-counter/app
複製到另 1 個 Docker 映像檔 /root/
路徑底下,變成第 2 個 Docker 映像檔,同時這個映像檔是以 alpine 為基底,該映象檔相當小,也能夠大幅度縮小最終的映像檔所佔用的空間,如此就完成 multi-stage builds 了。
編譯指令如下:
$ docker build -t alexellis2/href-counter:latest .
編譯成功的話,可以輸入指令 docker images
查看,可以發現最終的映像檔僅有 12.7 MB 的大小:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
alexellis2/href-counter latest 31063291ead7 2 days ago 12.7MB
總結
Multi-stage builds 相當適合像 golang, C, C++ 等需要編譯的程式語言,善用該功能除了可以讓 Dockerfile 更加結構化之外,搭配合適的映像檔也能夠有效優化 Docker 映像欓的大小。
Docker 官方文件中也提及更多關於 multi-stage builds 的用法,其中也有在 Docker Engine 17.05 以前是如何透過其他方式完成 multi-stage builds 的作法,有空的話也可以閱讀閱讀該文件。
以上, happy coding!
References
https://docs.docker.com/develop/develop-images/multistage-build/