建置您的 Go 映像檔

概觀

在本節中,您將建置一個容器映像檔。該映像檔包含執行應用程式所需的一切 - 已編譯的應用程式二進位檔案、執行階段、程式庫以及應用程式所需的所有其他資源。

所需軟體

要完成本教學課程,您需要以下項目

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

認識範例應用程式

範例應用程式是微服務的簡化版本。它特意地簡化,以便將重點放在學習 Go 應用程式容器化的基礎知識。

應用程式提供兩個 HTTP 端點

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

它以 HTTP 錯誤 404 回應任何其他請求。

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

應用程式是無狀態的。

應用程式的完整原始碼位於 GitHub 上:github.com/docker/docker-gs-ping

如果您熟悉 Go,則應用程式的 main.go 檔案很簡單

為應用程式建立 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 不同,這些 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://docker-docs.dev.org.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://docker-docs.dev.org.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 構建過程可以訪問上下文中 located 的任何文件。

構建命令可以選擇使用 --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

這是因為您在構建的第二階段使用的 "distroless" 基礎映像非常精簡,專為靜態二進制文件的精簡部署而設計。

多階段構建還有很多其他功能,包括多架構構建的可能性,因此請隨時查看 多階段構建。然而,這對您目前的進度並非必要。

後續步驟

在本模組中,您遇到了您的範例應用程式,並為其構建了容器映像。

在下一模組中,您將了解如何將映像作為容器運行。