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 分为两个阶段:
- 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文件。
 - 运行阶段: 使用 
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 作为构建工具。
- builder 阶段: 使用 
gradle:7.6.1-jdk17作为基础镜像,包含了 JDK 和 Gradle。WORKDIR /app:设置工作目录为/app。COPY build.gradle settings.gradle ./和COPY src ./src:将build.gradle、settings.gradle文件和src目录复制到工作目录。RUN gradle build -x test:使用 Gradle 构建应用,并排除测试。构建完成后,build/libs目录下会生成*.jar文件。
 - 运行阶段: 使用 
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 会直接使用缓存的层,而不需要重新下载依赖。
不同构建工具的优化考量
虽然多阶段构建的原理相同,但不同的构建工具在优化方面需要考虑一些差异。
| 构建工具 | 优化考量