多階段構建

解釋

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

多階段構建在您的 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.3.0

    Spring Initializr 是 Spring 專案的快速入門生成器。它提供可擴充套件的 API 來生成基於 JVM 的專案,其中包含對幾個常見概念的實現——例如 Java、Kotlin 和 Groovy 的基本語言生成。

    選擇**生成**以建立並下載此專案的 zip 檔案。

    對於此演示,您已將 Maven 構建自動化與 Java、Spring Web 依賴項和 Java 21 結合使用以獲取元資料。

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

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

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

    pom.xml 檔案是 Maven 專案配置的核心。它是一個單一的配置檔案,
    包含構建自定義專案所需的大多數資訊。POM 非常龐大,看起來
    很嚇人。幸運的是,您還沒有必要了解每個細節才能有效地使用它。

  4. 建立一個 RESTful Web 服務來顯示“Hello World!”。

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

    package com.example.springbootdocker;
    
    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.springbootdocker 包並匯入必要的 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 包裝器指令碼和專案的 pom.xml 檔案複製到容器中的當前工作目錄 /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. 將容器啟動時執行的預設命令設定為。此命令指示容器執行 Maven 包裝器 (./mvnw) 以及 spring-boot:run 目標,這將構建並執行 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.0-M3:run (default-cli) @ spring-boot-docker ---
    [INFO] Attaching agents: []
     .   ____          _            __ _ _
     /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
     ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
     \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
     '  |____| .__|_| |_|_| |_\__, | / / / /
      =========|_|==============|___/=/_/_/_/
    
     :: Spring Boot ::             (v3.3.0-M3)
    
     2024-04-04T15:36:47.202Z  INFO 42 --- [spring-boot-docker] [           main]       
     c.e.s.SpringBootDockerApplication        : Starting SpringBootDockerApplication using Java    
     21.0.2 with PID 42 (/app/target/classes started by root in /app)
     ….
  2. 透過 Web 瀏覽器訪問“Hello World”頁面,地址為 https://:8080,或者透過以下 curl 命令

    $ 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)。此映象提供了執行編譯後的應用程式(JAR 檔案)所需的 Java 執行時環境。

    對於生產環境使用,強烈建議您使用 jlink 生成一個自定義的 JRE 類執行時。所有版本的 Eclipse Temurin 都提供 JRE 映象,但 `jlink` 可以讓您建立一個僅包含應用程式所需 Java 模組的最小執行時。這可以顯著減小最終映象的大小並提高安全性。 請參閱此頁面瞭解更多資訊。

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

  1. 現在,重新構建您的映象並執行您的生產環境就緒的構建。

    $ 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 階段。

  2. 使用 `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 映象更輕量級、更安全、更易於管理。

附加資源