爲Go程序創建最小的Docker Image

本文將會介紹如何使用docker打包一個golang編寫的應用程序,最終的產物就是一個Dockerfile文件,可別小瞧這短短几行代碼,涉及的知識點可不少,接下來我們就仔細剖析一下吧。

FROM golang:alpine

ADD src /go/src

RUN go install -v test

ENTRYPOINT ["/go/bin/test"]

CMD ["-logtostderr"]

1.基礎鏡像選擇

第一行是指定一個基礎鏡像,在此基礎上創建我們的鏡像,此處使用的是golang:alpine版本,

這是一個相對較小的linux系統,砍掉了linux中的許多工具,預裝了golang, 包管理工具使用的是apk,可以把這個鏡像docker pull下來把玩一番,默認的shell是sh,執行命令docker run -t-i golang:alpine /bin/sh 進入命令行。進入後執行env查看環境變量,因為其GOPATH這個環境變量對後面的環境部署有用,可以看到環境變量GOPATH默認值為/go

2.映射代碼文件並安裝

使用 ADD src /go/src 將主機scr文件映射到/go/src目錄下,為什麼非得是這個/go/src這個目錄吶?沒錯就是上面的GOPATH環境變量的路徑,因為我們後面需要執行go install命令進行安裝,否則的話就需要重新設置GOPATH才能編譯代碼。如下test是程序的主程序,glog是使用的開源日誌庫,整個文件結構如下:

.

├── Dockerfile

└── src

├── github.com

│ └── golang

│ └── glog

│ ├── glog_file.go

│ ├── glog.go

│ ├── glog_test.go

│ ├── LICENSE

│ └── README

└── test

└── main.go

此處glog庫沒有使用glide等包管理工具,直接使用git submodule來管理, 優勢是git push不會將glog代碼Push到遠程倉庫,只是添加一個對glog的引用,並且當glog庫中代碼被修改後可以只需要在glog的子目錄中git pull即可,也就是說在本地會拉取具體的代碼進行編譯等,但是在遠程倉庫只是保存引用。

可以通過命令生成glog這個子模塊: git submodule add https://github.com/golang/glog.git src/github.com/golang/glog。注意git submoule命令中被引用到的位置為src/github.com/golang而不是直接的src/ 中,因為執行該命令後本地代碼倉庫會clone glog這個代碼倉庫,將它的代碼拉下來,只是創建glog這個目錄,所以前面的一些父目錄需要自己創建。關於命令更多的介紹參見 Git。

組織好文件結構就可以進行go install了,生成的可執行在$GOPATH/bin中,後面就是基本的指定入口程序和參數。通過docker build -t="name" . 生成鏡像

3.更進一步:提前編譯

上面的方式是將代碼拷貝進基礎鏡像並在其內部編譯,毫無疑問的是golang:alpine中包含一系列程序運行的依賴,程序運行會動態加載這些庫,我們可以用ldd命令查看所生成的二進制文件的依賴:

linux-vdso.so.1 => (0x00007ffc5b1e4000)

libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f50a1f13000)

libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f50a1b4a000)

/lib64/ld-linux-x86-64.so.2 (0x00005611a4b0a000)

那麼問題來了? 如果將這些依賴靜態編譯至可執行文件中, 並且只將可執行文件添加到鏡像中, 那就不需要在鏡像中保存這些運行時依賴和源碼了,就可以創建一個更小的鏡像了。幸運的是無論是golang的編譯機制還是docker的基礎鏡像都提供這樣的實現:

使用命令生成靜態編譯的二進制文件:CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main test

此時用ldd查看生成的可執行文件的依賴,可以看到顯示not a dynamic executable,這裡我們禁用CGO使其生成靜態二進制文件,同時設置系統為linux。我們將基礎鏡像設置為 scratch,這是一個空的鏡像,是用來構建其他基礎鏡像的, 無需下載即可使用。

重新編寫的Dockerfile如下:

FROM scratch

