Docker 多阶段构建

多阶段构建

目录

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

在传统构建中,所有构建指令都在一个构建容器中顺序执行:下载依赖项、编译代码、打包应用程序。所有这些层最终都在你的最终镜像中。这种方法虽然可行,但会导致镜像臃肿,携带不必要的负载,并增加你的安全风险。这时多阶段构建就派上用场了。

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

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

对于解释型语言,如 JavaScript、Ruby 或 Python,你可以在一个阶段中构建和压缩代码,然后将生产就绪的文件复制到一个较小的运行时镜像中。这优化了你的部署镜像。

对于编译型语言,如 C、Go 或 Rust,多阶段构建让你在一个阶段中编译,并将编译好的二进制文件复制到最终的运行时镜像中。无需在最终镜像中捆绑整个编译器。

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

dockerfile 复制代码
# 第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 配对作为元数据。

导航到项目目录。解压缩文件后,你会看到以下项目目录结构:

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 很庞大,看起来可能令人畏惧。但幸运的是,你还不需要理解其中的每一个细节就可以有效使用它。

创建一个显示"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 了。

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

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

dockerfile 复制代码
FROM eclipse-temurin:21.0.2_13-jdk-jammy

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

dockerfile 复制代码
WORKDIR /app

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

dockerfile 复制代码
COPY .mvn/ .mvn
COPY mvnw pom.xml ./

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

dockerfile 复制代码
RUN ./mvnw dependency:go-offline

将主机机器上的 src 目录复制到容器内的 /app 目录中。

dockerfile 复制代码
COPY src ./src

设置容器启动时要执行的默认命令。该命令指示容器使用 spring-boot:run 目标运行 Maven 包装器 (./mvnw),这将构建并执行你的 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"]

构建容器镜像

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

sh 复制代码
docker build -t spring-helloworld .

使用 docker images 命令检查 Docker 镜像的大小:

sh 复制代码
docker images

执行后将产生如下输出:

plaintext 复制代码
REPOSITORY          TAG       IMAGE ID       CREATED          SIZE
spring-helloworld   latest    ff708d5ee194   3 minutes ago    880MB

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

运行 Spring Boot 应用程序

现在你已经构建了镜像,是时候运行容器了。

sh 复制代码
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)
 ....

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

sh 复制代码
curl localhost:8080

输出:

plaintext 复制代码
Hello World

使用多阶段构建

考虑以下 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。它使用一个更精简的 eclipse-temurin:21.0.2_13-jre-jammy 镜像,只包含运行应用程序所需的 Java 运行时环境 (JRE)。这个镜像提供了一个 Java 运行时环境 (JRE),足以运行编译好的应用程序 (JAR 文件)。

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

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

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

sh 复制代码
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 命令查看镜像大小差异:

sh 复制代码
docker images

你会得到类似以下的输出:

plaintext 复制代码
spring-helloworld-builder latest    c5c76cb815c0   24 minutes ago      428MB
spring-helloworld         latest    ff708d5ee194   About an hour ago   880MB

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

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

相关推荐
群联云防护小杜26 分钟前
如何给负载均衡平台做好安全防御
运维·服务器·网络·网络协议·安全·负载均衡
PyAIGCMaster1 小时前
ubuntu装P104驱动
linux·运维·ubuntu
奈何不吃鱼1 小时前
【Linux】ubuntu依赖安装的各种问题汇总
linux·运维·服务器
aherhuo1 小时前
kubevirt网络
linux·云原生·容器·kubernetes
zzzhpzhpzzz1 小时前
Ubuntu如何查看硬件型号
linux·运维·ubuntu
蜜獾云1 小时前
linux firewalld 命令详解
linux·运维·服务器·网络·windows·网络安全·firewalld
陌北v11 小时前
Docker Compose 配置指南
运维·docker·容器·docker-compose
只会copy的搬运工1 小时前
Jenkins 持续集成部署——Jenkins实战与运维(1)
运维·ci/cd·jenkins
catoop2 小时前
K8s 无头服务(Headless Service)
云原生·容器·kubernetes
娶不到胡一菲的汪大东2 小时前
Ubuntu概述
linux·运维·ubuntu