多階段建置
說明
在傳統的建置方式中,所有的建置指令都是在同一個建置容器內依序執行:下載相依套件、編譯程式碼以及封裝應用程式。所有這些層級最終都會留在您的最終映像檔中。這種方法雖然可行,但會導致映像檔變得臃腫,不僅佔用不必要的空間,還會增加您的資安風險。這正是「多階段建置」可以派上用場的地方。
多階段建置在您的 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 檔案)。最後,它定義了啟動應用程式的執行時期設定(使用
CMD或ENTRYPOINT)。
試試看
在本實作指南中,您將發揮多階段建置的威力,為一個 Java 範例應用程式建立精簡且高效的 Docker 映像檔。我們將以一個使用 Maven 建置的簡單「Hello World」Spring Boot 應用程式作為範例。
下載並安裝 Docker Desktop。
開啟此 預先初始化的專案來產生 ZIP 檔案。其外觀如下

Spring Initializr 是一個 Spring 專案的快速啟動產生器。它提供了一個可擴充的 API,用於產生基於 JVM 的專案,並針對幾種常見概念提供實作,例如 Java、Kotlin 和 Groovy 的基礎語言產生功能。
選擇 Generate 來建立並下載此專案的 zip 檔案。
在此示範中,您將 Maven 建置自動化工具與 Java、Spring Web 相依套件以及 Java 21 搭配使用作為您的中繼資料。
瀏覽專案目錄。解壓縮檔案後,您將看到以下專案目錄結構
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 filessrc/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檔案,內容如下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 了。
在包含所有其他資料夾和檔案(如 src、pom.xml 等)的相同資料夾中,建立一個名為
Dockerfile的檔案。在
Dockerfile中,透過加入以下這行來定義您的基礎映像檔FROM eclipse-temurin:21.0.8_9-jdk-jammy現在,使用
WORKDIR指令定義工作目錄。這將指定後續指令運行的位置,以及將檔案複製到容器映像檔內的目標目錄。WORKDIR /app將 Maven 包裝器 (wrapper) 指令碼和您專案的
pom.xml檔案複製到 Docker 容器內的當前工作目錄/app中。COPY .mvn/ .mvn COPY mvnw pom.xml ./在容器內執行指令。它會執行
./mvnw dependency:go-offline指令,該指令使用 Maven 包裝器 (./mvnw) 來下載您專案的所有相依套件,而不會建置最終的 JAR 檔案(有助於加快建置速度)。RUN ./mvnw dependency:go-offline將主機上的
src目錄複製到容器內的/app目錄中。COPY src ./src設定容器啟動時執行的預設指令。此指令指示容器執行帶有
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"]
建置容器映像檔
執行以下指令來建置 Docker 映像檔
$ docker build -t spring-helloworld .使用
docker images指令檢查 Docker 映像檔的大小$ docker images這樣做將會產生類似以下的輸出
REPOSITORY TAG IMAGE ID CREATED SIZE spring-helloworld latest ff708d5ee194 3 minutes ago 880MB此輸出顯示您的映像檔大小為 880MB。它包含了完整的 JDK、Maven 工具鏈等。在生產環境中,您不需要在最終映像檔中包含這些內容。
執行 Spring Boot 應用程式
現在您已經建置好映像檔,是時候執行容器了。
$ 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) ….透過網頁瀏覽器造訪 https://:8080,或使用此 curl 指令來存取您的「Hello World」頁面
$ curl localhost:8080 Hello World
使用多階段建置
考慮以下的 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 建置過程會使用一個基礎映像檔來進行編譯、封裝和單元測試,然後使用另一個獨立的映像檔作為應用程式的執行環境。因此,最終的映像檔體積更小,因為它不包含任何開發或偵錯工具。透過將建置環境與最終執行環境分離,您可以大幅縮減映像檔大小並提升最終映像檔的安全性。
現在,重新建置您的映像檔並執行您準備好用於生產環境的建置版本。
$ 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 階段。使用
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 映像檔更輕量、更安全且更易於管理。