本指南将引导您完成为运行 Spring Boot 应用程序构建 Docker 镜像的过程。我们从基本的 Dockerfile
开始,并进行一些调整。然后,我们展示了几种使用构建插件(Maven 和 Gradle)而不是 docker
的选项。这是一份“入门”指南,因此范围仅限于一些基本需求。如果您正在为生产环境构建容器镜像,需要考虑许多因素,而这些内容无法在一份简短的指南中全部涵盖。
这里还有一个 Docker 主题指南,它涵盖了我们在这里提到的更多选择,并且更加详细。
你将构建什么
Docker 是一个具有“社交”功能的 Linux 容器管理工具包,允许用户发布容器镜像并使用他人发布的镜像。Docker 镜像是一个用于运行容器化进程的模板。在本指南中,我们将为一个简单的 Spring Boot 应用程序构建一个镜像。
所需条件
-
大约15分钟
-
常用的文本编辑器或IDE
-
Java 17 或更高版本
-
您也可以直接将代码导入到您的 IDE 中:
如果您使用的不是 Linux 机器,那么您需要一个虚拟化服务器。如果您安装了 VirtualBox,其他工具如 Mac 的 boot2docker
可以无缝地为您管理它。访问 VirtualBox 的下载站点 并选择适合您机器的版本,然后下载并安装。不用担心实际运行它。
您还需要 Docker,它只能在 64 位机器上运行。有关在您的机器上设置 Docker 的详细信息,请参阅 https://docs.docker.com/installation/#installation。在继续之前,请验证您是否可以从 shell 运行 docker
命令。如果您使用 boot2docker
,需要 首先 运行它。
从 Spring Initializr 开始
您可以使用这个预先初始化的项目,然后点击生成以下载一个 ZIP 文件。该项目已配置为适合本教程中的示例。
要手动初始化项目:
-
访问 https://start.spring.io。该服务会拉取应用程序所需的所有依赖项,并为您完成大部分设置。
-
选择 Gradle 或 Maven 以及您想要使用的语言。本指南假设您选择了 Java。
-
点击 Dependencies 并选择 Spring Web。
-
点击 Generate。
-
下载生成的 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 组来运行应用程序。然后通过 COPY
命令将项目 JAR 文件复制到容器中,命名为 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 用户身份运行您的应用程序。有关如何更改默认设置的详细信息,请查看 Gradle 或 Maven 的配置指南。
使用 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 之前使用 docker login
确保您已通过身份验证。
推送之后
在这个例子中,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 的大小,以适应容器。
应用程序随后在 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 —— 在本例中是 https://192.168.59.103:8080,即虚拟机的外部 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
:
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
配置文件执行相同的操作:
docker run -e "SPRING_PROFILES_ACTIVE=dev" -p 8080:8080 -t springio/gs-spring-boot-docker
在 Docker 容器中调试应用程序
要调试应用程序,您可以使用 JPDA Transport。我们将容器视为远程服务器。要启用此功能,请在容器运行时在 JAVA_OPTS
变量中传递 Java 代理设置,并将代理的端口映射到本地主机。对于 Docker for Mac,存在一个限制,因为如果不使用 black magic usage,我们无法通过 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
将其映射到主机上的相同端口。
另请参阅
以下指南也可能有所帮助: