POABOB

小小工程師的筆記分享

0%

Golang 最佳化打包 Docker Image 至上線環境

前言

由於 Docker 讓我們開發者不論是在開發還是上線環境有一個統一的環境可以讓我們執行,導致容器化的部屬方式變得非常熱門。
然而,每次當我們在 Docker 打包 golang 的執行檔時,常常發現鏡像(Docker Image)佔用空間非常大,甚至可能有幾GB的大小這麼多。於是,優化其鏡像大小成為了一個重要的議題。
本篇文章將會將實際專案,從原本1.51GB大小的鏡像縮減至38MB,如果對於本文有更好的建議也歡迎留言提出來。

初始Dockerfile

Dockerfile.prod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
FROM golang:1.19-alpine AS builder

# 建立環境變數
ENV GO111MODULE=auto \
CGO_ENABLED=0 \
GOOS=linux \
GOARCH=amd64

# 指定工作目錄
WORKDIR /app/go/

# 把當前專案複製到/app/go裡
COPY . /app/go

# 安裝環境依賴函式庫
RUN go mod tidy
RUN go mod download
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o go-pano.output *.go
RUN go install github.com/rubenv/sql-migrate/...@latest

# 暴露端口
EXPOSE 80

# 執行
CMD ["/app/go/go-pano.output"]

當我一開始執行 docker build 的時候,發現打包後竟然高達1.51GB,這樣線上環境如果要pull的話肯定很花時間和流量。

1
2
3
4
$ docker build -f ./Dockerfile.prod -t poabob/pano-go:prod . 
$ docker image ls poabob/pano-go
REPOSITORY TAG IMAGE ID CREATED SIZE
poabob/pano-go prod 4ae56d080e02 34 seconds ago 1.51GB

優化流程

使用 .dockerignore 避免非必要的文件打包進入鏡像

我們可以在原始檔案發現在打包的時候會將本地專案目錄的資料完全複製到容器之中,可是如果專案目錄有些檔案本來就沒有必要被打包進去的話,勢必就要來避免這些檔案的移動。

1
COPY . /app/go

我們可以使用 .dockerignore 來聲明哪些檔案在打包的時候要避免掉(用法跟.gitignore一樣)。

.dockerignore

1
2
3
4
5
6
7
8
9
10
11
12
13
**/.git
**/.DS_Store

# 裡面有存放開發環境留下來的volume
**/dist

*.log
Dockerfile*
docker-compose*
.dockerignore
.git
.gitignore
.vscode

接著我們可以再來重新打包試試看大小有無變化。

1
2
3
4
$ docker build -f ./Dockerfile.prod -t poabob/pano-go:prod .
$ docker image ls poabob/pano-go
REPOSITORY TAG IMAGE ID CREATED SIZE
poabob/pano-go prod a7629caa7217 15 seconds ago 946MB
  • 結果將原本的 1.51GB 縮減至 946MB,原因是因為 dist 目錄中有python 的 service 本來就不該被打包進來。

減少使用會增加鏡像layer的指令

鏡像中的 layer 與我們 Git 的 commit 一樣,用來區分版本與版本之間的差異,藉此來我們在重複打包的時候,可以藉由原本 Layer 中儲存的 Cache 來節省我們打包的時間。

但是鏡像的 layer 是會佔空間的,所以每當我們打包環境的層數越多,就會讓該鏡像越肥大。目前有三種指令會增加 layer 的數量。

  • RUN
  • ADD
  • COPY

Dockerfile.prod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
FROM golang:1.19-alpine AS builder

# 建立環境變數
ENV GO111MODULE=auto \
CGO_ENABLED=0 \
GOOS=linux \
GOARCH=amd64

# 指定工作目錄
WORKDIR /app/go/

# 把當前專案複製到/app/go裡
COPY . /app/go

# 安裝環境依賴函式庫
RUN go mod tidy \
&& go mod download \
&& CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o go-pano.output *.go \
&& go install github.com/rubenv/sql-migrate/...@latest

