透過 Multiple Stage Builds 編譯出最小的 Docker Image
I'm a Docker rookie 🐣. 本文章是記錄使用 Multi Stage Builds 的心得,若有錯誤歡迎告知,謝謝!
Docker 提供非常便利的 Dockerfile 讓我們可以自定義映像檔的環境設定、本機端的檔案複製、命令的執行等等,但是事實上,我們在製作這些容器的映像檔時,可能會需要安裝編譯環境、或是其他的依賴套件,不知不覺會造成映像檔非常的肥大。
前陣子在開發簡易 591 租屋網搜尋網站時,就打算借這個機會來學習 Docker,在看了文件之後,大概對 Docker 使用上有些概念了,也搜尋一些文章來看,例如怎麼撰寫一個好的 Dockerfile,剛好在 CodeTengu 碼天狗 的 ISSUE 87 中,看到 @vinta 前輩有分享一篇 How to write excellent Dockerfiles 的文章,覺得真的很受用啊,內容最後也提到了 Multi Stage Builds,所以也順便了解一下。
在搜尋了一些文章後,才了解 Multi Stage Builds 這件事,簡單來說,以前的作法需要多個 Dockerfile 來處理,@kevingo 前輩的 Go-Small-Docker-Image 就是一個很好的範例,詳細可以閱讀 README 了解,或者也可以閱讀 @appleboy 前輩的「用 Docker Multi-Stage 編譯出 Go 語言最小 Image」。
Docker 是在 17.05 版本後提供了 Multi Stage Builds 的 feature,翻成中文稱為「多階段構建」,在 17.05 版本之前,如果要做到多階段構建這件事情,如上所述,你需要多個 Dockerfile 才能完成,如果環境的構建比較複雜的話,可能需要維護多個 Dockerfile,而 Multi Stage Builds 可以解決需要維護多個 Dockerfile 的問題。
撰寫 Dockerfile
Build Stage
FROM golang:alpine AS builderWORKDIR /go/src/fiveN1RUN set -ex; \apk add --no-cache curl git \&& curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh \&& dep versionCOPY . .RUN dep ensure -v -vendor-only \&& CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
在 Build Stage 的部分:
FROM golang:alpine
:使用 golang 官方的映像檔WORKDIR /go/src/fiveN1
:指定工作的目錄Run set -ex; \...
:安裝curl
、git
,以及 Go 套件管理dep
COPY . .
:複製我目錄的檔案到容器內的目標位置,也就是上面WORKDIR
所指定的目錄位置RUN dep ensure -v -vendor-only \...
:安裝相關套件並編譯
第一階段(Build Stage)安裝相關套件後,並編譯檔案,Go 編譯出來的結果是一個 Binary 的可執行檔案。實際上,我們所需要的就是這個 Binary 的檔案,其餘都是不需要的。
在第一行可以注意到後面使用了一個 AS
的關鍵字,我們可以為 FROM
加上一個 <NAME>
名稱來作為標示,預設上,每個 Stage 是不會有名稱的,而是用數字作為代表,例如 Dockerfile 內有好幾個 FROM
,由上到下的順序就是從 0
到 n
,使用 AS <NAME>
可以讓 Dockerfile 更加易讀明白。
Final Stage
FROM alpineRUN apk --no-cache add ca-certificatesWORKDIR /root/COPY --from=builder /go/src/fiveN1/app .EXPOSE 8000CMD ["./app"]
在 Final Stage 部分:
FROM alpine
:使用alpine
的映像檔RUN apk --no-cache add ca-certificates
:安裝ca-certificates
套件WORKDIR /root/
:指定工作目錄COPY --from=builder /go/src/fiveN1/app .
:這段非常關鍵,注意到使用了一個參數--from
,這裡的builder
也就上方 Build Stage 所產生的 artifacts,我們在builder
的時候將目錄指定為/go/src/fiveN1/
,在編譯後會產生一個app
的檔案,我們把它複製到 Final Stage 的目錄來EXPOSE 8000
:開放 Port 8000 端口CMD ["./app"]
:執行 Binary 檔案
COPY --from=builder
會複製先前在 Build Stage 所產出的 artifacts 到目前的 Stage,但它們並不會被保留在 Final Stage,這樣一來就可以有效的減少映像檔的大小囉!
執行 Docker 映像檔的建立
我們將上面的 Dockerfile 整理如下:
# BUILD STAGEFROM golang:alpine AS builderWORKDIR /go/src/fiveN1RUN set -ex; \apk add --no-cache curl git \&& curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh \&& dep versionCOPY . .RUN dep ensure -v -vendor-only \&& CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .# FINAL STAGEFROM alpineRUN apk --no-cache add ca-certificatesWORKDIR /root/COPY --from=builder /go/src/fiveN1/app .EXPOSE 8000CMD ["./app"]
我們為映像檔加上名稱和 tag,並使用現在目錄下的 Dockerfile,接著執行 docker build
的指令:
$ docker build -t neighborhood999/docker-fiven1-demo .
構建映像檔的 Log 輸出結果如下:
Sending build context to Docker daemon 10.24kBStep 1/11 : FROM golang:alpine AS builder---> 52d894fca6d4Step 2/11 : WORKDIR /go/src/fiveN1Removing intermediate container 53dc21b7eba9---> 7b8a3a6ec6deStep 3/11 : RUN set -ex; apk add --no-cache curl git && curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh && dep version---> Running in cc3537cc7ede+ apk add --no-cache curl gitfetch http://dl-cdn.alpinelinux.org/alpine/v3.7/main/x86_64/APKINDEX.tar.gzfetch http://dl-cdn.alpinelinux.org/alpine/v3.7/community/x86_64/APKINDEX.tar.gz(1/6) Installing libssh2 (1.8.0-r2)(2/6) Installing libcurl (7.60.0-r1)(3/6) Installing curl (7.60.0-r1)(4/6) Installing expat (2.2.5-r0)(5/6) Installing pcre2 (10.30-r0)(6/6) Installing git (2.15.2-r0)Executing busybox-1.27.2-r7.triggerOK: 19 MiB in 18 packages+ curl https://raw.githubusercontent.com/golang/dep/master/install.sh+ sh% Total % Received % Xferd Average Speed Time Time Time CurrentDload Upload Total Spent Left Speed0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0ARCH = amd64100 4594 100 4594 0 0 3179 0 0:00:01 0:00:01 --:--:-- 3179OS = linuxWill install into /go/binFetching https://github.com/golang/dep/releases/latest..Release Tag = v0.4.1Fetching https://github.com/golang/dep/releases/tag/v0.4.1..Fetching https://github.com/golang/dep/releases/download/v0.4.1/dep-linux-amd64..Setting executable permissions.Moving executable to /go/bin/dep+ dep versiondep:version : v0.4.1build date : 2018-01-24git hash : 37d9ea0ago version : go1.9.1go compiler : gcplatform : linux/amd64Removing intermediate container cc3537cc7ede---> 9f66d10378ecStep 4/11 : COPY . .---> 83a8135a7802Step 5/11 : RUN dep ensure -v -vendor-only && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .---> Running in e4fb8efbaabb(2/6) Wrote golang.org/x/net@master(3/6) Wrote github.com/google/go-querystring@masterRemoving intermediate container e4fb8efbaabb---> 86e8d32cf39bStep 6/11 : FROM alpine---> 3fd9065eaf02Step 7/11 : RUN apk --no-cache add ca-certificates---> Running in f5d70d603698fetch http://dl-cdn.alpinelinux.org/alpine/v3.7/main/x86_64/APKINDEX.tar.gzfetch http://dl-cdn.alpinelinux.org/alpine/v3.7/community/x86_64/APKINDEX.tar.gz(1/1) Installing ca-certificates (20171114-r0)Executing busybox-1.27.2-r7.triggerExecuting ca-certificates-20171114-r0.triggerOK: 5 MiB in 12 packagesRemoving intermediate container f5d70d603698---> c226c28e5180Step 8/11 : WORKDIR /root/Removing intermediate container 05582c41c40d---> 1ffe05b3b07cStep 9/11 : COPY --from=builder /go/src/fiveN1/app .---> 12c5349b3dbeStep 10/11 : EXPOSE 8000---> Running in a65c82c7c81aRemoving intermediate container a65c82c7c81a---> 44c86c4b3185Step 11/11 : CMD ["./app"]---> Running in 5f9061907f1cRemoving intermediate container 5f9061907f1c---> 24f5e7bb146dSuccessfully built 24f5e7bb146dSuccessfully tagged neighborhood999/docker-fiven1-demo:latest
最後使用 docker images
看看構建後的映像檔大小:
$ docker imagesREPOSITORY TAG IMAGE ID CREATED SIZEneighborhood999/docker-fiven1-demo latest 24f5e7bb146d 5 minutes ago 14.3MB
哇!只有 14.3MB,看起來是成功啦!完整的範例請參考我的 GitHub docker-fiveN1。