使用 Spring Boot 创建 Docker 镜像

1、概览

随着越来越多的企业转向使用容器,Docker 在软件开发中的地位也越来越重要。为此,Spring Boot 2.3 的一大新功能就是为 Spring Boot 应用轻松创建 Docker 镜像提供了支持。

本文将带你了解如何为 Spring Boot 应用创建 Docker 镜像。

2、传统的 Docker 构建

使用 Spring Boot 构建 Docker 镜像的传统方法是使用 Dockerfile

下面是一个简单的 Dockerfile 示例:

FROM openjdk:8-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 文件的解压内容来节省启动时间。

其次,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,就像任何典型的 maven package 一样。

接下来的一行开始构建 Docker 镜像。紧接着,我们可以看到构建过程拉取了 Packeto Builder。

Packeto 是云原生 Buildpacks 的实现。它会分析我们的项目并确定所需的框架和库。在本例中,它确定我们有一个 Spring Boot 项目,并添加了所需的 Buildpacks。

最后,可以看到生成的 Docker 镜像和总的构建时间。注意,第一次构建时,会花费大量时间下载 Buildpacks 和创建不同的层。

Buildpacks 的一大特点是 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

在某些情况下,我们可能不希望使用 Buildpacks,可能是因为我们的基础架构已经与其他工具绑定,或者我们已经有了要重用的自定义 Dockerfiles

因此,Spring Boot 也支持使用分层 Jar 构建 Docker 镜像。为了解其工作原理,先来看看典型的 Spring Boot Fat Jar 布局:

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

Fat Jar 主要由 3 个区域组成:

  • 启动 Spring 应用所需的 Bootstrap 类
  • 应用代码
  • 第三方依赖库

对于分层 Jar,结构看起来类似,但我们会得到一个新的 layers.idx 文件,它将 Fat Jar 中的每个目录映射到一个层:

- "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 提供了四个默认的层(layer):

  • dependencies:来自第三方的典型依赖
  • snapshot-dependencies:来自第三方的快照依赖
  • resources:静态资源
  • application:应用代码和资源

我们的目标是将应用代码和第三方库放置到能反映其变化频率的层中。

例如,应用代码可能是变化最频繁的,因此它有自己的层。此外,每个层都可以独立演化,只有当某个层发生变化时,才会重新构建该层的 Docker 镜像。

在了解了新的分层 Jar 结构后,来看看如何利用它来构建 Docker 镜像。

4.1、创建分层 Jar

首先,需要设置项目以创建分层 Jar 文件。对于 Maven 来说,这意味着在 POM 文件的 Spring Boot 插件部分添加一个新的配置:

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

使用此配置后,Maven 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 adoptopenjdk:11-jre-hotspot as builder
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract

FROM adoptopenjdk:11-jre-hotspot
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 指令都会在最终的 Docker 镜像中产生一个新层。

构建这个 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 镜像。


Ref:https://www.baeldung.com/spring-boot-docker-images