構建 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
該應用程式的 main.go 檔案很簡單,如果您熟悉 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.19Docker 映象可以從其他映象繼承。因此,您可以使用已經包含編譯和執行 Go 應用程式所需的所有工具和庫的官方 Go 映象,而不是從頭開始建立自己的基礎映象。
注意
如果您對建立自己的基礎映象感興趣,您可以檢視本指南的以下部分:建立基礎映象。但是,請注意,這對於繼續手頭的任務來說不是必需的。
現在您已經為即將建立的容器映象定義了基礎映象,您可以開始在其基礎上進行構建。
為了在執行其餘命令時更輕鬆,請在要構建的映象中建立一個目錄。這也指示 Docker 將此目錄用作所有後續命令的預設目標。這樣,您就不必在 Dockerfile 中鍵入完整的路徑,相對路徑將基於此目錄。
WORKDIR /app通常,您在下載 Go 編寫的專案後,首先要做的就是安裝編譯它所需的模組。請注意,基礎映象已經擁有工具鏈,但您的原始碼還沒有。
因此,在您可以在映象中執行 go mod download 之前,您需要將 go.mod 和 go.sum 檔案複製到其中。使用 COPY 命令來完成此操作。
在最簡單的情況下,COPY 命令接受兩個引數。第一個引數告訴 Docker 您想要複製到映象中的檔案。最後一個引數告訴 Docker 您想要將該檔案複製到哪裡。
將 go.mod 和 go.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://dockerdocs.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://dockerdocs.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 構建過程可以訪問上下文中的任何檔案。
build 命令可以選擇接受 --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:multistage 和 docker-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" 基礎影像非常精簡,專為靜態二進位制檔案的精簡部署而設計。
多階段構建還有很多內容,包括多架構構建的可能性,因此您可以隨意檢視 多階段構建。但是,這對於您在此處的進展來說並不是必需的。
下一步
在本模組中,您遇到了示例應用程式,併為其構建了容器影像。
在下一個模組中,您將瞭解如何將您的影像作為容器執行。