多阶段构建
解释
在传统的构建中,所有构建指令都按顺序在单个构建容器中执行:下载依赖项、编译代码和打包应用程序。所有这些层最终都会出现在您的最终镜像中。这种方法是可行的,但它会导致镜像臃肿,携带不必要的重量并增加安全风险。这就是多阶段构建的用武之地。
多阶段构建在 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 文件)。最后,它定义了用于启动应用程序的运行时配置(使用
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 包装器脚本和您项目的
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) ….通过您的 Web 浏览器访问 https://:8080 上的“Hello World”页面,或者使用此 curl 命令:
$ 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)。该镜像提供了一个 Java 运行时环境 (JRE),这足以运行编译后的应用程序(JAR 文件)。
对于生产用途,强烈建议您使用 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 镜像更轻量、更安全、更易于管理。