JAVA 使用 Dockerfile 构建体积过大?多阶段构建优化技巧

JAVA 应用 Dockerfile 构建体积优化:多阶段构建技巧

大家好,今天我们来聊聊如何通过 Dockerfile 的多阶段构建技术来优化 Java 应用的 Docker 镜像体积。相信很多开发者都遇到过这样的问题:辛辛苦苦写完 Java 代码,构建出来的 Docker 镜像却动辄几个 GB,这不仅浪费存储空间,也影响了镜像的传输速度和部署效率。

镜像体积过大的常见原因

在深入多阶段构建之前,我们先了解一下导致 Java 应用 Docker 镜像体积过大的常见原因:

  • 包含构建工具和依赖: 很多 Dockerfile 会直接在最终镜像中包含 Maven、Gradle 等构建工具,以及构建过程中下载的所有依赖。这些工具和依赖在应用运行阶段是不需要的,却白白占用了空间。
  • JDK 完整版: 完整的 JDK 包含了大量的工具和库,而 Java 应用在运行时只需要 JRE (Java Runtime Environment) 即可。
  • 缓存数据: 构建过程中产生的缓存数据,例如 Maven 的本地仓库,如果没有清理,也会被包含在最终镜像中。
  • 冗余文件: 有些文件可能在构建过程中被复制到镜像中,但实际应用运行时并不需要。

多阶段构建的原理

多阶段构建 (Multi-Stage Builds) 是 Docker 17.05 版本引入的一项功能,它允许在一个 Dockerfile 中使用多个 FROM 指令,每个 FROM 指令都代表一个新的构建阶段。每个阶段都可以使用不同的基础镜像,执行不同的构建操作。最终,我们可以选择性地将前一个阶段的产物复制到最终的镜像中,而不需要包含整个构建环境。

简单来说,多阶段构建就像流水线一样,每个阶段完成特定的任务,并将结果传递给下一个阶段。这样,我们就可以在一个阶段中使用完整的构建环境(例如包含 JDK 和 Maven),而在最终的阶段只包含运行应用所需的最小依赖。

多阶段构建的基本语法

Dockerfile 中使用多阶段构建的基本语法如下:

FROM <image> AS <name>

# 构建阶段的操作

FROM <image>

# 复制前一个阶段的产物
COPY --from=<name> <source> <destination>

# 最终镜像的操作
  • FROM <image> AS <name>:定义一个新的构建阶段,并指定基础镜像和阶段名称。阶段名称可以省略,但建议使用名称以便后续引用。
  • COPY --from=<name> <source> <destination>:从指定名称的构建阶段复制文件或目录到当前阶段。

使用 Maven 构建 Java 应用的多阶段 Dockerfile 示例

下面是一个使用 Maven 构建 Java 应用的多阶段 Dockerfile 示例:

# 构建阶段:使用 Maven 构建应用
FROM maven:3.8.6-openjdk-17 AS builder
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean install -DskipTests

# 运行阶段:使用 JRE 运行应用
FROM openjdk:17-jre-slim
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

