多阶段构建

目录

  • 尝试
  • 创建 Dockerfile
  • 构建容器镜像
  • 运行 Spring Boot 应用程序
  • 使用多阶段构建
  • 额外资源

在传统构建中,所有构建指令都在一个构建容器中顺序执行:下载依赖项、编译代码、打包应用程序。所有这些层最终都在你的最终镜像中。这种方法虽然可行,但会导致镜像臃肿,携带不必要的负载,并增加你的安全风险。这时多阶段构建就派上用场了。
多阶段构建在你的 Dockerfile 中引入多个阶段,每个阶段都有特定的目的。可以将其视为在多个不同环境中并行运行构建的不同部分。通过将构建环境与最终运行时环境分离,你可以显著减少镜像大小和攻击面。这对于具有大型构建依赖项的应用程序尤其有利。
多阶段构建推荐用于所有类型的应用程序。
对于解释型语言,如 JavaScript、Ruby 或 Python,你可以在一个阶段中构建和压缩代码,然后将生产就绪的文件复制到一个较小的运行时镜像中。这优化了你的部署镜像。
对于编译型语言,如 C、Go 或 Rust,多阶段构建让你在一个阶段中编译,并将编译好的二进制文件复制到最终的运行时镜像中。无需在最终镜像中捆绑整个编译器。
以下是使用伪代码的多阶段构建结构的简化示例。注意这里有多个 FROM 语句和新的 AS <stage-name>。此外,第二阶段中的 COPY 语句是从前一阶段复制的。

# 第1阶段:构建环境
FROM builder-image AS build-stage 
# 安装构建工具(如 Maven、Gradle)
# 复制源代码
# 构建命令(如编译、打包)

# 第2阶段:运行时环境
FROM runtime-image AS final-stage  
# 从构建阶段复制应用程序工件(如 JAR 文件)
COPY --from=build-stage /path/in/build/stage /path/to/place/in/final/stage
# 定义运行时配置(如 CMD、ENTRYPOINT) 

这个 Dockerfile 使用了两个阶段:

  1. 构建阶段:使用包含编译应用程序所需构建工具的基础镜像。包括安装构建工具、复制源代码和执行构建命令。
  2. 最终阶段:使用适合运行应用程序的较小基础镜像。从构建阶段复制编译好的工件(例如 JAR 文件)。最后,定义用于启动应用程序的运行时配置(使用 CMD 或 ENTRYPOINT)。

尝试

在本手把手指南中,你将学习如何利用多阶段构建为示例 Java 应用程序创建精简高效的 Docker 镜像。你将使用一个简单的基于 Spring Boot 的“Hello World”应用程序作为示例。

下载并安装 Docker Desktop

打开这个预初始化的项目来生成一个 ZIP 文件。如下所示:

Spring Initializr 是一个 Spring 项目的快速启动生成器。它提供了一个可扩展的 API,用于生成基于 JVM 的项目,包含多个常见概念的实现,例如 Java、Kotlin 和 Groovy 的基础语言生成。

选择“Generate”以创建并下载该项目的 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

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 了。

在包含所有其他文件夹和文件(如 srcpom.xml 等)的同一文件夹中创建一个名为 Dockerfile 的文件。

Dockerfile 中,通过添加以下行定义你的基础镜像:

FROM eclipse-temurin:21.0.2_13-jdk-jammy

现在,使用 WORKDIR 指令定义工作目录。这将指定未来命令的运行目录以及文件将在容器镜像内复制的位置。

WORKDIR /app

将 Maven 包装脚本和项目的 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

设置容器启动时要执行的默认命令。该命令指示容器使用 spring-boot:run 目标运行 Maven 包装器 (./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 -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)
 ….

通过浏览器访问 http://localhost:8080 ,或使用以下 curl 命令访问你的“Hello World”页面:

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 类似的运行时。JRE 镜像适用于所有版本的 Eclipse Temurin,但 jlink 允许你创建仅包含应用程序所需 Java 模块的最小运行时。这可以显著减少大小并提高最终镜像的安全性。参考此页面以获取更多信息。

使用多阶段构建,一个 Docker 构建使用一个基础镜像进行编译、打包和单元测试,然后使用另一个镜像进行应用程序运行。因此,最终镜像较小,因为它不包含任何开发或调试工具。通过将构建环境与最终运行时环境分离,你可以显著减少镜像大小并提高最终镜像的安全性。

现在,重建你的镜像并运行准备好的生产构建。

docker build -t spring-helloworld-builder .

此命令使用你所在目录中的 Dockerfile 的最终阶段构建一个名为 spring-helloworld-builderDocker 镜像。

注意

在你的多阶段 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

你的最终镜像只有 428 MB,而原始构建大小为 880 MB。

通过优化每个阶段并仅包含必要内容,你能够显著减少整体镜像大小,同时仍然实现相同的功能。这不仅提高了性能,还使你的 Docker 镜像更加轻量、安全且易于管理。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