構建您的 Go 映象

概述

在本節中,您將構建一個容器映象。該映象包含執行應用程式所需的一切——編譯後的應用程式二進位制檔案、執行時、庫以及應用程式所需的所有其他資源。

所需軟體

要完成本教程,您需要以下內容:

  • 本地執行的 Docker。請按照說明下載並安裝 Docker
  • 用於編輯檔案的 IDE 或文字編輯器。Visual Studio Code 是一個免費且受歡迎的選擇,但您可以使用任何您熟悉的工具。
  • 一個 Git 客戶端。本指南使用基於命令列的 git 客戶端,但您可以自由使用任何適合您的工具。
  • 一個命令列終端應用程式。本模組中顯示的示例來自 Linux shell,但它們應該在 PowerShell、Windows 命令提示符或 OS X 終端中工作,只需進行少量修改(如果有的話)。

瞭解示例應用程式

這個示例應用程式是一個微服務的漫畫。它故意設計得簡單,以便將重點放在學習 Go 應用程式容器化的基礎知識上。

該應用程式提供兩個 HTTP 端點:

  • 它對 / 的請求響應一個包含心形符號 (<3) 的字串。
  • 它對 /health 的請求響應 {"Status" : "OK"} JSON。

它對任何其他請求響應 HTTP 錯誤 404。

該應用程式監聽由環境變數 PORT 的值定義的 TCP 埠。預設值為 8080

該應用程式是無狀態的。

該應用程式的完整原始碼可在 GitHub 上獲取:github.com/docker/docker-gs-ping。我們鼓勵您分叉它並盡情地進行實驗。

要繼續,請將應用程式儲存庫克隆到您的本地機器:

$ git clone https://github.com/docker/docker-gs-ping

如果您熟悉 Go,應用程式的 main.go 檔案非常簡單。

package main

