/

在 Docker 容器中部署 Go 應用程式

在 Docker 容器中部署 Go 應用程式

簡介

如果你從沒聽過 Docker,不過這很不可能,第一件你應該知道的事情是 Docker 允許你以隔離方式執行應用程式,高度關注於分離的問題,但同時允許它們與外部世界通訊和互動。

還有你應該知道的是,每個人都在使用它,每個主要的雲端服務提供商也都有專用於運行容器的解決方案,所以你應該學習它!

安裝

安裝方法隨你的系統而異,所以請查看 https://www.docker.com/get-docker

我假設你已經安裝了 Docker,並在你的 shell 中可以使用 docker 命令。

Go 官方鏡像

Docker 維護著許多不同語言的官方鏡像清單,Go 也不例外,它是 2014 年原始 官方鏡像推出 的一部分。

官方鏡像庫可以在這裡找到: https://hub.docker.com/_/golang/。有許多標籤可以識別所需的 Go 版本,以及想要提取的操作系統。

一個範例應用

作為範例,我將在 Docker 容器中部署一個小型的 Go 應用程式。它在 8000 port 監聽,以 q 作為查詢參數來獲得網頁,然後取回並印出找到的連結:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package main

import (
"fmt"
"log"
"net/http"

"golang.org/x/net/html"
)

func main() {
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe("0.0.0.0:8000", nil))
}

func handler(w http.ResponseWriter, r *http.Request) {
url := r.URL.Query().Get("q")
fmt.Fprintf(w, "Page = %q\n", url)
if len(url) == 0 {
return
}
page, err := parse("https://" + url)
if err != nil {
fmt.Printf("Error getting page %s %s\n", url, err)
return
}
links := pageLinks(nil, page)
for _, link := range links {
fmt.Fprintf(w, "Link = %q\n", link)
}
}

func parse(url string) (*html.Node, error) {
fmt.Println(url)
r, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("Cannot get page")
}
b, err := html.Parse(r.Body)
if err != nil {
return nil, fmt.Errorf("Cannot parse page")
}
return b, err
}

