Spring Boot 与 Docker

本指南将引导您完成为运行 Spring Boot 应用构建 Docker 镜像的过程。我们从一个基本的 Dockerfile 开始,并做一些调整。然后,我们将展示一些使用构建插件(针对 Maven 和 Gradle)而不是 docker 的选项。这是一个“入门”指南,因此范围仅限于一些基本需求。如果您要构建用于生产环境的容器镜像,则需要考虑很多因素,而本简短指南无法涵盖所有内容。

还有一个关于 Docker 的专题指南,它涵盖了比本指南更广泛的选择,并且细节更丰富。

您将构建什么

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

您将需要什么

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

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

从 Spring Initializr 开始

您可以使用这个预初始化的项目并点击 Generate 下载 ZIP 文件。此项目已配置为适合本教程中的示例。

手动初始化项目

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

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

  3. 点击 Dependencies 并选择 Spring Web

  4. 点击 Generate

  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 的数组形式被使用,以便 Java 进程没有 shell 包装。关于 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 的目录。要将 DEPENDENCY 参数与 Gradle 一起使用,运行以下命令

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

要将 DEPENDENCY 参数与 Maven 一起使用,运行以下命令

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

如果我们做对了,它会包含一个包含依赖 JAR 的 BOOT-INF/lib 目录,以及一个包含应用类的 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 的环境中使用(在构建服务器中并不少见)。

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

使用 Gradle 构建 Docker 镜像

您可以使用一个命令通过 Gradle 构建带标签的 Docker 镜像

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

使用 Maven 构建 Docker 镜像

为了快速开始,您甚至无需修改 pom.xml 即可运行 Spring Boot 镜像生成器(请记住,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)
buildpack 在运行时使用内存计算器来调整 JVM 以适应容器大小。

然后可以通过 http://localhost: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——在这种情况下,是虚拟机的公共 IP https://192.168.59.103:8080

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

$ 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 run 命令传递一个环境变量一样简单(对于 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 Transport。我们将容器视为远程服务器。要启用此功能,请在 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 许可发布,文字部分采用 署名-禁止演绎 创意共享许可 发布。

获取代码