多階段建置

說明

在傳統的建置方式中,所有的建置指令都是在同一個建置容器內依序執行:下載相依套件、編譯程式碼以及封裝應用程式。所有這些層級最終都會留在您的最終映像檔中。這種方法雖然可行,但會導致映像檔變得臃腫,不僅佔用不必要的空間,還會增加您的資安風險。這正是「多階段建置」可以派上用場的地方。

多階段建置在您的 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 使用了兩個階段

  • 建置階段 (build stage) 使用包含編譯應用程式所需工具的基礎映像檔。它包含了安裝建置工具、複製原始碼以及執行建置指令的步驟。
  • 最終階段 (final stage) 使用適合執行應用程式的較小基礎映像檔。它會從建置階段複製編譯好的產出物(例如 JAR 檔案)。最後,它定義了啟動應用程式的執行時期設定(使用 CMDENTRYPOINT)。

試試看

在本實作指南中,您將發揮多階段建置的威力,為一個 Java 範例應用程式建立精簡且高效的 Docker 映像檔。我們將以一個使用 Maven 建置的簡單「Hello World」Spring Boot 應用程式作為範例。

  1. 下載並安裝 Docker Desktop。

  2. 開啟此 預先初始化的專案來產生 ZIP 檔案。其外觀如下

    A screenshot of Spring Initializr tool selected with Java 21, Spring Web and Spring Boot 3.4.0

    Spring Initializr 是一個 Spring 專案的快速啟動產生器。它提供了一個可擴充的 API,用於產生基於 JVM 的專案,並針對幾種常見概念提供實作,例如 Java、Kotlin 和 Groovy 的基礎語言產生功能。

    選擇 Generate 來建立並下載此專案的 zip 檔案。

    在此示範中,您將 Maven 建置自動化工具與 Java、Spring Web 相依套件以及 Java 21 搭配使用作為您的中繼資料。

  3. 瀏覽專案目錄。解壓縮檔案後,您將看到以下專案目錄結構

    spring-boot-docker
    ├── HELP.md
    ├── mvnw
    ├── mvnw.cmd
    ├── pom.xml
    └── src
        ├── main
        │   ├── java
        │   │   └── com
        │   │       └── example
        │   │           └── spring_boot_docker
        │   │               └── SpringBootDockerApplication.java
        │   └── resources
        │       ├── application.properties
        │       ├── static
        │       └── templates
        └── test
            └── java
                └── com
                    └── example
                        └── spring_boot_docker
                            └── SpringBootDockerApplicationTests.java
    
    15 directories, 7 files

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

    pom.xml 檔案是 Maven 專案設定的核心。它是一個單一的設定檔,
    包含了建置自訂專案所需的大部分資訊。POM 非常龐大,且看起來可能
    令人卻步。幸運的是,您暫時不需要理解其中的每個細節也能有效使用它。

  4. 建立一個顯示 "Hello World!" 的 RESTful Web 服務。

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

    package com.example.spring_boot_docker;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    
    @RestController
    @SpringBootApplication
    public class SpringBootDockerApplication {
    
        @RequestMapping("/")
            public String home() {
            return "Hello World";
        }
    
    	public static void main(String[] args) {
    		SpringApplication.run(SpringBootDockerApplication.class, args);
    	}
    
    }

    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.8_9-jdk-jammy
  3. 現在,使用 WORKDIR 指令定義工作目錄。這將指定後續指令運行的位置,以及將檔案複製到容器映像檔內的目標目錄。

    WORKDIR /app
  4. 將 Maven 包裝器 (wrapper) 指令碼和您專案的 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.8_9-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 -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. 透過網頁瀏覽器造訪 https://:8080,或使用此 curl 指令來存取您的「Hello World」頁面

    $ curl localhost:8080
    Hello World
    

使用多階段建置

  1. 考慮以下的 Dockerfile

    FROM eclipse-temurin:21.0.8_9-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.8_9-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)。此映像檔提供了足以執行已編譯應用程式(JAR 檔案)的 Java 執行時期環境 (JRE)。

    針對生產環境使用,強烈建議您使用 jlink 產生自訂的 JRE 執行時期環境。Eclipse Temurin 的所有版本都提供 JRE 映像檔,但 jlink 允許您建立一個極簡的執行時期環境,僅包含應用程式所需的 Java 模組。這可以大幅縮減最終映像檔的大小並提升安全性。請參閱此頁面以取得更多資訊。

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

  2. 現在,重新建置您的映像檔並執行您準備好用於生產環境的建置版本。

    $ docker build -t spring-helloworld-builder .
    

    此指令會使用您位於當前目錄中 Dockerfile 檔案裡的最終階段,建置一個名為 spring-helloworld-builder 的 Docker 映像檔。

    注意

    在您的多階段 Dockerfile 中,最終階段 (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 映像檔更輕量、更安全且更易於管理。

其他資源

© . This site is unofficial and not affiliated with Kubernetes or Docker Inc.