func pageLinks(links []string, n *html.Node) []string {
if n.Type == html.ElementNode && n.Data == "a" {
for _, a := range n.Attr {
if a.Key == "href" {
links = append(links, a.Val)
}
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
links = pageLinks(links, c)
}
return links
}

範例用法:

將應用程式擺到 Docker

我將應用程式放在 https://github.com/flaviocopes/findlinks

使用 go get 我可以輕鬆地下載並安裝它,使用 go get github.com/flaviocopes/findlinks

運行:

1
docker run golang go get -v github.com/flaviocopes/findlinks

如果你還沒有 golang 的 Docker 映像檔,這個命令將首先下載該映像檔,然後下載存儲庫並掃描標準庫中未包含的額外依賴,這裡是 golang.org/x/net/html

1
2
3
4
5
6
7
8
9
10
11
12
$ docker run golang go get -v github.com/flaviocopes/findlinks
github.com/flaviocopes/findlinks (download)
Fetching https://golang.org/x/net/html?go-get=1
Parsing meta tags from https://golang.org/x/net/html?go-get=1 (status code 200)
get "golang.org/x/net/html": found meta tag main.metaImport{Prefix:"golang.org/x/net", VCS:"git", RepoRoot:"https://go.googlesource.com/net"} at https://golang.org/x/net/html?go-get=1
get "golang.org/x/net/html": verifying non-authoritative meta tag
Fetching https://golang.org/x/net?go-get=1
Parsing meta tags from https://golang.org/x/net?go-get=1 (status code 200)
golang.org/x/net (download)
golang.org/x/net/html/atom
golang.org/x/net/html
github.com/flaviocopes/findlinks

這個命令建立一個容器並運行它。我們可以使用 docker ps -l 命令(-l 選項告訴 Docker 列出最近使用的容器)檢視它:

1
2
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
343d96441f16 golang "go get -v github...." 3 minutes ago Exited (0) 2 minutes ago mystifying_swanson

容器在 go get 命令完成後退出。

Docker 剛建立了一個即時的映像檔並運行它,為了再次運行它,我們需要重複這個過程,但可以透過映像檔來協助我們:現在讓我們從容器中創建一個映像檔,以便稍後運行它:

1
docker commit $(docker ps -lq) findlinks

以上命令使用 docker ps -lq 獲取最後一個容器的 ID,並提交該映像檔。
你可以使用 docker images findlinks 檢查映像檔是否已安裝:

1
2
3
$ docker images findlinks
REPOSITORY TAG IMAGE ID CREATED SIZE
findlinks latest 4e7ebb87d02e 11 seconds ago 720MB

我們可以使用以下命令在 findlinks 映像檔上運行 findlinks 命令:

1
docker run -p 8000:8000 findlinks findlinks

就是這樣了!我們的應用程式現在將回應 http://192.168.99.100:8000/,其中 192.168.99.100 是 Docker 容器的 IP 位址。

你可以測試 http://192.168.99.100:8000/?q=flaviocopes.com 這個網址,這將打印出我們在本地運行應用程式時所得到的相同輸出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Page = "flaviocopes.com"
Link = "https://flaviocopes.com/index.xml"
Link = "https://twitter.com/flaviocopes"
Link = "https://github.com/flaviocopes"
Link = "https://stackoverflow.com/users/205039/flaviocopes"
Link = "https://linkedin.com/in/flaviocopes/"
Link = "mailto:[[email protected]](/cdn-cgi/l/email-protection)"
Link = "/"
Link = "/page/contact/"
Link = "/page/about/"
Link = "https://flaviocopes.com/golang-tutorial-rest-api/"
Link = "https://flaviocopes.com/golang-environment-variables/"
Link = "https://flaviocopes.com/golang-sql-database/"
Link = "https://flaviocopes.com/golang-is-go-object-oriented/"
Link = "https://flaviocopes.com/golang-comparing-values/"
Link = "https://flaviocopes.com/golang-data-structures/"
Link = "https://flaviocopes.com/golang-data-structure-binary-search-tree/"
Link = "https://flaviocopes.com/golang-data-structure-graph/"
Link = "https://flaviocopes.com/golang-data-structure-linked-list/"
Link = "https://flaviocopes.com/golang-data-structure-queue/"
Link = "https://flaviocopes.com/golang-data-structure-stack/"
Link = "https://flaviocopes.com/golang-event-listeners/"

精簡 Docker 映像檔

以上結果的問題是映像檔太大,對於這個簡單的程式來說,720MB 其實是不可接受的大小,這是非常簡單的情境。我們可能想部署數千個應用程式實例,這個大小不適用。

為什麼映像檔這麼大?因為 Go 應用程式是在容器內編譯的。因此映像檔需要安裝 Go 編譯器。當然還需要編譯器所需的所有東西,如 GCC,以及整個 Linux 發行版(Debian Jessie)。它下載並安裝 Go,編譯應用程式並運行,這一切都是如此快速,我們甚至都沒有注意到。但我們可以做得更好。我們該怎麼做?我們使用了 Nick Gauthier為 Go 應用程式構建最小的 Docker 容器 文章學到的東西。

我們告訴 Docker 執行 golang:1.8.3 映像檔並使用 CGO 禁用 CGO靜態編譯 我們的應用程式,這意味著映像檔甚至都不需要通常在動態連結時需要存取的 C 函式庫,使用:

1
docker run --rm -it -v "$GOPATH":/gopath -v "$(pwd)":/app -e "GOPATH=/gopath" -w /app golang:1.8.3 sh -c 'CGO_ENABLED=0 go build -a --installsuffix cgo --ldflags="-s" -o findlinks'

現在我們在該資料夾中擁有一個 findlinks 執行檔:

1
2
3
4
5
6
7
$ ll
.rw-r--r-- 77 flavio 17 Aug 18:57 Dockerfile
.rwxr-xr-x 4.2M flavio 17 Aug 19:13 findlinks
.rw-r--r-- 1.1k flavio 12 Aug 18:10 findlinks.go

$ file findlinks
findlinks: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped

請注意,這當然不是我在 OSX 上編譯所得到的檔案,它是一個準備好在 Linux 上運行的二進位檔:

1
2
$ file findlinks
findlinks: Mach-O 64-bit executable x86_64

然後,我們創建一個 Dockerfile,告訴 Docker 使用 iron/base,一個非常輕量的映像檔:

1
2
3
4
FROM iron/base
WORKDIR /app
COPY findlinks /app/
ENTRYPOINT ["./findlinks"]

現在我們可以建立映像檔,並將其標記為 flaviocopes/golang-docker-example-findlinks

1
docker build -t flaviocopes/golang-docker-example-findlinks .

然後運行它:

1
docker run --rm -it -p 8000:8000 flaviocopes/golang-docker-example-findlinks

輸出結果與之前相同,但這次映像檔不是 720MB,而只有 11.1MB。

1
2
3
REPOSITORY TAG IMAGE ID CREATED SIZE
flaviocopes/golang-docker-example-findlinks latest f32d2fd74638 14 minutes ago 11.1MB
findlinks latest c60f6792b9f3 20 minutes ago 720MB

多階段建置

多階段建置 讓我們能夠簡單地獲得輕量級映像檔,而不需要分別編譯二進位檔和運行它。這是我們在應用程式中的 Dockerfile:

1
2
3
4
5
6
7
8
9
10
11
FROM golang:1.8.3 as builder
WORKDIR /go/src/github.com/flaviocopes/findlinks
RUN go get -d -v golang.org/x/net/html
COPY findlinks.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o findlinks .

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /go/src/github.com/flaviocopes/findlinks/findlinks .
CMD ["./findlinks"]

運行:

1
$ docker build -t flaviocopes/golang-docker-example-findlinks .

將構建一個輕量級(10.8MB)映像檔:

1
2
3
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
flaviocopes/golang-docker-example-findlinks latest aa2081ca7016 12 seconds ago 10.8MB

運行映像檔:

1
$ docker run --rm -it -p 8000:8000 flaviocopes/golang-docker-example-findlinks