ADD main /

ENTRYPOINT ["/main"]

CMD ["-logtostderr"]

執行docker build -t example-scratch .生成鏡像,可以看到該鏡像的大小隻有幾M,並且和二進制程序main的大小相同。

Dockerfile中FROM scratch並不會增加層數, 所以用此Dockerfile構建的鏡像只是三層,並且鏡像的大小和二進制文件的大小相同,可以通過docker image history查看這些信息

gaorong@gaorong-TM1604 % ls -lh main

-rwxrwxr-x 1 gaorong gaorong 2.4M 6月 9 11:59 main*

gaorong@gaorong-TM1604 % docker images example-scratch

REPOSITORY TAG IMAGE ID CREATED SIZE

example-scratch latest 817e7d91c8c0 About an hour ago 2.42MB

gaorong@gaorong-TM1604 % docker image history example-scratch

IMAGE CREATED CREATED BY SIZE COMMENT

817e7d91c8c0 About an hour ago /bin/sh -c #(nop) CMD ["-logtostderr"] 0B

323b904e4844 About an hour ago /bin/sh -c #(nop) ENTRYPOINT ["/main"] 0B

8216c95b5652 About an hour ago /bin/sh -c #(nop) ADD file:6828257fa0b521333… 2.42MB

4.builder image

上述鏡像構建是需要提前編譯好二進制,然後才能拷貝到最終的鏡像中,可否將編譯的這一步驟也容器化了呢? 當然可以,可以寫一個Dockerfile.builder來進行build操作

FROM golang:alpine

ENV CGO_ENABLED=0 GOOS=linux

CMD ["go", "build", "-a", "-installsuffix", "cgo", "-o", "main", "test" ]

在執行的時候需要將當前文件目錄volume掛載進容器的/go目錄下: docker run -v `pwd`:/go builder。

本文所使用的案例太過簡單,builder image意義不大,假如你參與一個大型項目,項目中有一個Makefile,其中定義了好多操作,例如生成項目的rpm包,生成rpm這些操作需要調用額外的程序執行,如果參與項目的每個人都配置這樣一個開發環境未免太麻煩,此時就可以利用builder image將所需要的環境全部打包進去,然後在builder image調用makefile即可。 著名的案例就是kubernetes,開發者在個人電腦上只需要安裝了docker就可以編譯生成kuberetes所有的binary,甚至golang也無需安裝。

5.分階段編譯(multi-stage builds)

可以利用docker 的分階段編譯將上述兩個操作合併起來,先在一個鏡像中構建,在另一個鏡像中執行。下面的Dockerfile摘自prometheus這個第三方監控的dmo中的Dockerfile,可以看到它是首先在builder鏡像中下載對應的依賴並且編譯程序,最後在scratch基礎鏡像中執行程序。

# This Dockerfile builds an image for a client_golang example.

#

# Use as (from the root for the client_golang repository):

# docker build -f examples/$name/Dockerfile -t prometheus/golang-example-$name .

# Builder image, where we build the example.

FROM golang:1.9.0 AS builder

WORKDIR /go/src/github.com/prometheus/client_golang

COPY . .

WORKDIR /go/src/github.com/prometheus/client_golang/prometheus

RUN go get -d

WORKDIR /go/src/github.com/prometheus/client_golang/examples/simple

RUN CGO_ENABLED=0 GOOS=linux go build -a -tags netgo -ldflags '-w'

# Final image.

FROM scratch

LABEL maintainer "The Prometheus Authors <prometheus-developers>"/<prometheus-developers>

COPY --from=builder /go/src/github.com/prometheus/client_golang/examples/simple .

EXPOSE 8080

ENTRYPOINT ["/simple"]

其實就是將一個鏡像作為builder鏡像,然後將build產物在另外一個鏡像中執行。

英文來自:https://blog.codeship.com/building-minimal-docker-containers-for-go-applications/

譯文來自:http://www.cnblogs.com/gaorong/p/Docker.html