这个 Dockerfile 分为两个阶段:

  1. builder 阶段: 使用 maven:3.8.6-openjdk-17 作为基础镜像,包含了 JDK 和 Maven。
    • WORKDIR /app:设置工作目录为 /app
    • COPY pom.xml .COPY src ./src:将 pom.xml 文件和 src 目录复制到工作目录。
    • RUN mvn clean install -DskipTests:使用 Maven 构建应用,并跳过测试。构建完成后,target 目录下会生成 *.jar 文件。
  2. 运行阶段: 使用 openjdk:17-jre-slim 作为基础镜像,这是一个精简的 JRE 镜像。
    • WORKDIR /app:设置工作目录为 /app
    • COPY --from=builder /app/target/*.jar app.jar:从 builder 阶段的 /app/target 目录复制 *.jar 文件到当前阶段的 /app 目录,并重命名为 app.jar
    • EXPOSE 8080:声明应用监听 8080 端口。
    • ENTRYPOINT ["java", "-jar", "app.jar"]:设置容器启动时执行的命令,即运行 app.jar 文件。

通过这种方式,最终的镜像只包含运行 Java 应用所需的 JRE 和 JAR 文件,而不需要包含 Maven 和完整的 JDK。

使用 Gradle 构建 Java 应用的多阶段 Dockerfile 示例

下面是一个使用 Gradle 构建 Java 应用的多阶段 Dockerfile 示例:

# 构建阶段:使用 Gradle 构建应用
FROM gradle:7.6.1-jdk17 AS builder
WORKDIR /app
COPY build.gradle settings.gradle ./
COPY src ./src
RUN gradle build -x test

# 运行阶段:使用 JRE 运行应用
FROM openjdk:17-jre-slim
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

这个 Dockerfile 与 Maven 的示例类似,只是使用了 Gradle 作为构建工具。

  1. builder 阶段: 使用 gradle:7.6.1-jdk17 作为基础镜像,包含了 JDK 和 Gradle。
    • WORKDIR /app:设置工作目录为 /app
    • COPY build.gradle settings.gradle ./COPY src ./src:将 build.gradlesettings.gradle 文件和 src 目录复制到工作目录。
    • RUN gradle build -x test:使用 Gradle 构建应用,并排除测试。构建完成后,build/libs 目录下会生成 *.jar 文件。
  2. 运行阶段: 使用 openjdk:17-jre-slim 作为基础镜像,这是一个精简的 JRE 镜像。
    • WORKDIR /app:设置工作目录为 /app
    • COPY --from=builder /app/build/libs/*.jar app.jar:从 builder 阶段的 /app/build/libs 目录复制 *.jar 文件到当前阶段的 /app 目录,并重命名为 app.jar
    • EXPOSE 8080:声明应用监听 8080 端口。
    • ENTRYPOINT ["java", "-jar", "app.jar"]:设置容器启动时执行的命令,即运行 app.jar 文件。

多阶段构建的优势

使用多阶段构建可以带来以下优势:

  • 减小镜像体积: 避免在最终镜像中包含不必要的构建工具和依赖。
  • 提高构建速度: 每个阶段可以并行构建,提高整体构建速度。
  • 增强安全性: 减少最终镜像中包含的工具,降低安全风险。
  • 简化 Dockerfile: 将构建过程分解为多个阶段,使 Dockerfile 更加清晰易懂。

进一步优化:利用 .dockerignore 文件

.dockerignore 文件类似于 .gitignore 文件,用于指定在构建镜像时需要忽略的文件和目录。合理使用 .dockerignore 文件可以避免将不必要的文件复制到镜像中,进一步减小镜像体积。

例如,可以在 .dockerignore 文件中添加以下内容:

.git
target/
build/
*.log

这样,在构建镜像时,.git 目录、target 目录、build 目录和所有 .log 文件都会被忽略。

更进一步优化:分层构建与缓存利用

Docker 镜像是由多个层组成的,每一层对应 Dockerfile 中的一条指令。当 Docker 构建镜像时,会尝试利用缓存。如果 Dockerfile 中的指令没有发生变化,Docker 会直接使用缓存的层,而不需要重新执行该指令。

为了更好地利用缓存,建议遵循以下原则:

  • 将变化频率较低的指令放在前面: 例如,可以先复制依赖文件,然后安装依赖,最后复制源代码。
  • 尽量避免在同一层中修改多个文件: 这样可以避免因为其中一个文件的变化而导致整个层失效。

例如,可以将 Maven 的依赖下载单独作为一个阶段:

FROM maven:3.8.6-openjdk-17 AS dependencies
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline

FROM maven:3.8.6-openjdk-17 AS builder
WORKDIR /app
COPY --from=dependencies /root/.m2 ./.m2
COPY pom.xml .
COPY src ./src
RUN mvn clean install -DskipTests

FROM openjdk:17-jre-slim
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

在这个示例中,dependencies 阶段专门用于下载 Maven 依赖。如果 pom.xml 文件没有发生变化,Docker 会直接使用缓存的层,而不需要重新下载依赖。

不同构建工具的优化考量

虽然多阶段构建的原理相同,但不同的构建工具在优化方面需要考虑一些差异。

| 构建工具 | 优化考量

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注