多阶段构建
解释
在传统构建中,所有构建指令都在单个构建容器中按顺序执行:下载依赖项、编译代码和打包应用程序。所有这些图层最终都会形成您的最终图像。这种方法有效,但会导致体积庞大的图像承载不必要的重量并增加安全风险。这就是多阶段构建的用武之地。
多阶段构建在 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 构建的简单的基于 Spring Boot 的“Hello World”应用程序作为示例。
下载并安装Docker Desktop。
打开这个 预初始化的项目以生成 ZIP 文件。看起来是这样的:
Spring Initializr是 Spring 项目的快速入门生成器。它提供了一个可扩展的 API 来生成基于 JVM 的项目,并实现了几个常见概念,例如 Java、Kotlin 和 Groovy 的基本语言生成。
选择“生成”以创建并下载该项目的 zip 文件。
在本演示中,您将 Maven 构建自动化与 Java(Spring Web 依赖项)和用于元数据的 Java 21 配对。
浏览项目目录。解压文件后,您将看到以下项目目录结构:
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 很大并且看起来
令人畏惧。值得庆幸的是,您还不需要了解每一个复杂的细节就可以有效地使用它。创建一个显示“Hello World!”的 RESTful Web 服务。
在该
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
.
Dockerfile
创建一个在包含所有其他文件夹和文件(如 src、pom.xml 等)的同一文件夹中命名的文件。在 中
Dockerfile
,通过添加以下行来定义您的基础映像:FROM eclipse-temurin:21.0.2_13-jdk-jammy
现在,使用指令定义工作目录
WORKDIR
。这将指定未来命令将运行的位置以及目录文件将复制到容器映像内的位置。WORKDIR /app
将 Maven 包装器脚本和项目文件复制到Docker 容器内的
pom.xml
当前工作目录中。/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
设置容器启动时默认执行的命令。此命令指示容器
./mvnw
以目标运行 Maven 包装器 ( )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"]
构建容器镜像
执行以下命令构建Docker镜像:
$ docker build -t spring-helloworld .
使用以下命令检查 Docker 镜像的大小
docker images
:$ docker images
这样做将产生如下输出:
REPOSITORY TAG IMAGE ID CREATED SIZE spring-helloworld latest ff708d5ee194 3 minutes ago 880MB
此输出显示您的图像大小为 880MB。它包含完整的 JDK、Maven 工具链等。在生产中,您的最终图像中不需要它。
运行 Spring Boot 应用程序
现在您已经构建了映像,是时候运行容器了。
$ 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) ….
通过 Web 浏览器访问“Hello World”页面: 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) 环境。该阶段被命名为构建器。
第二阶段是一个名为 的新阶段
final
。自启动以来FROM builder
,它继承了基础阶段(JDK环境)的所有内容。它使用更精简的eclipse-temurin:21.0.2_13-jre-jammy
映像,仅包含运行应用程序所需的 Java 运行时环境 (JRE)。该映像提供了一个 Java 运行时环境 (JRE),足以运行已编译的应用程序(JAR 文件)。
对于生产用途,强烈建议您使用 jlink 生成自定义的类似 JRE 的运行时。 JRE 映像可用于 Eclipse Temurin 的所有版本,但
jlink
允许您创建仅包含应用程序必需的 Java 模块的最小运行时。这可以显着减小最终图像的大小并提高其安全性。 请参阅此页面了解更多信息。
通过多阶段构建,Docker 构建使用一个基础映像进行编译、打包和单元测试,然后使用一个单独的映像用于应用程序运行时。因此,最终映像的尺寸较小,因为它不包含任何开发或调试工具。通过将构建环境与最终运行时环境分开,您可以显着减小映像大小并提高最终映像的安全性。
现在,重建您的映像并运行您的即用型生产版本。
$ docker build -t spring-helloworld-builder .
此命令从当前目录中的文件构建一个
spring-helloworld-builder
使用最后阶段命名的 Docker 映像。Dockerfile
笔记
在你的多阶段 Dockerfile 中,最后一个阶段(final)是默认的构建目标。这意味着如果您没有使用命令
--target
中的标志显式指定目标阶段docker build
,Docker 将默认自动构建最后一个阶段。您可以使用docker build -t spring-helloworld-builder --target builder .
JDK 环境仅构建构建器阶段。使用命令查看图像大小差异
docker images
:$ docker images
您将得到类似于以下内容的输出:
spring-helloworld-builder latest c5c76cb815c0 24 minutes ago 428MB spring-helloworld latest ff708d5ee194 About an hour ago 880MB
与 880 MB 的原始构建大小相比,您的最终映像只有 428 MB。
通过优化每个阶段并仅包含必要的部分,您能够显着减小
整体图像大小,同时仍实现相同的功能。这不仅提高了性能,
还使您的 Docker 映像更轻量、更安全且更易于管理。