跳至主要内容

4.13 Golang 應用程式的多階段建置

這邊的範例放在本書 GitHub 儲存庫中 ch-04 的 golang-multi-stage-example

接著我們進入到資料夾內,並且建置映像檔。

$ docker image build --tag golang-example .
[+] Building 2.8s (11/11) FINISHED
=> [internal] load build definition from Dockerfile 0.1s
=> => transferring dockerfile: 233B 0.0s
=> [internal] load .dockerignore 0.1s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/golang:alpine3.16 0.0s
=> [auth] library/ruby:pull token for registry-1.docker.io 2.5s
=> [1/5] FROM docker.io/library/golang:alpine3.16@sha256:d475ce... 0.0s
=> [internal] load build context 0.1s
=> => transferring context: 290B 0.1s
=> CACHED [2/5] WORKDIR /app 0.0s
=> CACHED [3/5] RUN export GO111MODULE=on && go mod init example.com/m/v2 0.0s
=> CACHED [4/5] COPY main.go ./ 0.0s
=> CACHED [5/5] RUN go build -o ./server 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:db9d62ed38ad68d1bb8f555e376cf9290cd062315.... 0.0s
=> => naming to docker.io/library/golang-example 0.0s

接著將建置完的映像檔運行成容器,先確認這個映像檔是可以使用的。

$ docker container run --publish 8000:8000 --detach golang-example # 不換行
b7f10fba5e0f4f3d670134c44007c0e69795cc2547a9...

接著打開瀏覽器輸入 http://localhost:8000,會看到『 Hello,這是一個用 Golang 建置的 WebServer 』字樣。

確定可以使用之後,我們來看一下這個映像檔的大小:

$ docker image list --filter=reference='golang-example'
REPOSITORY TAG IMAGE ID CREATED SIZE
golang-example latest db9d62ed38ad 18 minutes ago 359MB

目前是 359MB,我們的最終目標可以讓映像檔剩下約 15MB 左右,這樣在傳輸的速度就會超級快,且能達到的目的是一樣的,讓我們著手來修改這個 Dockerfile 吧!

FROM golang:alpine3.16

CMD ["/app/server"]

EXPOSE 8000

WORKDIR /app

RUN export GO111MODULE=on && \
go mod init example.com/m/v2

COPY main.go ./

RUN go build -o ./server

首先要寫好 Dockerfile 的多階段建置,對於該程式語言的應用程式需要有基礎的了解,以編譯式的程式語言來說,需要先把應用程式編譯好,接著只需要執行這個編譯完的檔案就能夠啟動服務。

所以預想中的流程是編譯出這個 Golang 的應用程式,並且只把它帶到下一個階段然後執行它,這樣就能大幅度地減少映像檔的容量。

先把第一階段寫出來,首先進入一個叫做 app 的檔案目錄,並且執行 Golang 會使用到的指令,之後複製主要的檔案 main.go 進到映像檔中,然後編譯它。

FROM golang:alpine3.16 AS builder

WORKDIR /app

RUN export GO111MODULE=on && \
go mod init example.com/m/v2

COPY main.go ./

RUN go build -o ./server

這邊我們就完成了第一階段,接著我們需要複製第一階段編譯完叫做 server 的檔案到第二階段,並且執行它。

FROM golang:alpine3.16 AS builder

WORKDIR /app

RUN export GO111MODULE=on && \
go mod init example.com/m/v2

COPY main.go ./

RUN go build -o ./server

-----階段分界示意線----- # 不要寫進 Dockerfile
FROM alpine:latest

WORKDIR /app

CMD ["/app/server"]

EXPOSE 8000

COPY --from=builder /app/server /app/server <- 複製第一階段的檔案到最終階段

接著我們再重新建置一次映像檔,並且貼上不同的標籤以便等等可以做比較。

$ docker image build --tag golang-min-example .
[+] Building 4.7s (15/15) FINISHED
=> [internal] load build definition from Dockerfile 0.1s
=> => transferring dockerfile: 37B 0.0s
=> [internal] load .dockerignore 0.1s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/golang:alpine3.16 4.4s
=> [internal] load metadata for docker.io/library/alpine:latest 0.0s
=> [auth] library/ruby:pull token for registry-1.docker.io 0.0s
=> [builder 1/5] FROM docker.io/library/golang:alpine3.16@sha25... 0.0s
=> [stage-1 1/3] FROM docker.io/library/alpine:latest 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 29B 0.0s
=> CACHED [stage-1 2/3] WORKDIR /app 0.0s
=> CACHED [builder 2/5] WORKDIR /app 0.0s
=> CACHED [builder 3/5] RUN export GO111MODULE=on && go mod.... 0.0s
=> CACHED [builder 4/5] COPY main.go ./ 0.0s
=> CACHED [builder 5/5] RUN go build -o ./server 0.0s
=> CACHED [stage-1 3/3] COPY --from=builder /app/server /app/server 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:30e6f003267f9c910fbd2f3e0f88e93fe61595....... 0.0s
=> => naming to docker.io/library/golang-example 0.0s

接著一樣將其運行成容器,試試看是不是多階段建置也能夠達到相同的效果。

$ docker container rm --force $(docker container ls --all --quiet)
# 先清掉所有容器,避免 port 衝突

$ docker container run --publish 8000:8000 --detach golang-min-example # 不換行
c638511e9dd3f9af5eb195a948bd76caa3...

再來一樣打開瀏覽器輸入 http://localhost:8000,就會看到和上次啟動容器時一模一樣的畫面,證明這個方式建置的映像檔可以達到一樣的目的。

再來就是要揭曉映像檔容器大小的時刻了,讓我們來看一下到底差了多少。

$ docker image list --filter=reference='golang-*'
REPOSITORY TAG IMAGE ID CREATED SIZE
golang-min-example latest 30e6f003267 49 minutes ago 12MB
golang-example latest db9d62ed38a 30 minutes ago 359MB

足足差了 347 MB,但做的事情是一模一樣的,這就是有沒有使用多階段建置的差別,別小看這 347 MB,現在所流行的微服務就是透過多個不同的映像檔組成一個完整的應用程式,若是每一個都有 350 MB 的大小,那累積起來的容量差距將更明顯。

而且把不需要的編譯工具丟掉,一方面也提升了應用程式的安全性,當一個應用程式的執行環境愈乾淨的時候,可以攻擊的漏洞就會大幅度地減少。

但這麼巨大的容量差距只會發生在編譯式的程式語言的應用程式上,畢竟其可以只透過編譯後的檔案來運行應用程式,而直譯式的程式語言就沒有辦法減少那麼多的容量,但還是有不少的空間可以縮小,就讓我們接著繼續看看下一個示範吧!