Spring Boot Docker 应用程序

时间:2022-12-27 20:03:00

Spring Boot Docker 应用程序

许多人使用容器来包装他们的 Spring Boot 应用程序,构建容器并不是一件简单的事情。这是针对 Spring Boot 应用程序开发人员的指南,容器并不总是开发人员的良好抽象。它们迫使您了解和思考低层次的问题。但是,有时可能会要求您创建或使用容器,因此了解构建基块是值得的。在本指南中,我们旨在向您展示在面临需要创建自己的容器的前景时可以做出的一些选择。

我们假设您知道如何创建和构建基本的 Spring 启动应用程序。如果没有,请转到其中一个入门指南— 例如,关于构建休息服务.从那里复制代码,并使用本指南中包含的一些想法进行练习。

还有一个入门指南码头工人,这也是一个很好的起点,但它没有涵盖我们在这里涵盖的选择范围或更详细地涵盖它们。

基本 Dockerfile

Spring Boot 应用程序很容易转换为可执行的 JAR 文件。所有入门指南执行此操作,以及您从中下载的每个应用程序Spring Initializr具有创建可执行 JAR 的构建步骤。有了Maven,你跑了,有了Gradle,你跑了。运行该 JAR 的基本 Dockerfile 在项目的顶层如下所示:​​./mvnw install​​​​./gradlew build​

​Dockerfile​

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

你可以作为命令的一部分传入(Maven和Gradle不同)。对于 Maven,以下命令有效:​​JAR_FILE​​​​docker​

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

对于 Gradle,以下命令有效:

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

选择构建系统后,就不需要 .您可以对 JAR 位置进行硬编码。对于Maven,这将如下所示:​​ARG​

​Dockerfile​

FROM eclipse-temurin:17-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.7.4)

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
/ #

我们在示例中使用的高山基础容器没有 ,所以这是一个外壳。它具有 的部分但不是全部功能。​​bash​​​​ash​​​​bash​

如果你有一个正在运行的容器,并且想要查看它,你可以通过运行:​​docker exec​

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

传递给命令的位置。如果未使用 ,docker 会分配一个助记名,您可以从 的输出中获取该名称。还可以使用容器的 SHA 标识符代替名称。SHA 标识符在输出中也可见。​​myapp​​​​--name​​​​docker run​​​​--name​​​​docker ps​​​​docker ps​

入口点

这执行表格的 Dockerfile 被使用 ,以便没有 shell 包装 Java 进程。优点是 java 进程响应发送到容器的信号。实际上,这意味着(例如)如果您在本地映像,则可以使用 .如果命令行有点长,可以在运行它之前将其提取到 shell 脚本中,然后将其提取到映像中。以下示例演示如何执行此操作:​​ENTRYPOINT​​​​KILL​​​​docker run​​​​CTRL-C​​​​COPY​

​Dockerfile​

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

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

​run.sh​

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

入口点的另一个有趣的方面是是否可以在运行时将环境变量注入 Java 进程。例如,假设您希望在运行时选择添加 Java 命令行选项。您可以尝试这样做:

​Dockerfile​

FROM eclipse-temurin:17-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

此操作失败,因为替换需要外壳。exec 窗体不使用 shell 来启动进程,因此不会应用这些选项。您可以通过将入口点移动到脚本(如前面所示的示例)或在入口点中显式创建 shell 来解决此问题。下面的示例演示如何在入口点中创建 shell:​​${}​​​​run.sh​

​Dockerfile​

FROM eclipse-temurin:17-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.7.4)
...
2019-10-29 09:12:12.169 DEBUG 1 --- [ main] ConditionEvaluationReportLoggingListener :


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

(前面的输出显示了由 Spring 引导生成的完整输出的一部分。​​DEBUG​​​​-Ddebug​

使用 带有显式 shell(如前面的示例所示)意味着您可以将环境变量传递到 Java 命令中。但是,到目前为止,您还不能向 Spring Boot 应用程序提供命令行参数。以下命令不会在端口 9000 上运行应用程序:​​ENTRYPOINT​

docker run -p 9000:9000 myorg/myapp --server.port=9000

该命令生成以下输出,将端口显示为 8080 而不是 9000:

.   ____          _            __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.7.4)
...
2019-10-29 09:20:19.718 INFO 1 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8080

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

​Dockerfile​

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

然后,您可以运行相同的命令并将端口设置为 9000:

$ docker run -p 9000:9000 myorg/myapp --server.port=9000

如以下输出样本所示,端口确实设置为 9000:

.   ____          _            __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.7.4)
...
2019-10-29 09:30:19.751 INFO 1 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 9000

