使用 Spring Boot 创建 Docker 镜像

更新于 2025-12-30

Michael Pratt 2024-01-08

1. 引言

随着越来越多的组织转向容器化和虚拟服务器,Docker 正在成为软件开发工作流中越来越重要的一部分。为此,Spring Boot 2.3 引入了一项令人兴奋的新功能:可以轻松地为 Spring Boot 应用程序创建 Docker 镜像。

在本教程中,我们将探讨如何为 Spring Boot 应用程序创建 Docker 镜像。

2. 传统的 Docker 构建方式

使用 Spring Boot 构建 Docker 镜像的传统方法是编写一个 Dockerfile。下面是一个简单的示例:

FROM openjdk:17-jdk-alpine
EXPOSE 8080
ARG JAR_FILE=target/demo-app-1.0.0.jar
ADD ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

然后我们可以使用 docker build 命令来构建 Docker 镜像。这种方式对大多数应用程序来说都能正常工作,但也存在一些缺点。

首先,我们使用的是 Spring Boot 生成的“fat jar”(即包含所有依赖的可执行 JAR 文件)。这可能会影响启动时间,尤其是在容器化环境中。通过将 JAR 文件解压后添加内容,我们可以减少启动时间。

其次,Docker 镜像是分层构建的。而 Spring Boot 的 fat jar 会将所有应用程序代码和第三方库打包到一个单独的层中。这意味着即使只修改了一行代码,整个层也必须重新构建。

如果在构建前先解压 JAR 文件,那么应用程序代码和第三方库就可以分别拥有自己的层。这样我们就能充分利用 Docker 的缓存机制:当仅有一行代码发生更改时,只需重新构建对应的那一层即可。

考虑到这一点,让我们看看 Spring Boot 是如何改进 Docker 镜像构建流程的。

3. Buildpacks(构建包)

Buildpacks 是一种提供框架和应用依赖的工具。

例如,给定一个 Spring Boot 的 fat jar,buildpack 会自动为我们提供 Java 运行时环境。这使我们无需编写 Dockerfile 就能自动生成一个合理的 Docker 镜像。

Spring Boot 同时支持 Maven 和 Gradle 使用 buildpacks。以 Maven 为例,我们可以运行以下命令:

./mvnw spring-boot:build-image

让我们看一下部分关键输出,了解背后发生了什么:

[INFO] Building jar: target/demo-0.0.1-SNAPSHOT.jar
...
[INFO] Building image 'docker.io/library/demo:0.0.1-SNAPSHOT'
...
[INFO]  > Pulling builder image 'gcr.io/paketo-buildpacks/builder:base-platform-api-0.3' 100%
...
[INFO]     [creator]     ===> DETECTING
[INFO]     [creator]     5 of 15 buildpacks participating
[INFO]     [creator]     paketo-buildpacks/bellsoft-liberica 2.8.1
[INFO]     [creator]     paketo-buildpacks/executable-jar    1.2.8
[INFO]     [creator]     paketo-buildpacks/apache-tomcat     1.3.1
[INFO]     [creator]     paketo-buildpacks/dist-zip          1.3.6
[INFO]     [creator]     paketo-buildpacks/spring-boot       1.9.1
...
[INFO] Successfully built image 'docker.io/library/demo:0.0.1-SNAPSHOT'
[INFO] Total time:  44.796 s

第一行显示我们像往常一样构建了标准的 fat jar。

下一行开始构建 Docker 镜像。紧接着,我们看到它拉取了 Paketo 构建器镜像。

Paketo 是 Cloud Native Buildpacks 的一种实现。它会分析我们的项目,并自动确定所需的框架和库。在本例中,它识别出这是一个 Spring Boot 项目,并自动添加了必要的 buildpack。

最后,我们看到生成的 Docker 镜像和总构建时间。注意首次构建时,需要花费相当多的时间下载 buildpack 并创建不同的层。

buildpack 的一大优势在于生成的 Docker 镜像是多层结构。因此,如果我们只修改了应用程序代码,后续构建速度会显著加快:

...
[INFO]     [creator]     Reusing layer 'paketo-buildpacks/executable-jar:class-path'
[INFO]     [creator]     Reusing layer 'paketo-buildpacks/spring-boot:web-application-type'
...
[INFO] Successfully built image 'docker.io/library/demo:0.0.1-SNAPSHOT'
...
[INFO] Total time:  10.591 s

