Spring Boot 与 Docker

本指南将引导您完成为运行 Spring Boot 应用程序构建 Docker 镜像的过程。我们将从一个基本的 Dockerfile 开始,然后进行一些调整。之后,我们将介绍两种不使用 docker 命令而使用构建插件(适用于 Maven 和 Gradle)的选项。这是一个“入门”指南,因此范围仅限于一些基本需求。如果您正在为生产环境构建容器镜像,有许多事项需要考虑,而一本简短的指南不可能涵盖所有内容。

还有一份关于 Docker 的 专题指南,它涵盖了比本指南更广泛的选择,并且更为详细。

您将构建什么

Docker 是一个 Linux 容器管理工具包,具有“社交”特性,允许用户发布容器镜像并使用其他人发布的镜像。Docker 镜像是一个运行容器化进程的“配方”。在本指南中,我们将为一个简单的 Spring Boot 应用程序构建一个镜像。

您需要准备什么

如果您不使用 Linux 机器,则需要一个虚拟化服务器。如果您安装了 VirtualBox,其他工具(如 Mac 的 boot2docker)可以无缝地为您管理它。请访问 VirtualBox 的下载站点并选择适合您机器的版本。下载并安装。无需担心实际运行它。

您还需要 Docker,它只在 64 位机器上运行。有关为您的机器设置 Docker 的详细信息,请参阅 https://docs.container.net.cn/installation/#installation。在继续之前,请验证您可以在 shell 中运行 docker 命令。如果您使用 boot2docker,您需要 **首先** 运行它。

从 Spring Initializr 开始

您可以使用这个 预先初始化好的项目,然后点击“生成”下载 ZIP 文件。这个项目已配置为适应本教程中的示例。

手动初始化项目

  1. 导航到 https://start.spring.io。此服务会为您拉取应用程序所需的所有依赖项,并为您完成大部分设置。

  2. 选择 Gradle 或 Maven 以及您想要使用的语言。本指南假设您选择了 Java。

  3. 点击 Dependencies 并选择 Spring Web

  4. 单击生成

  5. 下载生成的 ZIP 文件,这是一个已根据您的选择配置好的 Web 应用程序存档。

如果您的 IDE 集成了 Spring Initializr,您可以从 IDE 中完成此过程。
您还可以从 Github fork 该项目并在您的 IDE 或其他编辑器中打开它。

设置 Spring Boot 应用程序

现在您可以创建一个简单的应用程序

src/main/java/hello/Application.java

package hello;

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;

@SpringBootApplication
@RestController
public class Application {

  @RequestMapping("/")
  public String home() {
    return "Hello Docker World";
  }

  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }

}

该类被标记为 @SpringBootApplication@RestController,这意味着 Spring MVC 已准备好使用它来处理 Web 请求。@RequestMapping/ 映射到 home() 方法,该方法会发送一个 Hello World 响应。main() 方法使用 Spring Boot 的 SpringApplication.run() 方法来启动应用程序。

现在我们可以不使用 Docker 容器(即在宿主操作系统中)运行应用程序

如果您使用 Gradle,请运行以下命令

./gradlew build && java -jar build/libs/gs-spring-boot-docker-0.1.0.jar

如果您使用 Maven,请运行以下命令

./mvnw package && java -jar target/gs-spring-boot-docker-0.1.0.jar

然后访问 localhost:8080 查看您的“Hello Docker World”消息。

容器化

Docker 使用简单的 “Dockerfile” 文件格式来指定镜像的“层”。在您的 Spring Boot 项目中创建以下 Dockerfile:

示例 1. Dockerfile
FROM openjdk:8-jdk-alpine
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

如果您使用 Gradle,您可以使用以下命令运行它

docker build --build-arg JAR_FILE=build/libs/\*.jar -t springio/gs-spring-boot-docker .

如果您使用 Maven,您可以使用以下命令运行它

docker build -t springio/gs-spring-boot-docker .

此命令将构建一个镜像,并将其标记为 springio/gs-spring-boot-docker

这个 Dockerfile 非常简单,但它是运行一个简陋的 Spring Boot 应用程序所需的一切:只有 Java 和一个 JAR 文件。构建会创建一个 spring 用户和 spring 组来运行应用程序。然后,它将项目 JAR 文件(通过 COPY 命令)复制到容器中作为 app.jar,并在 ENTRYPOINT 中运行。使用 Dockerfile ENTRYPOINT 的数组形式是为了避免 shell 包装 Java 进程。关于 Docker 的 专题指南 更详细地介绍了这个主题。

