我一般使用docker運行個人 golang 程序,在這裏分享一下我構建 docker 鏡像的經驗。我構建 docker 鏡像不只優化構建後的體積,還要優化構建速度。linux
首先貼出代碼例子,咱們假設要構建一個 http 服務git
package main
import (
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
func main() {
fmt.Println("Server Ready")
router := gin.Default()
router.GET("/", func(c *gin.Context) {
c.String(200, "hello world, this time is: "+time.Now().Format(time.RFC1123Z))
})
router.GET("/github", func(c *gin.Context) {
_, err := http.Get("https://api.github.com/")
if err != nil {
c.String(500, err.Error())
return
}
c.String(200, "access github api ok")
})
if err := router.Run(":9900"); err != nil {
panic(err)
}
}
複製代碼
說明:github
https://api.github.com
,爲了演示後面遇到的證書坑這裏咱們能夠先試一試構建後包的體積golang
$ go build -o server
$ ls -alh | grep server
-rwxrwxrwx 1 eyas eyas 14.6M May 29 10:26 server
複製代碼
14.6MB,這是一個http服務的 hello world,固然這是由於使用了 gin ,因此有些大,若是用標準包 net/http
寫的 hello world,體積大概是接近 7 MBdocker
先看看第一個版本api
FROM golang:1.14-alpine as builder
WORKDIR /usr/src/app ENV GOPROXY=https://goproxy.cn
COPY ./go.mod ./ COPY ./go.sum ./ RUN go mod download COPY . . RUN go build -ldflags "-s -w" -o server
FROM scratch as runner
COPY --from=builder /usr/src/app/server /opt/app/ CMD ["/opt/app/server"] 複製代碼
說明:緩存
golang:1.14-alpine
做爲編譯環境,是由於這是體積最小的golang編譯環境go.mod
和 go.sum
,而後 go mod download
,是爲了防止每次構建都會從新下載依賴包,利用docker構建緩存提高構建速度-ldflags "-s -w"
去除構建包的調試信息,減少go構建後程序體積,大概能減少 1/4
吧FROM XXX as xxx
,在構建程序包的時候,使用帶編譯環境的鏡像去構建,運行的時候其實徹底不須要go的編譯環境,因此在運行階段使用docker的空鏡像 scratch
去運行。這部是減少鏡像體積最有效的方法了。好了,下面開始構建鏡像bash
$ docker build -t server .
...
Successfully built 8d3b91210721
Successfully tagged server:latest
複製代碼
到了這一步,構建成功,看看鏡像大小app
$ docker images
server latest 8d3b91210721 1 minutes ago 11MB
複製代碼
11MB,還行,如今運行一下curl
$ docker run -p 9900:9900 server
standard_init_linux.go:211: exec user process caused "no such file or directory"
複製代碼
發現啓動報錯了,並且main函數的第一行打印語句都沒有出現,因此整個程序徹底沒有運行。錯誤緣由是缺乏庫依賴文件。這實際上是構建的 go 程序還依賴底層的 so 庫文件,不信能夠在物理機編譯後看看它的依賴
$ go build -o server
$ ldd server
linux-vdso.so.1 (0x00007ffcfb775000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f9a8dc47000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f9a8d856000)
/lib64/ld-linux-x86-64.so.2 (0x00007f9a8de66000)
複製代碼
這是否是跟咱們的認知有點出入呢,說好無依賴的呢,結果仍是有幾個依賴庫文件呢,雖然這幾個依賴都是最底層的,通常操做系統都會有,可誰叫咱們選了 scratch
,這個鏡像裏面除了linux內核之外真的什麼都沒了。
這是由於go build 是默認啓用 CGO 的,不信你能夠試試這個命令 go env CGO_ENABLED
,在 CGO 開啓狀況下,不管代碼有沒有用CGO,都會有庫依賴文件,解決方法也很簡單,手動指定關閉CGO就行,並且包體積並不會增長哦,還會減小呢
$ CGO_ENABLED=0 go build -o server
$ ldd server
not a dynamic executable
複製代碼
FROM golang:1.14-alpine as builder
WORKDIR /usr/src/app
ENV GOPROXY=https://goproxy.cn
COPY ./go.mod ./
COPY ./go.sum ./
RUN go mod download
COPY . .
-RUN go build -ldflags "-s -w" -o server
+RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o server
FROM scratch as runner
COPY --from=builder /usr/src/app/server /opt/app/
CMD ["/opt/app/server"]
複製代碼
改動點: go build 前加了 CGO_ENABLED=0
$ docker build -t server .
...
Successfully built a81385160e25
Successfully tagged server:latest
$ docker run -p 9900:9900 server
[GIN-debug] GET / --> main.main.func1 (3 handlers)
[GIN-debug] GET /github --> main.main.func2 (3 handlers)
[GIN-debug] Listening and serving HTTP on :9900
複製代碼
正常啓動了,咱們訪問一下試試,訪問以前看看當前時間
$ date
Fri May 29 13:11:28 CST 2020
$ curl http://localhost:9900
hello world, this time is: Fri, 29 May 2020 05:18:28 +0000
$ curl http://localhost:9900/github
Get "https://api.github.com/": x509: certificate signed by unknown authority
複製代碼
發現有問題
13:11:28
,可是根據由顯示的時間是 05:11:53
,實際上是docker 容器內的時區不對,默認是 0 時區,但是咱們國家是 東8區https://api.github.com/
這是 https 站點,報證書錯誤解決問題
FROM golang:1.14-alpine as builder
WORKDIR /usr/src/app
ENV GOPROXY=https://goproxy.cn
+RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
+ apk add --no-cache ca-certificates tzdata
COPY ./go.mod ./
COPY ./go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o server
FROM scratch as runner
+COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
+COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/src/app/server /opt/app/
CMD ["/opt/app/server"]
複製代碼
在 builder 階段,安裝了 ca-certificates tzdata 兩個庫,在runner階段,將時區配置和根證書複製了一份
$ docker build -t server .
...
Successfully built e0825838043d
Successfully tagged server:latest
$ docker run -p 9900:9900 server
[GIN-debug] GET / --> main.main.func1 (3 handlers)
[GIN-debug] GET /github --> main.main.func2 (3 handlers)
[GIN-debug] Listening and serving HTTP on :9900
複製代碼
訪問一下試試
$ date
Fri May 29 13:27:16 CST 2020
$ curl http://localhost:9900
hello world, this time is: Fri, 29 May 2020 13:27:16 +0800
$ curl http://localhost:9900/github
access github api ok
複製代碼
一切正常了,看看當前鏡像大小
$ docker images
server latest e0825838043d 9 minutes ago 11.3MB
複製代碼
才 11.3MB,已經很小了,可是,還能夠更小,就是把構建後的包再壓縮一次
FROM golang:1.14-alpine as builder
WORKDIR /usr/src/app
ENV GOPROXY=https://goproxy.cn
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
- apk add --no-cache ca-certificates tzdata
+ apk add --no-cache upx ca-certificates tzdata
COPY ./go.mod ./
COPY ./go.sum ./
RUN go mod download
COPY . .
-RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o server
+RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o server &&\
+ upx --best server -o _upx_server && \
+ mv -f _upx_server server
FROM scratch as runner
COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/src/app/server /opt/app/
CMD ["/opt/app/server"]
複製代碼
在 builder 階段,安裝了 upx ,而且go build 完成後,使用 upx 壓縮了一下,執行一下構建,你會發現這個構建時間變長了,這是由於我給 upx 設置的參數是 --best
,也就是最大壓縮級別,這樣壓縮出來的後會儘量的小,若是嫌慢,能夠下降壓縮級別從 -1
到 -9
,數字越大壓縮級別越高,也越慢。我使用 --best
構建完成後看看鏡像體積。
$ docker build -t server .
...
Successfully built 80c3f3cde1f7
Successfully tagged server:latest
$ docker images
server latest 80c3f3cde1f7 1 minutes ago 4.26MB
複製代碼
這下子可小了,才 4.26MB,再去試試那兩個接口,一切正常。優化到此結束。
FROM golang:1.14-alpine as builder
WORKDIR /usr/src/app ENV GOPROXY=https://goproxy.cn
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \ apk add --no-cache upx ca-certificates tzdata COPY ./go.mod ./ COPY ./go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o server &&\ upx --best server -o _upx_server && \ mv -f _upx_server server
FROM scratch as runner
COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /etc/localtime COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=builder /usr/src/app/server /opt/app/ CMD ["/opt/app/server"] 複製代碼
要減少鏡像體積,首先多階段構建這很重要,這樣就能夠把編譯環境和運行環境分開。
另外,選擇 scratch
這個鏡像其實很不明智,它雖然很小,可是它太原始了,裏面什麼工具都沒有,程序啓動後,連容器都進不去,就算進去了什麼都作不了。因此就算一昧的追求儘量小的鏡像體積,也不建議選擇 scratch
做爲運行環境,我暫時只踩到小部分的坑,後面還有更多坑沒踩,我也沒有興趣繼續踩 scratch
的坑。
建議選擇 alpine
,alpine 的鏡像大小是 5.61MB
這個大小其實仍是鏡像解壓後的大小,實際上下載鏡像的時候,只須要下載 2.68 MB
。還有,上文全部我說的鏡像體積,全都是指解壓後的鏡像體積,和實際上傳下載時的體積是不同的,docker本身會壓縮一次再傳輸鏡像
還有個很小的鏡像是 busybox
,它的體積是 1.22MB
,下載 705.6 KB
,有大部分的linux命令可用,可是運行環境仍是很原始,有興趣能夠去嘗試
不管是 alpine 仍是 busybox ,他們都會上述時區和證書問題,一樣按照上面方法就能解決,切換到 alpine 或者 busybox 也很簡單,只須要修改 runner 基礎鏡像就行
-FROM scratch as runner
+FROM alpine as runner
複製代碼
或者
-FROM scratch as runner
+FROM busybox as runne
複製代碼