请注意 for “command”(在本例中为第一个程序参数)和 “command参数”(其余程序参数)的用法。如果将脚本用于入口点,则不需要 (在前面的示例中)。以下列表显示了脚本文件中的正确命令:​​${0}​​​​${@}​​​​${0}​​​​/app/run.sh​

​run.sh​

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

到目前为止,docker 配置非常简单,生成的镜像效率不是很高。docker 镜像有一个文件系统层,里面有胖 JAR,我们对应用程序代码所做的每次更改都会更改该层,该层可能为 10MB 或更多(对于某些应用程序甚至高达 50MB)。我们可以通过将 JAR 拆分为多个层来改进这一点。

较小的图像

请注意,前面示例中的基本映像是 。图像小于来自的标准库图像​​eclipse-temurin:17-jdk-alpine​​​​alpine​​​​eclipse-temurin​​Dockerhub.您还可以使用标签而不是 在基本映像中节省大约 20MB 的内存。并非所有应用程序都使用 JRE(而不是 JDK),但大多数应用程序都可以。一些组织强制实施一条规则,即每个应用程序都必须使用 JRE,因为存在滥用某些 JDK 功能(如编译)的风险。​​jre​​​​jdk​

另一个可以让你获得较小图像的技巧是使用金林克,与 OpenJDK 11 及更高版本捆绑在一起。JLink 允许您从完整 JDK 中的模块子集构建自定义 JRE 发行版,因此您不需要在基本映像中使用 JRE 或 JDK。原则上,这将使您获得比使用官方 docker 映像更小的总映像大小。实际上,您自己的基础映像中的自定义 JRE 不能在其他应用程序之间共享,因为它们需要不同的自定义。因此,您可能为所有应用程序提供了较小的映像,但它们仍然需要更长的时间才能启动,因为它们不会从缓存 JRE 层中受益。

最后一点突出了映像构建者的一个非常重要的问题:目标不一定总是构建尽可能小的映像。较小的图像通常是一个好主意,因为它们需要更少的时间来上传和下载,但前提是其中的图层都尚未缓存。如今,映像注册表非常复杂,通过尝试巧妙地构建映像,您很容易失去这些功能的好处。如果使用通用基础层,则映像的总大小就不那么重要了,并且随着注册表和平台的发展,它可能会变得不那么重要。话虽如此,尝试优化应用程序映像中的层仍然很重要且有用。但是,目标应始终是将变化最快的内容放在最高层中,并与其他应用程序共享尽可能多的大型较低层。

更好的 Dockerfile

Spring Boot 胖 JAR 自然具有“层”,因为 JAR 本身的打包方式。如果我们先解压缩它,它已经分为外部和内部依赖。要在 docker 构建中一步完成此操作,我们需要先解压缩 JAR。以下命令(坚持使用Maven,但Gradle版本非常相似)解压缩Spring Boot胖JAR:

