多阶段构建
说明
在传统构建中,所有构建指令按顺序在一个构建容器中执行:下载依赖、编译代码和打包应用。所有这些层最终都会包含在您的最终镜像中。这种方法可行,但会导致镜像体积庞大,包含不必要的负载,并增加您的安全风险。这就是多阶段构建的用武之地。
多阶段构建在您的 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 镜像。您将使用一个简单的、基于 Spring Boot 并使用 Maven 构建的“Hello World”应用作为示例。
下载并安装 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 files
src/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.2_13-jdk-jammy
现在,使用
WORKDIR
指令定义工作目录。这将指定将来命令执行的位置以及文件将被复制到容器镜像中的目录。WORKDIR /app
将 Maven wrapper 脚本和您的项目
pom.xml
文件都复制到 Docker 容器内的当前工作目录/app
中。COPY .mvn/ .mvn COPY mvnw pom.xml ./
在容器内执行一条命令。它运行
./mvnw dependency:go-offline
命令,该命令使用 Maven wrapper (./mvnw
) 下载项目的所有依赖项,而无需构建最终的 JAR 文件(这对于加快构建速度很有用)。RUN ./mvnw dependency:go-offline
将主机上项目的
src
目录复制到容器内的/app
目录中。COPY src ./src
设置容器启动时将执行的默认命令。此命令指示容器使用
spring-boot:run
目标运行 Maven wrapper (./mvnw
),这将构建并执行您的 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"]
构建容器镜像
执行以下命令来构建 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 浏览器访问 http://localhost:8080,或者通过以下 curl 命令访问:
$ curl localhost:8080 Hello World
使用多阶段构建
考虑以下 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)。此镜像提供了一个 Java 运行时环境(JRE),足以运行编译后的应用(JAR 文件)。
对于生产环境使用,强烈建议您使用 jlink 生成自定义的 JRE 类运行时。所有版本的 Eclipse Temurin 都提供了 JRE 镜像,但 jlink 允许您创建一个仅包含应用所需 Java 模块的最小化运行时。这可以显著减小最终镜像的大小并提高安全性。请参阅此页面获取更多信息。
使用多阶段构建时,Docker 构建会使用一个基础镜像进行编译、打包和单元测试,然后使用另一个独立的镜像作为应用运行时。因此,最终镜像的体积会更小,因为它不包含任何开发或调试工具。通过将构建环境与最终运行时环境分开,您可以显著减小镜像大小并提高最终镜像的安全性。
现在,重新构建您的镜像并运行随时可用的生产构建。
$ docker build -t spring-helloworld-builder .
此命令使用当前目录中 Dockerfile 文件中的
final
阶段构建一个名为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 镜像更轻量、更安全、更易于管理。