多階段建構

說明

在傳統建構中,所有建構指令都會依序在單一建構容器中執行:下載依賴項、編譯程式碼和打包應用程式。所有這些層最終都會出現在您的最終映像檔中。這種方法有效,但它会导致映像檔過大,帶來不必要的負擔並增加您的安全風險。這就是多階段建構的用武之地。

多階段建構在 Dockerfile 中引入了多個階段,每個階段都有特定用途。可以將其想像成能夠在多個不同環境中同時執行建構的不同部分。透過將建構環境與最終執行階段環境分離,您可以顯著減小映像檔大小和攻擊面。這對於具有大量建構依賴項的應用程式特別有益。

建議所有類型的應用程式都使用多階段建構。

  • 對於直譯語言,例如 JavaScript、Ruby 或 Python,您可以在一個階段中建構和縮小程式碼,然後將可供生產環境使用的檔案複製到較小的執行階段映像檔。這可以最佳化您的映像檔以進行部署。
  • 對於編譯語言,例如 C、Go 或 Rust,多階段建構允許您在一個階段中進行編譯,然後將編譯後的二進位檔複製到最終執行階段映像檔。無需在最終映像檔中捆綁整個編譯器。

以下是以虛擬程式碼為例的多階段建構結構的簡化範例。請注意,有多個 FROM 陳述式和一個新的 AS <階段名稱>。此外,第二階段中的 COPY 陳述式正從前一階段複製 --from

# Stage 1: Build Environment
FROM builder-image AS build-stage 
# Install build tools (e.g., Maven, Gradle)
# Copy source code
# Build commands (e.g., compile, package)

# Stage 2: Runtime environment
FROM runtime-image AS final-stage  
#  Copy application artifacts from the build stage (e.g., JAR file)
COPY --from=build-stage /path/in/build/stage /path/to/place/in/final/stage
# Define runtime configuration (e.g., CMD, ENTRYPOINT) 

此 Dockerfile 使用兩個階段

  • 建構階段使用包含編譯應用程式所需的建構工具的基本映像檔。它包含安裝建構工具、複製原始程式碼和執行建構指令的指令。
  • 最後階段使用適合執行應用程式的小型基本映像檔。它從建構階段複製編譯後的成品(例如 JAR 檔案)。最後,它定義用於啟動應用程式的執行階段設定(使用 CMDENTRYPOINT)。

試用看看

在本實作指南中,您將瞭解多階段建構的強大功能,以便為範例 Java 應用程式建立精簡且高效的 Docker 映像檔。您將使用以 Maven 建構的簡單「Hello World」Spring Boot 應用程式作為範例。

  1. 下載並安裝預先初始化的專案A screenshot of Spring Initializr tool selected with Java 21, Spring Web and Spring Boot 3.4.0

    Spring Initializr

src/main/java 目錄包含專案的原始程式碼,src/test/java 目錄
包含測試原始碼,而 pom.xml 檔案是專案的專案物件模型 (POM)。

