昨天我們成功地利用 Dockerfile 打包了一個 image,但是關於 image 的大小與打包的流程都可以做最佳化,今天就來修改昨天的專案,繼續的往最佳化之路邁進吧!
昨天的專案完成如下
https://github.com/codingXiang/random_anonymous_chat/tree/docker
這邊我們不會對 make
指令多最描述,以興趣的朋友可以看 這裡 的介紹。
透過 Makefile
可以使得開發與發布更為方便且迅速,以下在 Makefile
中編寫
透過變數將 go
的相關指令進行封裝
GO_CMD=go
GO_BUILD=$(GO_CMD) build
GO_CLEAN=$(GO_CMD) clean
GO_TEST=$(GO_CMD) test
GO_GET=$(GO_CMD) get
GO_VET=$(GO_CMD) vet
GO_RUN=$(GO_CMD) run
GO_MOD_DEP=$(GO_CMD) mod download
ALL_PATH=./...
將輸出的編譯檔也定義成變數
BINARY_NAME=app
UNPACK_PATH=$$path
將 docker
相關指令定義成變數
DOCKER_CMD=docker
DOCKER_BUILD=$(DOCKER_CMD) build
DOCKER_PUSH=$(DOCKER_CMD) push
DOCKER_IMAGE_NAME=random_anonymous_chat
通過不同的 target
實現系列的步驟
將 go 相關情境封裝成 target
deps:
$(GO_MOD_DEP)
test:
$(GO_TEST) -v $(ALL_PATH) -cover
build:
$(GO_BUILD) -o $(BINARY_NAME)
run:
$(GO_RUN) main.go
clean:
$(GO_CLEAN)
rm -f $(BINARY_NAME)
pack:
tar -cvzf $(BINARY_NAME)-v$(VERSION).tar.gz $(BINARY_NAME) ./config ./template
unpack:
tar -zxf $(BINARY_NAME).tar.gz -C $(UNPACK_PATH)
將 docker 相關情境封裝成 target
docker_build:
@echo "開始打包 Docker Image - $(DOCKER_FULL_IMAGE)"
$(DOCKER_BUILD) -t $(DOCKER_IMAGE_NAME) .
docker_push:
@echo "開始 push docker image - $(DOCKER_FULL_IMAGE)"
$(DOCKER_PUSH) $(DOCKER_IMAGE_NAME)
完整的 makefile 如下
GO_CMD=go
GO_BUILD=$(GO_CMD) build
GO_CLEAN=$(GO_CMD) clean
GO_TEST=$(GO_CMD) test
GO_GET=$(GO_CMD) get
GO_VET=$(GO_CMD) vet
GO_RUN=$(GO_CMD) run
GO_MOD_DEP=$(GO_CMD) mod download
ALL_PATH=./...
BINARY_NAME=app
UNPACK_PATH=$$path
DOCKER_CMD=docker
DOCKER_BUILD=$(DOCKER_CMD) build
DOCKER_PUSH=$(DOCKER_CMD) push
DOCKER_IMAGE_NAME=random_anonymous_chat
deps:
$(GO_MOD_DEP)
test:
$(GO_TEST) -v $(ALL_PATH) -cover
build:
$(GO_BUILD) -o $(BINARY_NAME)
run:
$(GO_RUN) main.go
clean:
$(GO_CLEAN)
rm -f $(BINARY_NAME)
pack:
tar -cvzf $(BINARY_NAME)-v$(VERSION).tar.gz $(BINARY_NAME) ./config ./template
unpack:
tar -zxf $(BINARY_NAME).tar.gz -C $(UNPACK_PATH)
docker_build:
@echo "開始打包 Docker Image - $(DOCKER_FULL_IMAGE)"
$(DOCKER_BUILD) -t $(DOCKER_IMAGE_NAME) .
docker_push:
@echo "開始 push docker image - $(DOCKER_FULL_IMAGE)"
$(DOCKER_PUSH) $(DOCKER_IMAGE_NAME)
可以直接通過 make docker_build
進行打包
原先的 dockerfile 有幾個地方可以進行修改,分別為:
muti-stage
的機制將包完的檔案放置另一個 image 內以下針對這兩個步驟一一說明
一開始我們選用的是 完整的 golang image,但是在打包的時候我們可以選擇 alpine
版本的,這樣的 image 會小很多,因為 alpine
是 linux
推出的極小 OS。
直接將 FROM
的部分改為 codingxiang/go_vc
FROM golang:alpine
muti-stage
的機制將包完的檔案放置另一個 image 內關於 muti-stage
的部分有興趣的朋友可以參考 這裡
由於 Go 在編譯後就是一個 binary 的執行檔,也就是可以不用有 go 的環境也可以執行,因此透過 mulit-stage
的方式來打包是一個不錯的選擇,這邊我們將整個流程分成 3 個 stage:
Stage1 : 用來 download 相依套件
Stage2 : 用來 build binary
Stage3 : 真正可執行的 Docker Image
就讓我們一個一個來吧
Stage1 的部分主要是用來下載相依套件,而相依套件是透過 go.mod
與 go.sum
兩個檔案所決定要下載哪些,因此在此 stage 只要將這兩個檔案複製進來即可
[備註] FROM 後面的 AS
是用來定義此 stage 的內容,其他的 stage 可以透過此識別碼來取得裡面的內容
FROM codingxiang/go_vc AS stage1
ENV RUN_PATH=/app PROJ_PATH=/build
RUN mkdir -p $RUN_PATH
WORKDIR $RUN_PATH
ENV GO111MODULE=on
COPY go.mod .
COPY go.sum .
RUN go mod download
Stage2 的部分主要用來編譯,因此要先繼承 stage1 的內容
FROM stage1 AS stage2
接著設定角色為 root
USER root
接著將外部的其他檔案 COPY 到 Container 內並且設定 WORKDIR 為 $PROJ_PATH
ADD . $PROJ_PATH
WORKDIR $PROJ_PATH
因為有寫好 makefile,所以只要呼叫 Makefile 內的 target
即可,這邊有三個步驟要執行,依序為
RUN make build pack unpack path=$RUN_PATH
整個 Stage2 的語法如下:
FROM stage1 AS stage2
USER root
ADD . $PROJ_PATH
WORKDIR $PROJ_PATH
RUN make build pack unpack path=$RUN_PATH
最後要來將編譯好的檔案放置到完全空的 image 中執行,因為這樣可以省去非常多的空間。
FROM 的部分選用 alpine
且 USER 選用 root
,這個之前提到過,是目前最小的 OS
Image 了
FROM alpine
USER root
定義執行路徑為環境變數並且同時建立,執行路徑為 /app
ENV RUN_PATH=/app
RUN mkdir -p $RUN_PATH
複製 stage2
編號好的檔案至此
COPY --from=stage2 ${RUN_PATH} ${RUN_PATH}
最後定義執行路徑為 /app
,程式進入點為 ./app
這個 binary
WORKDIR ${RUN_PATH}
ENTRYPOINT ["./app"]
整個 Stage3 的語法如下:
FROM alpine
USER root
ENV RUN_PATH=/app
RUN mkdir -p $RUN_PATH
COPY --from=stage2 ${RUN_PATH} ${RUN_PATH}
WORKDIR ${RUN_PATH}
ENTRYPOINT ["./app"]
執行 make docker_build
的指令,會獲得結果如下
Sending build context to Docker daemon 286.7kB
Step 1/20 : FROM codingxiang/go_vc AS stage1
---> f3e1d12b7aff
Step 2/20 : ENV RUN_PATH=/app PROJ_PATH=/build
---> Using cache
---> 63318e54be20
Step 3/20 : RUN mkdir -p $RUN_PATH
---> Using cache
---> b03acc57efea
Step 4/20 : WORKDIR $RUN_PATH
---> Using cache
---> 9f53c01fb72b
Step 5/20 : ENV GO111MODULE=on
---> Using cache
---> d4d3b5c6b45b
Step 6/20 : COPY go.mod .
---> Using cache
---> edf9008a4cb1
Step 7/20 : COPY go.sum .
---> Using cache
---> 5ba7a0b07900
Step 8/20 : RUN go mod download
---> Using cache
---> 718c688752f8
Step 9/20 : FROM stage1 AS stage2
---> 718c688752f8
Step 10/20 : USER root
---> Using cache
---> e55e5b803080
Step 11/20 : ADD . $PROJ_PATH
---> Using cache
---> 5a7ac1599142
Step 12/20 : WORKDIR $PROJ_PATH
---> Using cache
---> b32bcb01f9e9
Step 13/20 : RUN make build pack unpack path=$RUN_PATH
---> Using cache
---> 58cfaef333ed
Step 14/20 : FROM alpine
---> a24bb4013296
Step 15/20 : USER root
---> Using cache
---> e55d31193b1c
Step 16/20 : ENV RUN_PATH=/app
---> Using cache
---> c3052518d3f6
Step 17/20 : RUN mkdir -p $RUN_PATH
---> Using cache
---> 887503deae49
Step 18/20 : COPY --from=stage2 ${RUN_PATH} ${RUN_PATH}
---> Using cache
---> d80087020e62
Step 19/20 : WORKDIR ${RUN_PATH}
---> Using cache
---> 6ebd7a6b6288
Step 20/20 : ENTRYPOINT ["./app"]
---> Using cache
---> 463359743e6b
Successfully built 463359743e6b
Successfully tagged random_anonymous_chat:latest
因為我已經執行過了,所以可以看到每一層 layer 都是寫 Using cache
可以透過 docker images
來查看目前 build 的 image 大小
REPOSITORY TAG IMAGE ID CREATED SIZE
random_anonymous_chat latest 463359743e6b 16 minutes ago 27.5MB
從結果可以發現,透過今天這些流程的最佳化,將原先 1.2G
大小的 Image 縮小至 27.5MB
,整個縮小了許多,也因為使用了 multi-stage
的機制,所以如果沒有新的相依套件的話,在 build stage1 時就會使用上次的 cache,整體的速度也加快許多。
從明天開始我們花點時間說一下關於 CI 要怎麼做吧!
你好
docker build有一個問題需要請教一下,
原本程式碼未使用Go 1.17後的func,是可以正常build的。
但是使用到了Go 1.17後的func ,例如:time.IsDST,就會出現undefined。
程式直接go run 是正常運作的。
=> CACHED [stage1 6/7] COPY go.sum .
=> CACHED [stage1 7/7] RUN go mod download
=> CACHED [stage2 1/3] ADD . /build
=> CACHED [stage2 2/3] WORKDIR /build
=> ERROR [stage2 3/3] RUN make build pack unpack path=/app
------
> [stage2 3/3] RUN make build pack unpack path=/app:
#20 0.216 go build -o app
#20 1.730 # godemo
#20 1.730 ./main.go:18:18: date.IsDST undefined (type "time".Time has no field or method IsDST)
#20 1.730 note: module requires Go 1.17
#20 1.759 make: *** [Makefile:24: build] Error 2
------
executor failed running [/bin/sh -c make build pack unpack path=$RUN_PATH]: exit code: 2
請問有甚麼方式可以解決?
會發生這個錯誤的原因在於這個範例中 Dockerfile stage1
的 FROM
是 codingxiang/go_vc
,這個 image 我當初包的版本是 1.14
,這邊你可以把 image 換成 golang:1.17.0-alpine
試試看~
已成功Build了,感謝。
補充說明:使用golang:< version >-alpine,
需安裝make,才可正常使用make指令。
RUN apk update && apk add make
若有使用到git指令則需安裝git
RUN apk update && apk add git
針對 Makefile 的部分
我也寫了一篇分享文
歡迎交流!
https://blog.goodjack.tw/2023/01/use-makefile-to-manage-workflows-for-web-projects.html