構建 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.19
Docker 映象可以從其他映象繼承。因此,您可以使用已經包含編譯和執行 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://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 構建過程可以訪問上下文中的任何檔案。
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" 基礎影像非常精簡,專為靜態二進位制檔案的精簡部署而設計。
多階段構建還有很多內容,包括多架構構建的可能性,因此您可以隨意檢視 多階段構建。但是,這對於您在此處的進展來說並不是必需的。
下一步
在本模組中,您遇到了示例應用程式,併為其構建了容器影像。
在下一個模組中,您將瞭解如何將您的影像作為容器執行。