import (
	"net/http"
	"os"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

func main() {

	e := echo.New()

	e.Use(middleware.Logger())
	e.Use(middleware.Recover())

	e.GET("/", func(c echo.Context) error {
		return c.HTML(http.StatusOK, "Hello, Docker! <3")
	})

	e.GET("/health", func(c echo.Context) error {
		return c.JSON(http.StatusOK, struct{ Status string }{Status: "OK"})
	})

	httpPort := os.Getenv("PORT")
	if httpPort == "" {
		httpPort = "8080"
	}

	e.Logger.Fatal(e.Start(":" + httpPort))
}

// Simple implementation of an integer minimum
// Adapted from: https://gobyexample.com/testing-and-benchmarking
func IntMin(a, b int) int {
	if a < b {
		return a
	}
	return b
}

為應用程式建立 Dockerfile

要使用 Docker 構建容器映象,需要一個包含構建指令的 Dockerfile

使用(可選的)解析器指令行開始您的 Dockerfile,該指令行指示 BuildKit 根據指定語法的語法規則解釋您的檔案。

然後您告訴 Docker 您希望為應用程式使用哪個基礎映象。

# syntax=docker/dockerfile:1

FROM golang:1.19

Docker 映象可以從其他映象繼承。因此,您可以不從頭開始建立自己的基礎映象,而是使用官方的 Go 映象,該映象已經擁有編譯和執行 Go 應用程式所需的所有工具和庫。

注意

如果您對建立自己的基礎映象感到好奇,可以檢視本指南的以下部分:建立基礎映象。但請注意,這對於您手頭的任務並非必需。

現在您已經為即將到來的容器映象定義了基礎映象,您可以開始在其之上進行構建。

為了讓您在執行其餘命令時更輕鬆,請在您正在構建的映象中建立一個目錄。這還指示 Docker 將此目錄用作所有後續命令的預設目標。這樣,您就不必在 Dockerfile 中輸入完整的檔案路徑,相對路徑將基於此目錄。

WORKDIR /app

通常,一旦您下載了一個用 Go 編寫的專案,您做的第一件事就是安裝編譯它所需的模組。請注意,基礎映象已經有工具鏈,但您的原始碼尚未在其中。

因此,在您可以在映象中執行 go mod download 之前,您需要將 go.modgo.sum 檔案複製到其中。使用 COPY 命令來完成此操作。

在其最簡單的形式中,COPY 命令接受兩個引數。第一個引數告訴 Docker 您要複製到映象中的檔案。最後一個引數告訴 Docker 您希望將該檔案複製到何處。

go.modgo.sum 檔案複製到您的專案目錄 /app 中,由於您使用了 WORKDIR,該目錄是映象內的當前目錄 (./)。與一些現代 shell 似乎對使用尾隨斜槓 (/) 無動於衷,並且可以在大多數情況下弄清楚使用者意圖不同,Docker 的 COPY 命令在解釋尾隨斜槓時非常敏感。

COPY go.mod go.sum ./
注意

如果您想熟悉 COPY 命令對尾隨斜槓的處理,請參閱Dockerfile 參考。這個尾隨斜槓可能會以您無法想象的更多方式導致問題。

現在您已經將模組檔案複製到您正在構建的 Docker 映象中,您也可以使用 RUN 命令在那裡執行 go mod download 命令。這與您在本地機器上執行 go 完全相同,但這次這些 Go 模組將安裝到映象中的一個目錄中。

RUN go mod download

至此,您已經安裝了 Go 工具鏈版本 1.19.x 和所有 Go 依賴項到映象中。

接下來您需要做的是將您的原始碼複製到映象中。您將像之前處理模組檔案一樣使用 COPY 命令。

COPY *.go ./

COPY 命令使用萬用字元將主機上(Dockerfile 所在的目錄)當前目錄中所有帶有 .go 副檔名的檔案複製到映象中的當前目錄。

現在,要編譯您的應用程式,請使用熟悉的 RUN 命令:

RUN CGO_ENABLED=0 GOOS=linux go build -o /docker-gs-ping

這應該很熟悉。該命令的結果將是一個名為 docker-gs-ping 的靜態應用程式二進位制檔案,位於您正在構建的映象的檔案系統根目錄中。您可以將二進位制檔案放置在該映象中您想要的任何其他位置,根目錄在這方面沒有特殊含義。使用它只是為了保持檔案路徑短以提高可讀性。

現在,剩下要做的就是告訴 Docker 在您的映象用於啟動容器時要執行什麼命令。

您可以使用 CMD 命令來完成此操作:

CMD ["/docker-gs-ping"]

以下是完整的 Dockerfile

# syntax=docker/dockerfile:1

FROM golang:1.19

# Set destination for COPY
WORKDIR /app

# Download Go modules
COPY go.mod go.sum ./
RUN go mod download

# Copy the source code. Note the slash at the end, as explained in
# https://docs.docker.net.tw/reference/dockerfile/#copy
COPY *.go ./

# Build
RUN CGO_ENABLED=0 GOOS=linux go build -o /docker-gs-ping

# Optional:
# To bind to a TCP port, runtime parameters must be supplied to the docker command.
# But we can document in the Dockerfile what ports
# the application is going to listen on by default.
# https://docs.docker.net.tw/reference/dockerfile/#expose
EXPOSE 8080

# Run
CMD ["/docker-gs-ping"]

Dockerfile 還可以包含註釋。它們總是以 # 符號開頭,並且必須位於一行的開頭。註釋是為了方便您記錄您的 Dockerfile

還有 Dockerfile 指令的概念,例如您新增的 syntax 指令。指令必須始終位於 Dockerfile 的最頂部,因此在添加註釋時,請確保註釋位於您可能使用的任何指令之後:

# syntax=docker/dockerfile:1
# A sample microservice in Go packaged into a container image.

FROM golang:1.19

# ...

構建映象

現在您已經建立了 Dockerfile,可以從中構建映象。docker build 命令從 Dockerfile 和上下文建立 Docker 映象。構建上下文是位於指定路徑或 URL 中的一組檔案。Docker 構建過程可以訪問上下文中包含的任何檔案。

構建命令可選地接受 --tag 標誌。此標誌用於用字串值標記映象,該字串值易於人類閱讀和識別。如果您不傳遞 --tag,Docker 將使用 latest 作為預設值。

構建您的第一個 Docker 映象。

$ docker build --tag docker-gs-ping .

構建過程將在執行構建步驟時列印一些診斷訊息。以下只是這些訊息可能是什麼樣子的示例。

[+] Building 2.2s (15/15) FINISHED
 => [internal] load build definition from Dockerfile                                                                                       0.0s
 => => transferring dockerfile: 701B                                                                                                       0.0s
 => [internal] load .dockerignore                                                                                                          0.0s
 => => transferring context: 2B                                                                                                            0.0s
 => resolve image config for docker.io/docker/dockerfile:1                                                                                 1.1s
 => CACHED docker-image://docker.io/docker/dockerfile:1@sha256:39b85bbfa7536a5feceb7372a0817649ecb2724562a38360f4d6a7782a409b14            0.0s
 => [internal] load build definition from Dockerfile                                                                                       0.0s
 => [internal] load .dockerignore                                                                                                          0.0s
 => [internal] load metadata for docker.io/library/golang:1.19                                                                             0.7s
 => [1/6] FROM docker.io/library/golang:1.19@sha256:5d947843dde82ba1df5ac1b2ebb70b203d106f0423bf5183df3dc96f6bc5a705                       0.0s
 => [internal] load build context                                                                                                          0.0s
 => => transferring context: 6.08kB                                                                                                        0.0s
 => CACHED [2/6] WORKDIR /app                                                                                                              0.0s
 => CACHED [3/6] COPY go.mod go.sum ./                                                                                                     0.0s
 => CACHED [4/6] RUN go mod download                                                                                                       0.0s
 => CACHED [5/6] COPY *.go ./                                                                                                              0.0s
 => CACHED [6/6] RUN CGO_ENABLED=0 GOOS=linux go build -o /docker-gs-ping                                                                  0.0s
 => exporting to image                                                                                                                     0.0s
 => => exporting layers                                                                                                                    0.0s
 => => writing image sha256:ede8ff889a0d9bc33f7a8da0673763c887a258eb53837dd52445cdca7b7df7e3                                               0.0s
 => => naming to docker.io/library/docker-gs-ping                                                                                          0.0s

您的確切輸出可能會有所不同,但如果沒有錯誤,您應該在輸出的第一行看到 FINISHED 字樣。這意味著 Docker 已成功構建名為 docker-gs-ping 的映象。

檢視本地映象

要檢視本地機器上的映象列表,您有兩種選擇。一種是使用 CLI,另一種是使用Docker Desktop。由於您目前正在終端中工作,因此讓我們看看如何使用 CLI 列出映象。

要列出映象,請執行 docker image ls 命令(或簡寫 docker images):

$ docker image ls

REPOSITORY                       TAG       IMAGE ID       CREATED         SIZE
docker-gs-ping                   latest    7f153fbcc0a8   2 minutes ago   1.11GB
...

您的確切輸出可能會有所不同,但您應該看到帶有 latest 標籤的 docker-gs-ping 映象。因為您在構建映象時沒有指定自定義標籤,所以 Docker 假定標籤將是 latest,這是一個特殊值。

標記映象

映象名稱由斜槓分隔的名稱元件組成。名稱元件可能包含小寫字母、數字和分隔符。分隔符定義為句點、一個或兩個下劃線,或者一個或多個破折號。名稱元件不能以分隔符開頭或結尾。

一個映象由一個清單和層列表組成。簡單來說,一個標籤指向這些工件的組合。您可以為一個映象設定多個標籤,事實上,大多數映象都有多個標籤。為您構建的映象建立一個輔助標籤,然後檢視其層。

使用 docker image tag(或 docker tag 簡寫)命令為您的映象建立一個新標籤。此命令接受兩個引數;第一個引數是源映象,第二個是要建立的新標籤。以下命令為您構建的 docker-gs-ping:latest 建立一個新標籤 docker-gs-ping:v1.0

$ docker image tag docker-gs-ping:latest docker-gs-ping:v1.0

Docker tag 命令為映象建立了一個新標籤。它不建立新映象。該標籤指向同一個映象,只是引用該映象的另一種方式。

現在再次執行 docker image ls 命令以檢視更新後的本地映象列表:

$ docker image ls

REPOSITORY                       TAG       IMAGE ID       CREATED         SIZE
docker-gs-ping                   latest    7f153fbcc0a8   6 minutes ago   1.11GB
docker-gs-ping                   v1.0      7f153fbcc0a8   6 minutes ago   1.11GB
...

您可以看到有兩個以 docker-gs-ping 開頭的映象。您知道它們是同一個映象,因為如果您檢視 IMAGE ID 列,您可以看到這兩個映象的值是相同的。此值是 Docker 內部用於標識映象的唯一識別符號。

刪除您剛剛建立的標籤。為此,您將使用 docker image rm 命令,或其簡寫 docker rmi(代表“移除映象”)

$ docker image rm docker-gs-ping:v1.0
Untagged: docker-gs-ping:v1.0

請注意,Docker 的響應告訴您該映象尚未刪除,只是取消了標籤。

透過執行以下命令進行驗證:

$ docker image ls

您將看到標籤 v1.0 不再在 Docker 例項保留的映象列表中。

REPOSITORY                       TAG       IMAGE ID       CREATED         SIZE
docker-gs-ping                   latest    7f153fbcc0a8   7 minutes ago   1.11GB
...

標籤 v1.0 已刪除,但您的機器上仍有 docker-gs-ping:latest 標籤可用,因此該映象仍然存在。

多階段構建

您可能已經注意到您的 docker-gs-ping 映象大小超過 1 GB,這對於一個微小的編譯 Go 應用程式來說太多了。您可能還會想,在構建映象之後,完整的 Go 工具套件(包括編譯器)去了哪裡。

答案是,完整的工具鏈仍然存在於容器映象中。這不僅因為檔案過大而帶來不便,而且在容器部署時也可能存在安全風險。

這兩個問題可以透過使用多階段構建來解決。

簡而言之,多階段構建可以將一個構建階段的工件帶到另一個構建階段,並且每個構建階段都可以從不同的基礎映象例項化。

因此,在以下示例中,您將使用一個完整的官方 Go 映象來構建您的應用程式。然後,您將把應用程式二進位制檔案複製到另一個基礎非常精簡且不包含 Go 工具鏈或其他可選元件的映象中。

示例應用程式儲存庫中的 Dockerfile.multistage 包含以下內容:

# syntax=docker/dockerfile:1

# Build the application from source
FROM golang:1.19 AS build-stage

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY *.go ./

RUN CGO_ENABLED=0 GOOS=linux go build -o /docker-gs-ping

# Run the tests in the container
FROM build-stage AS run-test-stage
RUN go test -v ./...

# Deploy the application binary into a lean image
FROM gcr.io/distroless/base-debian11 AS build-release-stage

WORKDIR /

COPY --from=build-stage /docker-gs-ping /docker-gs-ping

EXPOSE 8080

USER nonroot:nonroot

ENTRYPOINT ["/docker-gs-ping"]

由於您現在有兩個 Dockerfile,您必須告訴 Docker 您希望使用哪個 Dockerfile 來構建映象。用 multistage 標記新映象。這個標籤(像其他任何標籤一樣,除了 latest)對 Docker 沒有特殊含義,它只是您選擇的一個名稱。

$ docker build -t docker-gs-ping:multistage -f Dockerfile.multistage .

比較 docker-gs-ping:multistagedocker-gs-ping:latest 的大小,您會發現它們之間存在幾個數量級的差異。

$ docker image ls
REPOSITORY       TAG          IMAGE ID       CREATED              SIZE
docker-gs-ping   multistage   e3fdde09f172   About a minute ago   28.1MB
docker-gs-ping   latest       336a3f164d0f   About an hour ago    1.11GB

這是因為您在構建的第二階段使用的 “無發行版” 基礎映象非常精簡,專為靜態二進位制檔案的精益部署而設計。

多階段構建還有更多內容,包括多架構構建的可能性,因此請隨意檢視多階段構建。然而,這對於您在這裡的進展並非必不可少。

後續步驟

在本模組中,您瞭解了您的示例應用程式併為其構建了容器映象。

在下一個模組中,您將瞭解如何將您的映象作為容器執行。