# 暴露端口
EXPOSE 80

# 執行
CMD ["/app/go/go-pano.output"]
1
2
3
4
$ docker build -f ./Dockerfile.prod -t poabob/pano-go:prod . 
$ docker image ls poabob/pano-go
REPOSITORY TAG IMAGE ID CREATED SIZE
poabob/pano-go prod 615b8016ccf7 15 seconds ago 946MB
  • 結果還是原本的 946MB QQ,理論上應該縮小的。

使用更小的鏡像執行環境

當我們在 golang 官方鏡像的 DockerHub 可以看到,每個鏡像都有不同的標籤來當作後綴,而我一開始就使用 alpine 作為打包環境其實已經算是最小了,其他標籤解釋如下:

  • buster
    • Debian LTS 版本 10.7,其代號就是 Buster。所以原始環境是以 Debian 作為底層作業系統,並且擁有完整的相關依賴函式庫,缺點就是肥大
  • alpine
    • 基於 Alpine Linux 所產生的鏡像,其佔用空間算是最小的,大多數人都會使用它來作為縮小鏡像大小的手段,但是他就是因為什麼都沒有,很容易遇到相關依賴函式庫不支援的狀況
  • slim
    • 相對來說是上述兩者的折衷選擇,提供較少的資源,達到減少空間的效果,但是專案能不能正常運行還是要實際測試才知道。

使用多階段構建(multistage builds),不打包執行環境到鏡像中

上述操作流程我們很明顯的可以看到有把 golang 的執行環境給整個打包進來,可是我們的應用程式都打包成為二進位制檔案了,根本不需要這個開發環境
於是 Dockerfile 可以讓我們在同一個檔案內,使用不同鏡像進行多階段構建(multistage builds),我們只需要打包好執行檔,並將執行檔打包進去另外一個乾淨的執行環境即可。

Dockerfile.prod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# Build Stage
FROM golang:1.19-alpine AS builder

# 建立環境變數
ENV GO111MODULE=auto \
CGO_ENABLED=0 \
GOOS=linux \
GOARCH=amd64

# 指定工作目錄
WORKDIR /app/go/

# 把當前專案複製到/app/go裡
COPY . /app/go

# 安裝環境依賴函式庫
RUN go mod tidy \
&& go mod download \
&& CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o go-pano.output *.go

# Deploy Stage
FROM alpine:3.16.2 AS prod

# 指定工作目錄
WORKDIR /app/go/

COPY --from=builder /app/go/go-pano.output /app/go/go-pano.output
COPY --from=builder /app/go/config-prod.yml /app/go/config.yml

# 暴露端口
EXPOSE 80

# 執行
CMD ["/app/go/go-pano.output"]
1
2
3
4
$ docker build -f ./Dockerfile.prod -t poabob/pano-go:prod . 
$ docker image ls poabob/pano-go
REPOSITORY TAG IMAGE ID CREATED SIZE
poabob/pano-go prod d1db126af432 25 minutes ago 38MB
  • 結果將 946MB 大幅度的縮減至 38MB,並且測試過後基本上沒有任何功能上的缺失。

結論

本篇文章透過以下方式來減少我們在打包鏡像所遇到的鏡像肥大問題。

  1. 使用 .dockerignore
  2. 減少使用會增加鏡像layer的指令
  3. 使用更小的鏡像執行環境
  4. 使用多階段構建(multistage builds)

其中本篇是基於 golang 作為使用的範例,根據不同的程式語言像是 python 這種直譯式語言可能就不符合使用多階段構建(multistage builds)的方式,所以根據條件以及需求還煩請各位大大審慎評估後使用,畢竟很常聽說 alpine 鏡像的坑大大小小都有,最後還是得讓應用可以正常運行才是主要的任務。

參考資料

  • https://juejin.cn/post/7126754041442336775
  • http://blog.itpub.net/70002215/viewspace-2781629/
  • https://www.timiguo.com/archives/223/
  • https://hub.docker.com/_/golang
------ 本文結束 ------