为了减少 Tomcat 启动时间,我们过去曾添加一个系统属性,指向 /dev/urandom 作为熵源。但对于 JDK 8 或更高版本,这不再是必需的。

以用户权限运行应用程序有助于减轻一些风险(例如,请参阅 StackExchange 上的一个帖子)。因此,对 Dockerfile 的一个重要改进是作为非 root 用户运行应用程序。

示例 2. Dockerfile
FROM openjdk:8-jdk-alpine
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

构建和运行应用程序时,您可以在应用程序启动日志中看到用户名。

docker build -t springio/gs-spring-boot-docker .
docker run -p 8080:8080 springio/gs-spring-boot-docker

注意第一个 INFO 日志条目中的 started by

 :: Spring Boot ::        (v2.2.1.RELEASE)

2020-04-23 07:29:41.729  INFO 1 --- [           main] hello.Application                        : Starting Application on b94c86e91cf9 with PID 1 (/app started by spring in /)
...

此外,Spring Boot 的 fat JAR 文件在依赖项和应用程序资源之间有清晰的分离,我们可以利用这一点来提高性能。关键是在容器文件系统上创建分层。这些层在构建时和运行时(在大多数运行时环境中)都会被缓存,因此我们希望最常更改的资源(通常是应用程序本身的类和静态资源)在更慢更改的资源 **之后** 进行分层。因此,我们使用了稍有不同的 Dockerfile 实现。

示例 3. Dockerfile
FROM openjdk:8-jdk-alpine
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring
ARG DEPENDENCY=target/dependency
COPY ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY ${DEPENDENCY}/META-INF /app/META-INF
COPY ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]

这个 Dockerfile 有一个 DEPENDENCY 参数,指向一个我们解压了 fat JAR 的目录。要使用 Gradle 的 DEPENDENCY 参数,请运行以下命令:

