SpringBoot 2 与 Docker - 一个简单的 Dockerfile

Spring Boot 应用很容易转换为可执行的 JAR 文件。所有入门指南都这样做,从 Spring Initializr 下载的每个应用都将具有一个插件可执行 JAR 的构建步骤。使用 Maven,我们可以 ./mvnw install,使用 Gradle 可以 ./gradlew build。然后,在项目的顶层,运行该 JAR 的基本 Dockerfile 如下所示:

Dockerfile

FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

JAR_FILE 可以作为 docker 命令的一部分传入(Maven 和 Gradle 会有所不同)。例如,对于 Maven:

$ docker build --build-arg JAR_FILE=target/*.jar -t myorg/myapp .

对于 Gradle:

$ docker build --build-arg JAR_FILE=build/libs/*.jar -t myorg/myapp .

当然,一旦我们选好了构建系统,就不需要 ARG 了 - 我们只需要对 jar 位置进行硬编码即可。例如,对于 Maven:

Dockerfile

FROM openjdk:8-jdk-alpine
VOLUME /tmp
COPY target/*.jar app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

然后我们可以简单地用以下方法构建镜像:

$ docker build -t myorg/myapp .

并像这样运行它:

$ docker run -p 8080:8080 myorg/myapp
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.0.2.RELEASE)

Nov 06, 2018 2:45:16 PM org.springframework.boot.StartupInfoLogger logStarting
INFO: Starting Application v0.1.0 on b8469cdc9b87 with PID 1 (/app.jar started by root in /)
Nov 06, 2018 2:45:16 PM org.springframework.boot.SpringApplication logStartupProfileInfo
...

如果要在镜像内部四处浏览,可以像这样打开其中的 shell(基镜像没有 bash):

$ docker run -ti --entrypoint /bin/sh myorg/myapp
/ # ls
app.jar  dev      home     media    proc     run      srv      tmp      var
bin      etc      lib      mnt      root     sbin     sys      usr
/ #

我们在示例中使用的 alpine 基容器没有 bash,因此这是一个 ash shell。它具有 bash 的某些功能,但不是全部。

如果我们有一个正在运行的容器并且想窥视它,使用 docker exec 可以这么做:

$ docker run --name myapp -ti --entrypoint /bin/sh myorg/myapp
$ docker exec -ti myapp /bin/sh
/ #

其中 myapp 是传递给 docker run 命令的 --name。如果我们不使用 --name,则 docker 会分配一个助记符名称,我们可以从 docker ps 的输出中抓取该助记符。我们还可以使用容器的 sha 标识符代替名称,该名称也可以从 docker ps 中看到。

入口点

使用 Dockerfile ENTRYPOINTexec 形式,这样就没有 shell 封装 Java 进程。优点是 java 进程将响应发送到容器的 KILL 信号。实际上,这意味着,例如,如果我们在本地 docker run 镜像,则可以使用 CTRL-C 停止它。如果命令行太长,我们可以将其提取到 shell 脚本中,然后将其 COPY 到镜像中,然后再运行它。例如:

Dockerfile

FROM openjdk:8-jdk-alpine
VOLUME /tmp
COPY run.sh .
COPY target/*.jar app.jar
ENTRYPOINT ["run.sh"]

请记住使用 exec java … 来启动 Java 进程(以便它可以处理 KILL 信号):

run.sh

#!/bin/sh
exec java -jar /app.jar

入口点另一个有趣的方面是可以在运行时将环境变量注入到 Java 进程中。例如,假设我们想选择在运行时添加 java 命令行选项。我们可以尝试执行以下操作:

Dockerfile

FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","${JAVA_OPTS}","-jar","/app.jar"]

并且

$ docker build -t myorg/myapp .
$ docker run -p 9000:9000 -e JAVA_OPTS=-Dserver.port=9000 myorg/myapp

这将失败,因为 ${} 替换需要使用 shell;exec 形式不使用 shell 来启动该进程,因此不会应用该选项。我们可以通过将入口点移动到脚本(例如上面的 run.sh 示例),或通过在入口点显式创建 shell 来解决这个问题。例如:

Dockerfile

FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"]

然后,我们可以这样来启动应用:

$ docker run -p 8080:8080 -e "JAVA_OPTS=-Ddebug -Xmx128m" myorg/myapp
...
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.2.0.RELEASE)
...
2019-10-29 09:12:12.169 DEBUG 1 --- [           main] ConditionEvaluationReportLoggingListener :


============================
CONDITIONS EVALUATION REPORT
============================
...

(显示 Spring Boot 使用 -Ddebug 生成的完整 DEBUG 输出的一部分。)

ENTRYPOINT 与上述的显式 shell 一起使用意味着我们可以将环境变量传递到 java 命令中,但是到目前为止,我们还不能向 Spring Boot 应用提供命令行参数。该技巧无法在端口 9000 上运行该应用:

$ docker run -p 9000:9000 myorg/myapp --server.port=9000
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.2.0.RELEASE)
...
2019-10-29 09:20:19.718  INFO 1 --- [           main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port(s): 8080

它不起作用的原因是因为 docker 命令(--server.port=9000 部分)传递到了入口点(sh),而不是传递给了它启动的 Java 进程。要解决该问题,我们需要将命令行从 CMD 添加到 ENTRYPOINT

Dockerfile

FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar ${0} ${@}"]
$ docker run -p 9000:9000 myorg/myapp --server.port=9000
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.2.0.RELEASE)
...
2019-10-29 09:30:19.751  INFO 1 --- [           main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port(s): 9000

请注意,将 ${0} 用作 “命令”(在该例中为第一个程序参数),将 ${@} 用作 “命令参数”(其余程序参数)。如果我们使用脚本作为入口点,则不需要 ${0}(在上面的示例中为 /app/run.sh)。例如:

run.sh

#!/bin/sh
exec java ${JAVA_OPTS} -jar /app.jar ${@}

到目前为止,docker 配置非常简单,并且生成的镜像不是很有效。docker 镜像只有一个文件系统層,其中包含胖 jar,我们对应用代码进行的每一次更改都会更改该层,该层可能为 10MB 或更多(对于某些应用甚至可能为 50MB)。我们可以通过将 JAR 分为多个层来改善这一点。

更小的镜像

请注意,上面示例中的基镜像是 openjdk:8-jdk-alpine。alpine 镜像比 Dockerhub 的标准 openjdk 库镜像小。尚无 Java 11 的 alpine 镜像(AdoptOpenJDK 已有一段时间,但不再出现在其 Dockerhub 页面上)。我们还可以使用 “jre” 标签代替 “jdk” 以在基镜像中节省大约 20MB。并不是所有的应用都可以与 JRE 一起使用(而不是 JDK),但是大多数应用都可以,并且确实有些组织强制制定了每个应用必须遵循的规则,因为存在滥用某些 JDK 功能(例如编译)的风险。

可以使我们获得更小的镜像的另一种技巧是使用与 OpenJDK 11 捆绑在一起的 JLink。JLink 允许我们从完整 JDK 中的模块子集构建自定义 JRE 分发,因此,在基镜像中我们不需要 JRE 或 JDK。原则上与使用 openjdk 官方 docker 镜像相比,这将使我们具有较小的总镜像大小。实际上,我们还无法(目前来说)在 JDK 11 上使用 alpine 基镜像,因此我们对基镜像的选择将受到限制,并且最终结果可能会更大。另外,我们自己的基镜像中的自定义 JRE 无法在其他应用之间共享,因为它们将需要不同的自定义。因此,对于所有应用来说,它们的镜像可能较小,但是它们的启动时间仍然较长,因为它们无法从缓存 JRE 层中受益。

最后一点突出了镜像构建者的一个真正重要的关键点:目标不一定总是要构建尽可能小的镜像。通常,较小的镜像是一个好主意,因为它们只需要较少的时间就可以上传和下载,但前提是它们中的所有层都没有被缓存。如今,镜像注册表非常复杂,通过尝试巧妙地构建镜像,我们很容易失去这些功能的优势。如果使用公共基础层,则镜像的总大小将不再是问题,随着注册管理机构和平台的发展,镜像的总大小可能甚至会减少。话虽如此,尝试和优化应用镜像中的各层仍然很重要,也很有用,但是目标始终应该是将变化最快的东西放在最高层,并尽可能多地与其他应用共享大型底层。

发布了228 篇原创文章 · 获赞 13 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/stevenchen1989/article/details/105284512