多階段構建

解釋

在傳統的構建中,所有構建指令都按順序在單個構建容器中執行:下載依賴項、編譯程式碼和打包應用程式。所有這些層最終都會出現在您的最終映象中。這種方法是可行的,但它會導致映象臃腫,攜帶不必要的重量並增加安全風險。這就是多階段構建的用武之地。

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

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

  • 對於解釋型語言,如 JavaScript、Ruby 或 Python,您可以在一個階段構建和壓縮程式碼,然後將生產就緒的檔案複製到一個更小的執行時映象中。這可以最佳化您的映象以進行部署。
  • 對於編譯型語言,如 C、Go 或 Rust,多階段構建允許您在一個階段進行編譯,並將編譯後的二進位制檔案複製到最終的執行時映象中。無需將整個編譯器捆綁在最終映象中。

這是一個使用虛擬碼的多階段構建結構的簡化示例。請注意,這裡有多個 FROM 語句和一個新的 AS <stage-name>。此外,第二階段的 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. 下載並安裝 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 包裝器指令碼和您專案的 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. 透過您的 Web 瀏覽器訪問 https://:8080 上的“Hello World”頁面,或者使用此 curl 命令:

    $ 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)。該映象提供了一個 Java 執行時環境 (JRE),這足以執行編譯後的應用程式(JAR 檔案)。

    對於生產用途,強烈建議您使用 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 映象更輕量、更安全、更易於管理。

其他資源