Skip to content

多阶段构建

原文:https://docs.docker.com/guides/docker-concepts/building-images/multi-stage-builds/

解释

在传统的构建中,所有构建指令都按顺序在一个单一的构建容器中执行:下载依赖、编译代码以及打包应用程序。所有这些层都会出现在您的最终镜像中。这种方法是可行的,但它会导致生成体积庞大的镜像,携带不必要的负担,并增加您的安全风险。这就是多阶段构建发挥作用的地方。

多阶段构建在您的 Dockerfile 中引入了多个阶段,每个阶段都有特定的目的。可以将其视为在多个不同环境中同时运行构建的不同部分的能力。通过将构建环境与最终运行时环境分离,您可以显著减小镜像大小并减小攻击面。这对于具有大量构建依赖的应用程序特别有益。

推荐所有类型的应用程序都使用多阶段构建。

  • 对于解释型语言,如 JavaScript、Ruby 或 Python,您可以在一个阶段构建并压缩代码,并将生产就绪的文件复制到更小的运行时镜像中。这优化了您的镜像以便部署。
  • 对于编译型语言,如 C、Go 或 Rust,多阶段构建让您可以在一个阶段编译,并将编译后的二进制文件复制到最终的运行时镜像中。无需在最终镜像中捆绑整个编译器。

这里是一个使用伪代码的简化的多阶段构建结构示例。注意有多个FROM语句和一个新的AS <stage-name>。此外,第二阶段的COPY语句正在从前一阶段复制--from

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

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

这个 Dockerfile 使用了两个阶段:

  • 构建阶段使用一个包含构建工具的基础镜像来编译您的应用程序。它包括安装构建工具、复制源代码和执行构建命令的指令。
  • 最终阶段使用一个适合运行您的应用程序的更小的基础镜像。它从构建阶段复制了编译后的工件(例如,一个 JAR 文件)。最后,它定义了用于启动您的应用程序的运行时配置(使用CMDENTRYPOINT)。

现在试试

在这个动手指南中,您将解锁多阶段构建的力量,创建精简且高效的 Docker 镜像,用于一个示例 Java 应用程序。您将使用一个基于 Spring Boot 的简单“Hello World”应用程序作为示例,该应用程序使用 Maven 构建。

  1. 下载并安装 Docker Desktop。

  2. 打开这个预初始化项目以生成 ZIP 文件。以下是它的外观:

    Spring Initializr工具的截图,选择了Java 21、Spring Web和Spring Boot 3.3.0

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

    选择Generate来创建并下载这个项目的 zip 文件。

    在此演示中,您将 Maven 构建自动化与 Java、Spring Web 依赖以及 Java 21 元数据配对使用。

  3. 导航项目目录。解压文件后,您将看到以下项目目录结构:

    plaintext
    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 非常庞大,可能看起来令人望而却步。幸运的是,您现在还不需要了解每一个细节就能有效地使用它。

  4. 创建一个显示“Hello World!”的 RESTful Web 服务。

    src/main/java/com/example/springbootdocker/目录下,您可以用以下内容修改SpringBootDockerApplication.java文件:

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

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

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

    dockerfile
    FROM eclipse-temurin:21.0.2_13-jdk-jammy
  3. 现在,使用WORKDIR指令定义工作目录。这将指定将来命令将在容器镜像内的哪个目录运行并复制文件。

    dockerfile
    WORKDIR /app
  4. 将 Maven 包装脚本和项目的pom.xml文件复制到 Docker 容器中的当前工作目录/app中。

    dockerfile
    COPY .mvn/ .mvn
    COPY mvnw pom.xml ./
  5. 在容器内执行命令。它运行./mvnw dependency:go-offline命令,该命令使用 Maven 包装器(./mvnw)下载项目的所有依赖项,而不构建最终的 JAR 文件(有助于更快的构建)。

    dockerfile
    RUN ./mvnw dependency:go-offline
  6. 将您主机上的项目的src目录复制到容器内的/app目录中。

    dockerfile
    COPY src ./src
  7. 设置容器启动时执行的默认命令。此命令指示容器运行 Maven 包装器(./mvnw)与spring-boot:run目标,这将构建并执行您的 Spring Boot 应用程序。

    dockerfile
    CMD ["./mvnw", "spring-boot:run"]

    至此,您应该拥有以下 Dockerfile:

    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"]

构建容器镜像

  1. 执行以下命令构建 Docker 镜像:

    console
    docker build -t spring-helloworld .
  2. 使用docker images命令检查 Docker 镜像的大小:

    console
    docker images

    执行此操作将产生如下输出:

    console
    REPOSITORY          TAG       IMAGE ID       CREATED          SIZE
    spring-helloworld   latest    ff708d5ee194   3分钟前          880MB

    此输出显示您的镜像大小为 880MB。它包含完整的 JDK、Maven 工具链等。在生产中,您不需要在最终镜像中包含这些。

运行 Spring Boot 应用程序

  1. 现在您已经构建了一个镜像,是时候运行容器了。

    console
    docker run -d -p 8080:8080 spring-helloworld

    然后,您将在容器日志中看到类似以下的输出:

    plaintext
    [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)
     ….
  2. 通过您的 Web 浏览器在http://localhost:8080访问您的“Hello World”页面,或通过此 curl 命令:

    console
    curl localhost:8080
    Hello World

使用多阶段构建

  1. 考虑以下 Dockerfile:

    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。由于它从 builder 开始,因此继承了基础阶段(JDK 环境)的所有内容。它使用一个更精简的eclipse-temurin:21.0.2_13-jre-jammy镜像,只包含运行应用程序所需的 Java 运行时环境(JRE)。这个镜像提供了一个 Java 运行时环境(JRE),足以运行编译后的应用程序(JAR 文件)。

对于生产环境,强烈推荐您使用 jlink 制作类似 JRE 的自定义运行时。所有版本的 Eclipse Temurin 都提供 JRE 镜像,但jlink允许您创建一个只包含应用程序所需的 Java 模块的最小运行时。这可以显著减小您最终镜像的大小并提高其安全性。参考这个页面获取更多信息。

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

  1. 现在,重新构建您的镜像并运行您的生产就绪构建。

    console
    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 阶段。

  2. 使用docker images命令查看镜像大小的差异:

    console
    docker images

    您将获得类似以下的输出:

    console
    spring-helloworld-builder latest    c5c76cb815c0   24分钟前      428MB
    spring-helloworld         latest    ff708d5ee194   大约一小时前   880MB

    您的最终镜像仅为 428 MB,与原始构建大小 880 MB 相比。

    通过优化每个阶段并只包含所需的内容,您能够显著减小整体镜像大小,同时仍然实现相同的功能。

这不仅提高了性能,还使您的 Docker 镜像更加轻便、更安全,更易于管理。

额外资源