mkdir -p build/dependency && (cd build/dependency; jar -xf ../libs/*.jar)

要使用 Maven 的 DEPENDENCY 参数,请运行以下命令:

mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar)

如果我们做得正确,它已经包含了一个 BOOT-INF/lib 目录,其中包含依赖项 JAR,以及一个 BOOT-INF/classes 目录,其中包含应用程序类。请注意,我们使用了应用程序自己的主类:hello.Application。(这比使用 fat JAR 启动器的间接方式更快。)

解压 JAR 文件可能导致类路径顺序在 运行时发生变化。一个行为良好、编写良好的应用程序应该不关心这一点,但如果依赖项管理不当,您可能会看到行为上的变化。
如果您使用 boot2docker,您需要 **首先** 运行它,然后再进行任何 Docker 命令行或构建工具的操作(它会运行一个守护进程,在虚拟机中为您处理工作)。

从 Gradle 构建中,您需要在 Docker 命令行中添加显式的构建参数:

docker build --build-arg DEPENDENCY=build/dependency -t springio/gs-spring-boot-docker .

要在 Maven 中构建镜像,您可以使用更简单的 Docker 命令行:

docker build -t springio/gs-spring-boot-docker .
如果您只使用 Gradle,您可以更改 Dockerfile,使 DEPENDENCY 的默认值与解压后归档的位置匹配。

与其使用 Docker 命令行构建,不如使用构建插件。Spring Boot 支持使用其自身的构建插件通过 Maven 或 Gradle 构建容器。Google 还有一个名为 Jib 的开源工具,它提供了 Maven 和 Gradle 插件。这种方法的优点可能在于您不需要 Dockerfile。您可以使用与 docker build 相同的标准容器格式来构建镜像。此外,它可以在未安装 Docker 的环境(在构建服务器中很常见)中工作。

默认情况下,由默认构建包生成的镜像不会以 root 用户身份运行您的应用程序。请查阅 GradleMaven 的配置指南,了解如何更改默认设置。

使用 Gradle 构建 Docker 镜像

您可以使用一个命令通过 Gradle 构建一个已标记的 Docker 镜像:

./gradlew bootBuildImage --imageName=springio/gs-spring-boot-docker

使用 Maven 构建 Docker 镜像

为了快速入门,您可以直接运行 Spring Boot 镜像生成器,而无需更改 pom.xml(请记住,如果存在 Dockerfile,它将被忽略)。

./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=springio/gs-spring-boot-docker

要推送到 Docker 仓库,您需要有推送权限,而默认情况下您是没有的。将镜像前缀更改为您自己的 Dockerhub ID,并运行 docker login 以确保在运行 Docker 命令前已进行身份验证。

推送之后

示例中的 docker push 会失败(除非您是 Dockerhub 上的“springio”组织成员)。但是,如果您将配置更改为匹配您自己的 Docker ID,它应该会成功。然后您就拥有了一个新标记、已部署的镜像。

您 **不必** 注册 Docker 或发布任何内容即可运行本地构建的 Docker 镜像。如果您使用 Docker(从命令行或 Spring Boot)构建,您仍然拥有一个本地标记的镜像,并且可以像这样运行它:

$ docker run -p 8080:8080 -t springio/gs-spring-boot-docker
Container memory limit unset. Configuring JVM for 1G container.
Calculated JVM Memory Configuration: -XX:MaxDirectMemorySize=10M -XX:MaxMetaspaceSize=86381K -XX:ReservedCodeCacheSize=240M -Xss1M -Xmx450194K (Head Room: 0%, Loaded Class Count: 12837, Thread Count: 250, Total Memory: 1073741824)
....
2015-03-31 13:25:48.035  INFO 1 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2015-03-31 13:25:48.037  INFO 1 --- [           main] hello.Application                        : Started Application in 5.613 seconds (JVM running for 7.293)
构建包在运行时使用内存计算器来调整 JVM 的大小以适应容器。

然后,您可以通过 https://:8080 访问该应用程序(访问它会显示“Hello Docker World”)。

在使用带有 boot2docker 的 Mac 时,您通常会在启动时看到类似以下内容:

Docker client to the Docker daemon, please set:
    export DOCKER_CERT_PATH=/Users/gturnquist/.boot2docker/certs/boot2docker-vm
    export DOCKER_TLS_VERIFY=1
    export DOCKER_HOST=tcp://192.168.59.103:2376

要查看应用程序,您必须访问 DOCKER_HOST 中的 IP 地址,而不是 localhost — 在这种情况下,是 https://192.168.59.103:8080,即 VM 的公共对外 IP。

当它运行时,您可以在容器列表中看到类似如下的示例:

$ docker ps
CONTAINER ID        IMAGE                                   COMMAND                  CREATED             STATUS              PORTS                    NAMES
81c723d22865        springio/gs-spring-boot-docker:latest   "java -Djava.secur..."   34 seconds ago      Up 33 seconds       0.0.0.0:8080->8080/tcp   goofy_brown

要再次关闭它,您可以使用上一个列表中容器的 ID 运行 docker stop(您的 ID 将不同)。

docker stop goofy_brown
81c723d22865

如果您愿意,完成使用后也可以删除该容器(它会持久化存储在您的文件系统中,位于 /var/lib/docker 下)。

docker rm goofy_brown

使用 Spring Profiles

使用 Spring Profiles 运行您新创建的 Docker 镜像,就像将环境变量传递给 Docker 运行命令一样简单(用于 prod profile):

docker run -e "SPRING_PROFILES_ACTIVE=prod" -p 8080:8080 -t springio/gs-spring-boot-docker

您可以对 dev profile 做同样的事情:

docker run -e "SPRING_PROFILES_ACTIVE=dev" -p 8080:8080 -t springio/gs-spring-boot-docker

在 Docker 容器中调试应用程序

要调试应用程序,您可以使用 JPDA 传输。我们将容器视为远程服务器。要启用此功能,请在 JAVA_OPTS 变量中传递 Java 代理设置,并在容器运行时将代理的端口映射到 localhost。对于 Docker for Mac,存在一个限制,因为我们无法在没有 一些技巧 的情况下通过 IP 访问容器。

docker run -e "JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,address=5005,server=y,suspend=n" -p 8080:8080 -p 5005:5005 -t springio/gs-spring-boot-docker

总结

恭喜!您已经为一个 Spring Boot 应用程序创建了一个 Docker 容器!默认情况下,Spring Boot 应用程序在容器内运行在 8080 端口,我们通过在命令行中使用 -p 将其映射到主机上的相同端口。

另请参阅

以下指南也可能有所帮助

想写新指南或为现有指南做贡献吗?请查看我们的贡献指南

所有指南的代码均采用 ASLv2 许可,文字内容采用署名-禁止演绎知识共享许可

获取代码