pom.xml 檔案是 Maven 專案設定的核心。它是包含
建構自訂專案所需大部分資訊的單一設定檔。POM 很大,看起來可能
令人生畏。幸好,您還不需要瞭解每個細節才能有效地使用它。

  • 建立顯示「Hello World!」的 RESTful Web 服務。

    src/main/java/com/example/spring_boot_docker/ 目錄下,您可以修改您的
    SpringBootDockerApplication.java 檔案,並使用以下內容

    SpringbootDockerApplication.java 檔案首先宣告您的 com.example.spring_boot_docker 套件並導入必要的 Spring 架構。此 Java 檔案建立一個簡單的 Spring Boot Web 應用程式,當使用者造訪其首頁時,會回應「Hello World」。

  • 建立 Dockerfile

    現在您有了專案,就可以建立 Dockerfile 了。

    1. 在包含所有其他資料夾和檔案(例如 src、pom.xml 等)的相同資料夾中建立名為 Dockerfile 的檔案。

    2. Dockerfile 中,透過新增以下行來定義您的基本映像檔

      FROM eclipse-temurin:21.0.2_13-jdk-jammy
    3. 現在,使用 WORKDIR 指令定義工作目錄。這將指定未來指令將在哪裡執行,以及檔案將複製到容器映像檔內的哪個目錄。

      WORKDIR /app
    4. 將 Maven 包裝器 Script 和專案的 pom.xml 檔案複製到 Docker 容器內目前的工作目錄 /app 中。

      COPY .mvn/ .mvn
      COPY mvnw pom.xml ./
    5. 在容器內執行指令。它會執行 ./mvnw dependency:go-offline 指令,該指令使用 Maven 包裝器 (./mvnw) 下載專案的所有依賴項,而無需建構最終的 JAR 檔案(有助於加快建構速度)。

      RUN ./mvnw dependency:go-offline
    6. 將主機上專案的 src 目錄複製到容器內的 /app 目錄。

      COPY src ./src
    7. 設定容器啟動時要執行的預設指令。此指令指示容器使用 spring-boot:run 目標執行 Maven 包裝器 (./mvnw),這將建構並執行您的 Spring Boot 應用程式。

      CMD ["./mvnw", "spring-boot:run"]

      這樣,您應該會有以下 Dockerfile

      FROM eclipse-temurin:21.0.2_13-jdk-jammy
      WORKDIR /app
      COPY .mvn/ .mvn
      COPY mvnw pom.xml ./
      RUN ./mvnw dependency:go-offline
      COPY src ./src
      CMD ["./mvnw", "spring-boot:run"]

    建構容器映像檔

    1. 執行以下指令以建構 Docker 映像檔

      $ docker build -t spring-helloworld .
      
    2. 使用 docker images 指令檢查 Docker 映像檔的大小

      $ docker images
      

      執行此操作將產生如下所示的輸出

      REPOSITORY          TAG       IMAGE ID       CREATED          SIZE
      spring-helloworld   latest    ff708d5ee194   3 minutes ago    880MB
      

      此輸出顯示您的映像檔大小為 880MB。它包含完整的 JDK、Maven 工具鏈等等。在生產環境中,您不需要在最終映像檔中包含這些內容。

    執行 Spring Boot 應用程式

    1. 現在您已建構映像檔,接下來可以執行容器了。

      $ docker run -d -p 8080:8080 spring-helloworld
      

      然後,您會在容器日誌中看到類似以下的輸出

      [INFO] --- spring-boot:3.3.4:run (default-cli) @ spring-boot-docker ---
      [INFO] Attaching agents: []
      
           .   ____          _            __ _ _
          /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
         ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
          \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
           '  |____| .__|_| |_|_| |_\__, | / / / /
          =========|_|==============|___/=/_/_/_/
      
          :: Spring Boot ::                (v3.3.4)
      
      2024-09-29T23:54:07.157Z  INFO 159 --- [spring-boot-docker] [           main]
      c.e.s.SpringBootDockerApplication        : Starting SpringBootDockerApplication using Java
      21.0.2 with PID 159 (/app/target/classes started by root in /app)
       ….
    2. 透過您的網頁瀏覽器,在 http://localhost:8080 或透過以下 curl 指令存取您的「Hello World」頁面

      $ curl localhost:8080
      Hello World
      

    使用多階段建構

    1. 參考以下 Dockerfile

      FROM eclipse-temurin:21.0.2_13-jdk-jammy AS builder
      WORKDIR /opt/app
      COPY .mvn/ .mvn
      COPY mvnw pom.xml ./
      RUN ./mvnw dependency:go-offline
      COPY ./src ./src
      RUN ./mvnw clean install
      
      FROM eclipse-temurin:21.0.2_13-jre-jammy AS final
      WORKDIR /opt/app
      EXPOSE 8080
      COPY --from=builder /opt/app/target/*.jar /opt/app/*.jar
      ENTRYPOINT ["java", "-jar", "/opt/app/*.jar"]

      請注意,此 Dockerfile 已被拆分為兩個階段。

      • 第一個階段與先前的 Dockerfile 相同,提供用於建置應用程式的 Java 開發套件 (JDK) 環境。此階段的名稱為 builder。

      • 第二個階段是一個名為 final 的新階段。它使用更精簡的 eclipse-temurin:21.0.2_13-jre-jammy 映像檔,僅包含執行應用程式所需的 Java 執行環境 (JRE)。此映像檔提供了一個 Java 執行環境 (JRE),足以執行已編譯的應用程式 (JAR 檔案)。

      對於正式環境的使用,強烈建議您使用 jlink 建立自定義的類 JRE 執行環境。所有版本的 Eclipse Temurin 都有 JRE 映像檔,但 jlink 允許您建立僅包含應用程式所需 Java 模組的最小執行環境。這可以顯著減小最終映像檔的大小並提高其安全性。 參考此頁面 以獲得更多資訊。

      透過多階段建置,Docker 建置可以使用一個基礎映像檔進行編譯、打包和單元測試,然後使用另一個映像檔作為應用程式執行環境。因此,最終映像檔的體積會更小,因為它不包含任何開發或除錯工具。藉由將建置環境與最終執行環境分離,您可以大幅減小映像檔大小並提高最終映像檔的安全性。

    2. 現在,重新建置您的映像檔並執行您可立即使用的正式環境建置。

      $ docker build -t spring-helloworld-builder .
      

      此指令使用位於目前目錄中 Dockerfile 檔案的 final 階段,建置名為 spring-helloworld-builder 的 Docker 映像檔。

      **注意**

      在您的多階段 Dockerfile 中,final 階段 (final) 是建置的預設目標。這表示如果您沒有在 docker build 指令中使用 --target 旗標明確指定目標階段,Docker 將預設自動建置最後一個階段。您可以使用 docker build -t spring-helloworld-builder --target builder . 只建置具有 JDK 環境的 builder 階段。

    3. 使用 docker images 指令查看映像檔大小差異

      $ docker images
      

      您將獲得類似以下的輸出

      spring-helloworld-builder latest    c5c76cb815c0   24 minutes ago      428MB
      spring-helloworld         latest    ff708d5ee194   About an hour ago   880MB
      

      您的最終映像檔只有 428 MB,而原始建置大小為 880 MB。

      透過最佳化每個階段並僅包含必要的內容,您能夠在實現相同功能的同時,顯著減少
      整體映像檔大小。這不僅可以提高效能,還可以
      使您的 Docker 映像檔更輕巧、更安全且更易於管理。

    其他資源