多階段

本節探討多階段構建。使用多階段構建有兩個主要原因

  • 它們允許您並行執行構建步驟,使您的構建流程更快、更高效。
  • 它們允許您建立一個佔用空間更小的最終映象,其中只包含執行程式所需的元件。

在 Dockerfile 中,構建階段由 FROM 指令表示。上一節中的 Dockerfile 沒有利用多階段構建。它只是一個構建階段。這意味著最終映象中充斥著用於編譯程式的資源。

$ docker build --tag=buildme .
$ docker images buildme
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
buildme      latest    c021c8a7051f   5 seconds ago   150MB

程式編譯成可執行二進位制檔案,因此您不需要在最終映象中存在 Go 語言實用程式。

新增階段

使用多階段構建,您可以選擇對構建和執行時環境使用不同的基礎映象。您可以將構建工件從構建階段複製到執行時階段。

修改 Dockerfile 如下。此更改使用最小的 scratch 映象作為基礎建立另一個階段。在最終的 scratch 階段,將之前階段構建的二進位制檔案複製到新階段的檔案系統中。

  # syntax=docker/dockerfile:1
  FROM golang:1.21-alpine
  WORKDIR /src
  COPY go.mod go.sum .
  RUN go mod download
  COPY . .
  RUN go build -o /bin/client ./cmd/client
  RUN go build -o /bin/server ./cmd/server
+
+ FROM scratch
+ COPY --from=0 /bin/client /bin/server /bin/
  ENTRYPOINT [ "/bin/server" ]

現在,如果您構建映象並檢查它,您應該看到一個明顯更小的數字

$ docker build --tag=buildme .
$ docker images buildme
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
buildme      latest    436032454dd8   7 seconds ago   8.45MB

映象大小從 150MB 降至只有 8.45MB。這是因為生成的映象只包含二進位制檔案,其他什麼都沒有。

並行

您已減少了映象的佔用空間。下一步將展示如何使用多階段構建來提高構建速度,並利用並行性。構建目前一次生成一個二進位制檔案。沒有理由需要先構建客戶端再構建伺服器,反之亦然。

您可以將二進位制檔案構建步驟拆分為單獨的階段。在最終的 scratch 階段,從每個相應的構建階段複製二進位制檔案。透過將這些構建細分為單獨的階段,Docker 可以並行執行它們。

構建每個二進位制檔案的階段都需要 Go 編譯工具和應用程式依賴項。將這些通用步驟定義為可重用的基礎階段。您可以透過使用模式 FROM image AS stage_name 為階段命名來實現。這允許您在另一個階段的 FROM 指令中引用階段名稱(FROM stage_name)。

您還可以為二進位制檔案構建階段命名,並在將二進位制檔案複製到最終的 scratch 映象時,在 COPY --from=stage_name 指令中引用階段名稱。

  # syntax=docker/dockerfile:1
- FROM golang:1.21-alpine
+ FROM golang:1.21-alpine AS base
  WORKDIR /src
  COPY go.mod go.sum .
  RUN go mod download
  COPY . .
+
+ FROM base AS build-client
  RUN go build -o /bin/client ./cmd/client
+
+ FROM base AS build-server
  RUN go build -o /bin/server ./cmd/server

  FROM scratch
- COPY --from=0 /bin/client /bin/server /bin/
+ COPY --from=build-client /bin/client /bin/
+ COPY --from=build-server /bin/server /bin/
  ENTRYPOINT [ "/bin/server" ]

現在,build-clientbuild-server 階段不再是依次構建二進位制檔案,而是同時執行。

Stages executing in parallel

構建目標

最終映象現在很小,並且您使用並行性有效地構建了它。但是這個映象有點奇怪,因為它在同一個映象中包含了客戶端和伺服器二進位制檔案。它們不應該分別為兩個不同的映象嗎?

可以使用單個 Dockerfile 建立多個不同的映象。您可以使用 --target 標誌指定構建的目標階段。將未命名的 FROM scratch 階段替換為兩個名為 clientserver 的單獨階段。

  # syntax=docker/dockerfile:1
  FROM golang:1.21-alpine AS base
  WORKDIR /src
  COPY go.mod go.sum .
  RUN go mod download
  COPY . .

  FROM base AS build-client
  RUN go build -o /bin/client ./cmd/client

  FROM base AS build-server
  RUN go build -o /bin/server ./cmd/server

- FROM scratch
- COPY --from=build-client /bin/client /bin/
- COPY --from=build-server /bin/server /bin/
- ENTRYPOINT [ "/bin/server" ]

+ FROM scratch AS client
+ COPY --from=build-client /bin/client /bin/
+ ENTRYPOINT [ "/bin/client" ]

+ FROM scratch AS server
+ COPY --from=build-server /bin/server /bin/
+ ENTRYPOINT [ "/bin/server" ]

現在您可以構建客戶端和伺服器程式作為單獨的 Docker 映象(標籤)

$ docker build --tag=buildme-client --target=client .
$ docker build --tag=buildme-server --target=server .
$ docker images "buildme*" 
REPOSITORY       TAG       IMAGE ID       CREATED          SIZE
buildme-client   latest    659105f8e6d7   20 seconds ago   4.25MB
buildme-server   latest    666d492d9f13   5 seconds ago    4.2MB

現在映象更小了,每個大約 4MB。

此更改還避免了每次都要構建兩個二進位制檔案。當選擇構建 client 目標時,Docker 只構建通向該目標的階段。如果不需要 build-serverserver 階段,則會跳過它們。同樣,構建 server 目標會跳過 build-clientclient 階段。

總結

多階段構建對於建立佔用空間更小、更精簡的映象非常有用,還有助於加快構建速度。

相關資訊

下一步

下一節將介紹如何使用檔案掛載來進一步提高構建速度。