4. 分层 JAR(Layered JARs)

在某些情况下,我们可能不希望使用 buildpacks —— 也许我们的基础设施已经绑定到其他工具,或者我们已经有现成的自定义 Dockerfile 想要复用。

出于这些原因,Spring Boot 也支持使用分层 JAR的方式来构建 Docker 镜像。为了理解其工作原理,我们先看一下典型的 Spring Boot fat jar 的结构:

org/
  springframework/
    boot/
  loader/
...
BOOT-INF/
  classes/
...
lib/
...

fat jar 主要由三部分组成:

  • 启动 Spring 应用所需的引导类(Bootstrap classes)
  • 应用程序代码
  • 第三方库

而在分层 JAR 中,结构类似,但会多出一个 layers.idx 文件,用于将 fat jar 中的每个目录映射到一个 Docker 层:

- "dependencies":
  - "BOOT-INF/lib/"
- "spring-boot-loader":
  - "org/"
- "snapshot-dependencies":
- "application":
  - "BOOT-INF/classes/"
  - "BOOT-INF/classpath.idx"
  - "BOOT-INF/layers.idx"
  - "META-INF/"

Spring Boot 默认提供四个层:

  • dependencies:来自第三方的常规依赖
  • snapshot-dependencies:来自第三方的快照依赖
  • resources:静态资源(注:默认配置中未显式列出,但可通过自定义启用)
  • application:应用程序代码和资源

目标是将应用程序代码和第三方库放入反映其变更频率的独立层中。

例如,应用程序代码通常变更最频繁,因此被分配到独立的 application 层。这样,每一层都可以独立演进,只有当某一层发生变化时,Docker 镜像才需要重新构建该层。

现在我们了解了分层 JAR 的结构,接下来看看如何利用它来构建 Docker 镜像。

4.1 创建分层 JAR

首先,我们需要配置项目以生成分层 JAR。在 Maven 中,这意味着在 pom.xml 的 Spring Boot 插件部分添加如下配置:

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <layers>
            <enabled>true</enabled>
        </layers>
    </configuration>
</plugin>

启用此配置后,执行 mvn package(及其依赖的命令)将生成一个使用上述四个默认层的分层 JAR。

4.2 查看和提取分层

接下来,我们需要从 JAR 中提取各层,以便在 Docker 镜像中正确分层。

要查看某个分层 JAR 的层结构,可运行以下命令:

java -Djarmode=layertools -jar demo-0.0.1.jar list

要提取这些层,可运行:

java -Djarmode=layertools -jar demo-0.0.1.jar extract

4.3 创建 Docker 镜像

将这些层集成到 Docker 镜像中最简单的方法是使用 Dockerfile

FROM openjdk:17-jdk-alpine as builder
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract

FROM openjdk:17-jdk-alpine
COPY --from=builder dependencies/ ./
COPY --from=builder snapshot-dependencies/ ./
COPY --from=builder spring-boot-loader/ ./
COPY --from=builder application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]

这个 Dockerfile 首先从 fat jar 中提取各层,然后将每一层复制到最终的 Docker 镜像中。每个 COPY 指令都会在最终镜像中生成一个新的层。

构建该 Dockerfile 时,可以看到分层 JAR 中的每一层都被作为独立的 Docker 层添加:

...
Step 6/10 : COPY --from=builder dependencies/ ./
 ---> 2c631b8f9993
Step 7/10 : COPY --from=builder snapshot-dependencies/ ./
 ---> 26e8ceb86b7d
Step 8/10 : COPY --from=builder spring-boot-loader/ ./
 ---> 6dd9eaddad7f
Step 9/10 : COPY --from=builder application/ ./
 ---> dc80cc00a655
...

5. 结论

在本教程中,我们探讨了使用 Spring Boot 构建 Docker 镜像的多种方式:

  • 使用 Buildpacks,我们可以无需编写模板代码或自定义配置,即可获得合适的 Docker 镜像;
  • 或者,通过稍加努力使用 分层 JAR,我们可以构建出更精细、更高效的定制化 Docker 镜像。

这两种方法都显著优化了镜像构建效率和运行性能,尤其适合现代云原生应用场景。