mkdir target/dependency
(cd target/dependency; jar -xf ../*.jar)
docker build -t myorg/myapp .

然后我们可以使用以下​​Dockerfile​

​Dockerfile​

FROM eclipse-temurin:17-jdk-alpine
VOLUME /tmp
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"]

现在有三层,所有应用程序资源都在后面的两层中。如果应用程序依赖关系没有改变,则第一层(from )不需要改变,因此构建速度更快,并且只要基础层已经缓存,容器在运行时的启动也会更快。​​BOOT-INF/lib​

我们使用了硬编码的主应用程序类:。对于您的应用程序,这可能有所不同。如果需要,您可以使用另一个参数化它。您还可以将 Spring Boot 脂肪复制到映像中并使用它来运行应用程序。它可以工作,你不需要指定主类,但它在启动时会慢一点。​​hello.Application​​​​ARG​​​​JarLauncher​

弹簧引导层索引

从 Spring Boot 2.3.0 开始,使用 Spring Boot Maven 或 Gradle 插件构建的 JAR 文件包括图层信息在 JAR 文件中。此层信息根据应用程序内部版本之间更改的可能性来分隔应用程序的各个部分。这可用于使 Docker 映像层更加高效。

层信息可用于将 JAR 内容提取到每个层的目录中:

mkdir target/extracted
java -Djarmode=layertools -jar target/*.jar extract --destination target/extracted
docker build -t myorg/myapp .

然后我们可以使用以下:​​Dockerfile​

​Dockerfile​

FROM eclipse-temurin:17-jdk-alpine
VOLUME /tmp
ARG EXTRACTED=/workspace/app/target/extracted
COPY ${EXTRACTED}/dependencies/ ./
COPY ${EXTRACTED}/spring-boot-loader/ ./
COPY ${EXTRACTED}/snapshot-dependencies/ ./
COPY ${EXTRACTED}/application/ ./
ENTRYPOINT ["java","org.springframework.boot.loader.JarLauncher"]

Spring Boot 脂肪从 JAR 提取到映像中,因此可用于启动应用程序,而无需对主应用程序类进行硬编码。​​JarLauncher​

请参阅的Spring 引导文档有关使用分层功能的详细信息。

调整

如果你想尽快启动你的应用程序(大多数人都这样做),你可以考虑一些调整:

  • 使用spring-context-indexer (链接到文档).对于小型应用程序,它不会增加太多,但每一点都有帮助。
  • 请勿使用驱动器如果你能负担得起的话。
  • 使用最新版本的 Spring Boot 和 Spring。
  • 修复位置Spring Boot 配置文件with(通过命令行参数、系统属性或其他方法)。spring.config.location

您的应用程序在运行时可能不需要完整的 CPU,但它确实需要多个 CPU 才能尽快启动(至少两个,四个更好)。如果您不介意启动速度较慢,则可以将 CPU 限制在 4 以下。如果您*从少于四个 CPU 开始,则设置 可能会有所帮助,因为它会阻止 Spring 引导创建它可能无法使用的新线程(这适用于 Spring 引导 2.1.0 及更高版本)。​​-Dspring.backgroundpreinitializer.ignore=true​

多阶段构建

如​​Dockerfile​​更好的 Dockerfile假设胖 JAR 已经在命令行上构建。您还可以在 docker 中使用多阶段构建并将结果从一个映像复制到另一个映像来执行此步骤。以下示例使用 Maven 执行此操作:

​Dockerfile​

FROM eclipse-temurin:17-jdk-alpine as build
WORKDIR /workspace/app

COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .
COPY src src

RUN ./mvnw install -DskipTests
RUN mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar)

FROM eclipse-temurin:17-jdk-alpine
VOLUME /tmp
ARG DEPENDENCY=/workspace/app/target/dependency
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]

第一个图像被标记,它用于运行Maven,构建胖JAR,并解压缩它。解包也可以由 Maven 或 Gradle 完成(这是入门指南中采用的方法)。没有太大区别,除了必须编辑构建配置并添加插件。​​build​

请注意,源代码已拆分为四层。后面的层包含应用程序的构建配置和源代码,前面的层包含构建系统本身(Maven 包装器)。这是一个小的优化,这也意味着我们不必将目录复制到 docker 镜像,即使是用于构建的临时镜像。​​target​

源代码更改的每个构建都很慢,因为必须在第一部分中重新创建Maven缓存。但是你有一个完全独立的构建,只要他们有 docker,任何人都可以运行它来运行你的应用程序。这在某些环境中非常有用 - 例如,您需要与不了解Java的人共享代码。​​RUN​

实验性功能

Docker 18.06 附带一些“实验性”功能,包括缓存生成依赖项的方法。要打开它们,您需要在守护程序 () 中有一个标志,并在运行客户端时提供一个环境变量。然后,您可以在以下位置添加“魔术”第一行:​​dockerd​​​​Dockerfile​

​Dockerfile​

# syntax=docker/dockerfile:experimental

然后,该指令接受一个新标志:。下面的清单显示了一个完整的示例:​​RUN​​​​--mount​

​Dockerfile​

# syntax=docker/dockerfile:experimental
FROM eclipse-temurin:17-jdk-alpine as build
WORKDIR /workspace/app

COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .
COPY src src

RUN --mount=type=cache,target=/root/.m2 ./mvnw install -DskipTests
RUN mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar)

FROM eclipse-temurin:17-jdk-alpine
VOLUME /tmp
ARG DEPENDENCY=/workspace/app/target/dependency
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]

然后你可以运行它:

DOCKER_BUILDKIT=1 docker build -t myorg/myapp .

以下清单显示了示例输出:

...
=> /bin/sh -c ./mvnw install -DskipTests 5.7s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:3defa...
=> => naming to docker.io/myorg/myapp

使用实验性功能,您可以在控制台上获得不同的输出,但您可以看到,如果缓存是预热的,Maven 构建现在只需要几秒钟而不是几分钟。

此配置的 Gradle 版本非常相似:​​Dockerfile​

​Dockerfile​

# syntax=docker/dockerfile:experimental
FROM eclipse-temurin:17-jdk-alpine AS build
WORKDIR /workspace/app

COPY . /workspace/app
RUN --mount=type=cache,target=/root/.gradle ./gradlew clean build
RUN mkdir -p build/dependency && (cd build/dependency; jar -xf ../libs/*.jar)

FROM eclipse-temurin:17-jdk-alpine
VOLUME /tmp
ARG DEPENDENCY=/workspace/app/build/dependency
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]

虽然这些功能处于实验阶段,但用于打开和关闭构建工具包的选项取决于您使用的版本。检查您拥有的版本的文档(前面显示的示例对于 18.0.6 是正确的)。​​docker​​​​docker​

安全方面

与经典 VM 部署一样,不应使用根权限运行进程。相反,映像应包含运行应用程序的非 root 用户。

在 中,您可以通过添加另一个添加(系统)用户和组的层并将其设置为当前用户(而不是默认的 root)来实现此目的:​​Dockerfile​

​Dockerfile​

FROM eclipse-temurin:17-jdk-alpine

RUN addgroup -S demo && adduser -S demo -G demo
USER demo

...

如果有人设法突破您的应用程序并在容器内运行系统命令,此预防措施会限制其功能(遵循最小特权原则)。

一些进一步的命令只能以 root 身份工作,因此您可能必须将 USER 命令进一步向下移动(例如,如果您计划在容器中安装更多包,则只能以 root 身份工作)。​​Dockerfile​

对于其他方法,不使用 可能更合适。例如,在后面描述的构建包方法中,大多数实现默认使用非 root 用户。​​Dockerfile​

另一个注意事项是,大多数应用程序在运行时可能不需要完整的 JDK,因此一旦我们有了多阶段构建,我们就可以安全地切换到 JRE 基础映像。因此,在前面显示的多阶段构建中,我们可以用于最终的、可运行的映像:

​Dockerfile​

FROM eclipse-temurin:17-jre-alpine

...

如前所述,这也节省了映像中的一些空间,这些空间将被运行时不需要的工具占用。

构建插件

如果您不想直接在构建中调用,那么Maven和Gradle有一组丰富的插件可以为您完成这项工作。这里只是其中的几个。​​docker​

Spring Boot Maven 和 Gradle 插件

您可以使用 Spring Boot 构建插件马文和格拉德尔以创建容器映像。插件使用 创建 OCI 映像(与 创建的格式相同)​​docker build​​云原生构建包.您不需要 ,但您确实需要一个 Docker 守护程序,无论是在本地(这是您在使用 docker 构建时使用的守护程序)还是通过环境变量远程。默认构建器针对 Spring 引导应用程序进行了优化,并且映像如上例所示有效地分层。​​Dockerfile​​​​DOCKER_HOST​

以下示例在不更改文件的情况下使用 Maven:​​pom.xml​

./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=myorg/myapp

以下示例适用于 Gradle,无需更改文件:​​build.gradle​

./gradlew bootBuildImage --imageName=myorg/myapp

第一次构建可能需要很长时间,因为它必须下载一些容器映像和 JDK,但后续构建应该很快。

然后,您可以运行映像,如以下清单所示(带输出):

docker run -p 8080:8080 -t myorg/myapp
Setting Active Processor Count to 6
Calculating JVM memory based on 14673596K available memory
Calculated JVM Memory Configuration: -XX:MaxDirectMemorySize=10M -Xmx14278122K -XX:MaxMetaspaceSize=88273K -XX:ReservedCodeCacheSize=240M -Xss1M (Total Memory: 14673596K, Thread Count: 50, Loaded Class Count: 13171, Headroom: 0%)
Adding 129 container CA certificates to JVM truststore
Spring Cloud Bindings Enabled
Picked up JAVA_TOOL_OPTIONS: -Djava.security.properties=/layers/paketo-buildpacks_bellsoft-liberica/java-security-properties/java-security.properties -agentpath:/layers/paketo-buildpacks_bellsoft-liberica/jvmkill/jvmkill-1.16.0-RELEASE.so=printHeapHistogram=1 -XX:ActiveProcessorCount=6 -XX:MaxDirectMemorySize=10M -Xmx14278122K -XX:MaxMetaspaceSize=88273K -XX:ReservedCodeCacheSize=240M -Xss1M -Dorg.springframework.cloud.bindings.boot.enable=true
....
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

您可以看到应用程序正常启动。您可能还会注意到,JVM 内存需求已计算并设置为容器内的命令行选项。这与多年来在Cloud Foundry构建包中使用的内存计算相同。它代表了对一系列JVM应用程序(包括但不限于Spring Boot应用程序)的最佳选择的重要研究,其结果通常比JVM的默认设置要好得多。您可以自定义命令行选项并通过设置环境变量来覆盖内存计算器,如Paketo 构建包文档.

Spotify Maven 插件

这Spotify Maven 插件是一个受欢迎的选择。它要求您编写一个然后为您运行,就像您在命令行上执行此操作一样。有一些用于 docker 镜像标记和其他东西的配置选项,但它使应用程序中的 docker 知识集中在 中,许多人都喜欢。​​Dockerfile​​​​docker​​​​Dockerfile​

对于真正基本的用法,它将开箱即用,无需额外的配置:

mvn com.spotify:dockerfile-maven-plugin:build
...
[INFO] Building Docker context /home/dsyer/dev/demo/workspace/myapp
[INFO]
[INFO] Image will be built without a name
[INFO]
...
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 7.630 s
[INFO] Finished at: 2018-11-06T16:03:16+00:00
[INFO] Final Memory: 26M/595M
[INFO] ------------------------------------------------------------------------

这将构建一个匿名 docker 映像。我们现在可以在命令行上标记它,也可以使用 Maven 配置将其设置为 .以下示例无需更改文件即可工作:​​docker​​​​repository​​​​pom.xml​

$ mvn com.spotify:dockerfile-maven-plugin:build -Ddockerfile.repository=myorg/myapp

或者,您可以更改文件:​​pom.xml​

​pom.xml​

<build>
<plugins>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>dockerfile-maven-plugin</artifactId>
<version>1.4.8</version>
<configuration>
<repository>myorg/${project.artifactId}</repository>
</configuration>
</plugin>
</plugins>
</build>

Palantir Gradle 插件

这Palantir Gradle 插件与 a 一起工作,也可以为您生成 a。然后它就像在命令行上运行它一样运行。​​Dockerfile​​​​Dockerfile​​​​docker​

首先,您需要将插件导入到:​​build.gradle​

​build.gradle​

buildscript {
...
dependencies {
...
classpath('gradle.plugin.com.palantir.gradle.docker:gradle-docker:0.13.0')
}
}

然后,最后,您可以应用插件并调用其任务:

​build.gradle​

apply plugin: 'com.palantir.docker'

group = 'myorg'

bootJar {
baseName = 'myapp'
version = '0.1.0'
}

task unpack(type: Copy) {
dependsOn bootJar
from(zipTree(tasks.bootJar.outputs.files.singleFile))
into("build/dependency")
}
docker {
name "${project.group}/${bootJar.baseName}"
copySpec.from(tasks.unpack.outputs).into("dependency")
buildArgs(['DEPENDENCY': "dependency"])
}

在这个例子中,我们选择在目录中的特定位置解压缩 Spring Boot 胖 JAR,这是 docker 构建的根目录。然后前面显示的多层(不是多阶段)起作用。​​build​​​​Dockerfile​

Jib Maven 和 Gradle 插件

谷歌有一个名为​​臂​​这是相对较新的,但由于多种原因,这很有趣。可能最有趣的是你不需要docker来运行它。Jib 使用与获取的相同标准输出构建映像,但除非你要求,否则不会使用,因此它可以在未安装 docker 的环境中工作(在构建服务器中很常见)。您也不需要(无论如何都会被忽略)或任何东西来获取在Maven中构建的映像(Gradle要求您至少在中安装插件)。​​docker build​​​​docker​​​​Dockerfile​​​​pom.xml​​​​build.gradle​

Jib 的另一个有趣功能是它对图层固执己见,并且它以与上面创建的多层略有不同的方式优化它们。与胖 JAR 一样,Jib 将本地应用程序资源与依赖项分开,但它更进一步,还将快照依赖项放入单独的层中,因为它们更有可能更改。有用于进一步自定义布局的配置选项。​​Dockerfile​

以下示例适用于 Maven,而无需更改 :​​pom.xml​

$ mvn com.google.cloud.tools:jib-maven-plugin:build -Dimage=myorg/myapp

要运行该命令,您需要具有在存储库前缀下推送到 Dockerhub 的权限。如果您已在命令行上进行身份验证,则从您的本地配置开始工作。您还可以在您的(存储库的)中设置Maven“服务器”身份验证:​​myorg​​​​docker​​​​~/.docker​​​​~/.m2/settings.xml​​​​id​

​settings.xml​

<server>
<id>registry.hub.docker.com</id>
<username>myorg</username>
<password>...</password>
</server>

还有其他选项 — 例如,您可以针对 docker 守护程序在本地构建(例如在命令行上运行),使用 goal 而不是 。还支持其他容器注册表。对于每一个,您需要通过 Docker 或 Maven 设置设置本地身份验证。​​docker​​​​dockerBuild​​​​build​

gradle 插件具有类似的功能,一旦您将其放入 :.​​build.gradle​

​build.gradle​

plugins {
...
id 'com.google.cloud.tools.jib' version '1.8.0'
}

然后,您可以通过运行以下命令来构建映像:

./gradlew jib --image=myorg/myapp

与 Maven 构建一样,如果您在命令行上进行身份验证,则映像推送将从您的本地配置进行身份验证。​​docker​​​​~/.docker​

持续集成

如今,自动化(或应该是)是每个应用程序生命周期的一部分。人们用来执行自动化的工具往往非常擅长从源代码调用构建系统。因此,如果这为您提供了 docker 映像,并且构建代理中的环境与开发人员自己的环境充分一致,那么这可能就足够了。对 docker 注册表进行身份验证可能是最大的挑战,但所有自动化工具中都有一些功能可以帮助解决这个问题。

但是,有时最好将容器创建完全留给自动化层,在这种情况下,用户的代码可能不需要被污染。容器创建很棘手,开发人员有时不需要真正关心它。如果用户代码更干净,则其他工具更有可能“做正确的事”(应用安全修复、优化缓存等)。自动化有多种选择,如今它们都具有一些与容器相关的功能。我们要看看一对夫妇。

大堂

大堂是一个基于管道的自动化平台,可用于 CI 和 CD。它在VMware内部使用,项目的主要作者在那里工作。大厅中的所有内容都是无状态的,并在容器中运行,但 CLI 除外。由于运行容器是自动化管道的主要业务顺序,因此可以很好地支持创建容器。这码头工人镜像资源负责使生成的输出状态保持最新(如果它是容器映像)。

以下示例管道为前面显示的示例生成一个 docker 映像,假设它位于 的 github 中,在根目录中有一个生成任务声明:​​myorg/myapp​​​​Dockerfile​​​​src/main/ci/build.yml​

resources:
- name: myapp
type: git
source:
uri: https://github.com/myorg/myapp.git
- name: myapp-image
type: docker-image
source:
email: {{docker-hub-email}}
username: {{docker-hub-username}}
password: {{docker-hub-password}}
repository: myorg/myapp

jobs:
- name: main
plan:
- task: build
file: myapp/src/main/ci/build.yml
- put: myapp-image
params:
build: myapp

管道的结构非常具有声明性:定义“资源”(输入、输出或两者)和“作业”(使用操作并将其应用于资源)。如果任何输入资源发生更改,则会触发新的生成。如果作业期间有任何输出资源发生更改,则会对其进行更新。

可以在与应用程序源代码不同的位置定义管道。此外,对于通用生成设置,任务声明也可以集中或外部化。这允许在开发和自动化之间分离一些关注点,这适合某些软件开发组织。

詹金斯

詹金斯是另一个流行的自动化服务器。它具有广泛的功能,但最接近此处其他自动化示例的功能是管道特征。下面使用 Maven 构建一个 Spring 引导项目,然后使用 构建映像并将其推送到存储库:​​Jenkinsfile​​​​Dockerfile​

​Jenkinsfile​

node {
checkout scm
sh './mvnw -B -DskipTests clean package'
docker.build("myorg/myapp").push()
}

对于需要在生成服务器中进行身份验证的(实际)docker 存储库,可以使用 向对象添加凭据。​​docker​​​​docker.withCredentials(…)​

构建包

Spring Boot Maven 和 Gradle 插件使用构建包的方式与以下示例中的 CLI 完全相同。给定相同的输入,生成的图像是相同的。​​pack​

云铸造厂多年来一直在内部使用容器,用于将用户代码转换为容器的部分技术是Build Packs,这个想法最初是从希罗库.当前一代的构建包 (v2) 生成通用二进制输出,这些输出由平台组装到容器中。这新一代构建包(v3)是Heroku和其他公司(包括VMware)之间的合作,它直接和显式地构建容器映像。这对开发人员和运营商来说很有趣。开发人员不需要太关心如何构建容器的细节,但如果需要,他们可以轻松创建一个容器。构建包还具有许多用于缓存构建结果和依赖项的功能。通常,构建包的运行速度比本机 Docker 构建快得多。操作员可以扫描容器以审核其内容并对其进行转换以修补它们以进行安全更新。此外,您可以在本地(例如,在开发人员计算机上或在 CI 服务中)或在 Cloud Foundry 等平台中运行构建包。

构建包生命周期的输出是容器映像,但您不需要 .输出映像中的文件系统层由构建包控制。通常,许多优化是在开发人员不必了解或关心它们的情况下进行的。还有一个​​Dockerfile​​应用程序二进制接口在较低层(例如包含操作系统的基本映像)和上层(包含中间件和特定于语言的依赖项)之间。这使得平台(如Cloud Foundry)可以在存在安全更新的情况下修补较低层,而不会影响应用程序的完整性和功能。

为了让您了解构建包的功能,以下示例(与其输出一起显示)使用打包命令行界面从命令行(它可以与我们在本指南中使用的示例应用程序一起使用 - 不需要或任何特殊的构建配置):​​Dockerfile​

pack build myorg/myapp --builder=paketobuildpacks/builder:base --path=.
base: Pulling from paketobuildpacks/builder
Digest: sha256:4fae5e2abab118ca9a37bf94ab42aa17fef7c306296b0364f5a0e176702ab5cb
Status: Image is up to date for paketobuildpacks/builder:base
base-cnb: Pulling from paketobuildpacks/run
Digest: sha256:a285e73bc3697bc58c228b22938bc81e9b11700e087fd9d44da5f42f14861812
Status: Image is up to date for paketobuildpacks/run:base-cnb
===> DETECTING
7 of 18 buildpacks participating
paketo-buildpacks/ca-certificates 2.3.2
paketo-buildpacks/bellsoft-liberica 8.2.0
paketo-buildpacks/maven 5.3.2
paketo-buildpacks/executable-jar 5.1.2
paketo-buildpacks/apache-tomcat 5.6.1
paketo-buildpacks/dist-zip 4.1.2
paketo-buildpacks/spring-boot 4.4.2
===> ANALYZING
Previous image with name "myorg/myapp" not found
===> RESTORING
===> BUILDING

Paketo CA Certificates Buildpack 2.3.2
https://github.com/paketo-buildpacks/ca-certificates
Launch Helper: Contributing to layer
Creating /layers/paketo-buildpacks_ca-certificates/helper/exec.d/ca-certificates-helper

Paketo BellSoft Liberica Buildpack 8.2.0
https://github.com/paketo-buildpacks/bellsoft-liberica
Build Configuration:
$BP_JVM_VERSION 11 the Java version
Launch Configuration:
$BPL_JVM_HEAD_ROOM 0 the headroom in memory calculation
$BPL_JVM_LOADED_CLASS_COUNT 35% of classes the number of loaded classes in memory calculation
$BPL_JVM_THREAD_COUNT 250 the number of threads in memory calculation
$JAVA_TOOL_OPTIONS the JVM launch flags
BellSoft Liberica JDK 11.0.12: Contributing to layer
Downloading from https://github.com/bell-sw/Liberica/releases/download/11.0.12+7/bellsoft-jdk11.0.12+7-linux-amd64.tar.gz
Verifying checksum
Expanding to /layers/paketo-buildpacks_bellsoft-liberica/jdk
Adding 129 container CA certificates to JVM truststore
Writing env.build/JAVA_HOME.override
Writing env.build/JDK_HOME.override
BellSoft Liberica JRE 11.0.12: Contributing to layer
Downloading from https://github.com/bell-sw/Liberica/releases/download/11.0.12+7/bellsoft-jre11.0.12+7-linux-amd64.tar.gz
Verifying checksum
Expanding to /layers/paketo-buildpacks_bellsoft-liberica/jre
Adding 129 container CA certificates to JVM truststore
Writing env.launch/BPI_APPLICATION_PATH.default
Writing env.launch/BPI_JVM_CACERTS.default
Writing env.launch/BPI_JVM_CLASS_COUNT.default
Writing env.launch/BPI_JVM_SECURITY_PROVIDERS.default
Writing env.launch/JAVA_HOME.default
Writing env.launch/MALLOC_ARENA_MAX.default
Launch Helper: Contributing to layer
Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/active-processor-count
Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/java-opts
Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/link-local-dns
Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/memory-calculator
Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/openssl-certificate-loader
Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/security-providers-configurer
Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/security-providers-classpath-9
JVMKill Agent 1.16.0: Contributing to layer
Downloading from https://github.com/cloudfoundry/jvmkill/releases/download/v1.16.0.RELEASE/jvmkill-1.16.0-RELEASE.so
Verifying checksum
Copying to /layers/paketo-buildpacks_bellsoft-liberica/jvmkill
Writing env.launch/JAVA_TOOL_OPTIONS.append
Writing env.launch/JAVA_TOOL_OPTIONS.delim
Java Security Properties: Contributing to layer
Writing env.launch/JAVA_SECURITY_PROPERTIES.default
Writing env.launch/JAVA_TOOL_OPTIONS.append
Writing env.launch/JAVA_TOOL_OPTIONS.delim

Paketo Maven Buildpack 5.3.2
https://github.com/paketo-buildpacks/maven
Build Configuration:
$BP_MAVEN_BUILD_ARGUMENTS -Dmaven.test.skip=true package the arguments to pass to Maven
$BP_MAVEN_BUILT_ARTIFACT target/*.[jw]ar the built application artifact explicitly. Supersedes $BP_MAVEN_BUILT_MODULE
$BP_MAVEN_BUILT_MODULE the module to find application artifact in
Creating cache directory /home/cnb/.m2
Compiled Application: Contributing to layer
Executing mvnw --batch-mode -Dmaven.test.skip=true package

[ ... Maven build output ... ]

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 53.474 s
[INFO] Finished at: 2021-07-23T20:10:28Z
[INFO] ------------------------------------------------------------------------
Removing source code

Paketo Executable JAR Buildpack 5.1.2
https://github.com/paketo-buildpacks/executable-jar
Class Path: Contributing to layer
Writing env/CLASSPATH.delim
Writing env/CLASSPATH.prepend
Process types:
executable-jar: java org.springframework.boot.loader.JarLauncher (direct)
task: java org.springframework.boot.loader.JarLauncher (direct)
web: java org.springframework.boot.loader.JarLauncher (direct)

Paketo Spring Boot Buildpack 4.4.2
https://github.com/paketo-buildpacks/spring-boot
Creating slices from layers index
dependencies
spring-boot-loader
snapshot-dependencies
application
Launch Helper: Contributing to layer
Creating /layers/paketo-buildpacks_spring-boot/helper/exec.d/spring-cloud-bindings
Spring Cloud Bindings 1.7.1: Contributing to layer
Downloading from https://repo.spring.io/release/org/springframework/cloud/spring-cloud-bindings/1.7.1/spring-cloud-bindings-1.7.1.jar
Verifying checksum
Copying to /layers/paketo-buildpacks_spring-boot/spring-cloud-bindings
Web Application Type: Contributing to layer
Reactive web application detected
Writing env.launch/BPL_JVM_THREAD_COUNT.default
4 application slices
Image labels:
org.opencontainers.image.title
org.opencontainers.image.version
org.springframework.boot.version
===> EXPORTING
Adding layer 'paketo-buildpacks/ca-certificates:helper'
Adding layer 'paketo-buildpacks/bellsoft-liberica:helper'
Adding layer 'paketo-buildpacks/bellsoft-liberica:java-security-properties'
Adding layer 'paketo-buildpacks/bellsoft-liberica:jre'
Adding layer 'paketo-buildpacks/bellsoft-liberica:jvmkill'
Adding layer 'paketo-buildpacks/executable-jar:classpath'
Adding layer 'paketo-buildpacks/spring-boot:helper'
Adding layer 'paketo-buildpacks/spring-boot:spring-cloud-bindings'
Adding layer 'paketo-buildpacks/spring-boot:web-application-type'
Adding 5/5 app layer(s)
Adding layer 'launcher'
Adding layer 'config'
Adding layer 'process-types'
Adding label 'io.buildpacks.lifecycle.metadata'
Adding label 'io.buildpacks.build.metadata'
Adding label 'io.buildpacks.project.metadata'
Adding label 'org.opencontainers.image.title'
Adding label 'org.opencontainers.image.version'
Adding label 'org.springframework.boot.version'
Setting default process type 'web'
Saving myorg/myapp...
*** Images (ed1f92885df0):
myorg/myapp
Adding cache layer 'paketo-buildpacks/bellsoft-liberica:jdk'
Adding cache layer 'paketo-buildpacks/maven:application'
Adding cache layer 'paketo-buildpacks/maven:cache'
Successfully built image 'myorg/myapp'

这是运行构建包生命周期的 Docker 映像。通常,它将是所有开发人员或单个平台上所有开发人员的共享资源。您可以在命令行上设置默认生成器(在 中创建文件),然后在后续构建中省略该标志。​​--builder​​​​~/.pack​

构建器还知道如何从可执行的 JAR 文件构建映像,因此您可以先使用 Maven 进行构建,然后将 指向 JAR 文件以获得相同的结果。​​paketobuildpacks/builder:base​​​​--path​

科特

容器和平台空间的另一个新项目是科特.如果您不熟悉它,可以将其视为构建无服务器平台的构建块。它建立在Kubernetes,因此,最终,它使用容器映像并将其转换为平台上的应用程序或“服务”。不过,它的主要功能之一是能够使用源代码并为您构建容器,使其对开发人员和操作员更加友好。原生构建是执行此操作的组件,它本身就是一个灵活的平台,用于将用户代码转换为容器 - 您几乎可以用任何您喜欢的方式执行此操作。一些模板提供了通用模式(例如 aS Maven 和 Gradle 构建)和多阶段 docker 构建,使用卡尼科.还有一个模板使用构建包,这对我们来说很有趣,因为构建包一直对 Spring Boot 有很好的支持。

关闭

本指南提供了许多用于为 Spring 引导应用程序构建容器映像的选项。所有这些都是完全有效的选择,现在由您决定需要哪一个。第一个问题应该是“我真的需要构建容器映像吗?如果答案是“是”,那么您的选择很可能是由效率、可缓存性和关注点分离驱动的。是否希望使开发人员不必过多了解容器映像的创建方式?您是否希望开发人员在需要修补操作系统和中间件漏洞时负责更新映像?或者,开发人员可能需要完全控制整个过程,并且他们拥有所需的所有工具和知识。