本文將會介紹如何使用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@googlegroups.com>"
COPY --from=builder /go/src/github.com/prometheus/client_golang/examples/simple .
EXPOSE 8080
ENTRYPOINT ["/simple"]
其實就是將一個鏡像作為builder鏡像,然后將build產物在另外一個鏡像中執行。