ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
# 春季启动Docker 许多人正在使用容器包装他们的Spring Boot应用程序,而构建容器并不是一件容易的事。 这是Spring Boot应用程序开发人员的指南,容器对于开发人员而言并不总是一个很好的抽象-它们迫使您学习和思考非常低级的问题-但是有时您会被要求创建或使用容器,因此有必要了解这些基本要素。 在这里,我们旨在向您展示一些您需要创建自己的容器时可以做出的选择。 我们将假定您知道如何创建和构建基本的Spring Boot应用程序。 如果不这样做,请转到 [入门指南之一](https://spring.io/guides) ,例如有关构建 [REST服务的指南](https://spring.io/guides/gs/rest-service/) 。 从那里复制代码,并使用以下一些想法进行练习。 在 上也有一个入门指南 Docker ,这也是一个很好的起点,但是它没有涵盖我们在此处所进行的选择的范围,也没有详细介绍。 ## 基本的Dockerfile Spring Boot应用程序很容易转换为可执行的JAR文件。 所有的 [入门指南](https://spring.io/guides) 都这样做,从 下载的每个应用程序 [Spring Initializr](https://start.spring.io) 都将具有一个创建可执行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`\-您可以对罐子的位置进行硬编码。 例如,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 ... ~~~ 如果要在图像内部四处浏览,可以像这样打开其中的外壳(基本图像没有 `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 / # ~~~ 在哪里 `myapp` 是个 `--name` 传递给 `docker run`命令。 如果你不使用 `--name` 然后docker分配一个助记符名称,您可以从该命令的输出中抓取该名称 `docker ps`。 您也可以使用容器的SHA标识符代替名称,该名称也可以从以下位置看到 `docker ps`. ### 入口点 的 [exec形式](https://docs.docker.com/engine/reference/builder/#exec-form-entrypoint-example) Dockerfile `ENTRYPOINT`用来避免外壳程序包装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 ~~~ 这将失败,因为 `${}`替换需要外壳; exec表单不使用外壳程序来启动进程,因此不会应用选项。 您可以通过将入口点移至脚本(例如 `run.sh`上面的示例),或者通过在入口点显式创建外壳。 例如: `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 ============================ ... ~~~ (显示完整的部分 `DEBUG` 用生成的输出 `-Ddebug` 由Spring Boot提供。) 使用 `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映像只有一个文件系统层,其中包含胖子罐,我们对应用程序代码所做的每次更改都会更改该层,该层可能为10MB或更大(对于某些应用程序甚至为50MB)。 我们可以通过将JAR分为多个层来改善这一点。 ### 较小的图像 请注意,上面示例中的基本图像是 `openjdk:8-jdk-alpine`。 这 `alpine` 图像小于标准 `openjdk`来自 库映像 [Dockerhub的](https://hub.docker.com/_/openjdk/) 。 尚无Java 11的正式高山图像(AdoptOpenJDK已有一段时间,但不再出现在其 [Dockerhub页面上](https://hub.docker.com/r/adoptopenjdk/openjdk11/) )。 您还可以通过使用“ jre”标签而不是“ jdk”在基本映像中节省大约20MB。 并非所有的应用程序都可以与JRE一起使用(而不是JDK),但是大多数的应用程序都可以,并且确实有些组织会强制执行每个应用程序必须遵循的规则,因为存在滥用某些JDK功能(例如编译)的风险。 可以使您缩小图像的另一个技巧是使用 [JLink](https://openjdk.java.net/projects/jigsaw/quick-start#linker) ,它与OpenJDK 11捆绑在一起。JLink允许您从完整JDK中的模块子集构建自定义JRE分发,因此您不需要JRE或JDK在基本图像中。 原则上,与使用 `openjdk`官方docker映像。 实际上,您还无法使用 `alpine`带有JDK 11的基本映像,因此您对基本映像的选择将受到限制,并且可能会导致最终映像的尺寸更大。 另外,您自己的基本映像中的自定义JRE无法在其他应用程序之间共享,因为它们将需要不同的自定义。 因此,对于所有应用程序来说,它们可能都有较小的映像,但是它们仍需要更长的启动时间,因为它们无法从缓存JRE层中受益。 最后一点突出了图像构建者的一个真正重要的关注点:目标不一定总是要构建尽可能小的图像。 较小的图像通常是一个好主意,因为它们只需要花费较少的时间就可以上传和下载,但是前提是它们中的所有层都没有被缓存。 如今,图像注册表非常复杂,通过尝试巧妙地构建图像,您很容易失去这些功能的优势。 如果使用公共基础层,则图像的总大小将不再是问题,随着注册管理机构和平台的发展,图像的总大小可能甚至会减少。 话虽如此,尝试和优化我们的应用程序映像中的各层仍然是重要且有用的,但是目标始终应该是将变化最快的东西放置在最高层中,并共享尽可能多的较大,较低的层其他应用程序中尽可能多的层。 ## 更好的Dockerfile 由于罐子本身的包装方式,Spring Boot胖子罐子自然具有“层”。 如果我们先拆包,它将已经分为内部和外部依赖关系。 要在Docker构建中一步一步做到这一点,我们需要先打开jar的包装。 例如(坚持使用Maven,但Gradle版本非常相似): ~~~ $ mkdir target/dependency $ (cd target/dependency; jar -xf ../*.jar) $ docker build -t myorg/myapp . ~~~ 有了这个 `Dockerfile` `Dockerfile` ~~~ FROM openjdk:8-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"] ~~~ 现在共有3层,所有应用程序资源都位于后面的2层中。 如果应用程序依存关系不变,则第一层(来自 `BOOT-INF/lib`)不会更改,因此构建会更快,并且只要基本层已被缓存,容器在运行时的启动也会随之改变。 我们使用了硬编码的主应用程序类 hello.Application。 对于您的应用程序,这可能会有所不同。 您可以用另一个参数化它 ARG如果你想要的话。 您也可以复制Spring Boot的脂肪 JarLauncher 进入映像并使用它来运行应用程序-它可以工作,并且您不需要指定主类,但是启动时会有点慢。 ## 调整 如果您想尽快启动应用程序(大多数人都这样做),则可以考虑一些调整。 这里有一些想法: * 使用 `spring-context-indexer`( [链接到文档](https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans-scanning-index) )。 对于小型应用程序,它不会增加太多,但对您有所帮助。 * 不要使用 [执行器](https://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#production-ready) 如果您负担不起,请 。 * 使用Spring Boot 2.1和Spring 5.1。 * 修复的位置 [春天启动配置文件(S)](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-external-config-application-property-files) 与 `spring.config.location` (命令行参数或系统属性等)。 * 关闭JMX-您可能不需要在容器中使用- `spring.jmx.enabled=false` * 使用以下命令运行JVM `-noverify`。 还考虑 `-XX:TieredStopAtLevel=1` (这将在以后减慢JIT速度,但会节省启动时间)。 * 使用Java 8的容器内存提示: `-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap`。 对于Java 11,默认情况下是自动的。 您的应用程序在运行时可能不需要完整的CPU,但需要多个CPU才能尽快启动(至少2、4个更好)。 如果您不介意启动速度较慢,则可以将CPU的速度降低到4以下。如果您被迫以少于4个CPU的速度启动,则可能有助于设置 `-Dspring.backgroundpreinitializer.ignore=true`因为它阻止了Spring Boot创建可能无法使用的新线程(适用于Spring Boot 2.1.0及更高版本)。 ## 多阶段构建 这 `Dockerfile`以上假设胖JAR已在命令行上构建。 您也可以使用多阶段构建在Docker中执行此步骤,将结果从一个映像复制到另一个映像。 使用Maven的示例: `Dockerfile` ~~~ FROM openjdk:8-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 openjdk:8-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"] ~~~ 第一个图像标记为“ build”,它用于运行Maven并构建胖罐,然后将其解压缩。 拆包也可以由Maven或Gradle完成(这是《入门指南》中采用的方法)-确实没有太大区别,只是必须编辑构建配置并添加插件。 请注意,源代码已分为4层。 较后的层包含构建配置和应用程序的源代码,较早的层包含构建系统本身(Maven包装器)。 这是一个很小的优化,这也意味着我们不必复制 `target`目录到docker映像,甚至是用于构建的临时映像。 源代码更改的每个构建都将变慢,因为必须在第一个构建中重新创建Maven缓存。 `RUN`部分。 但是您拥有一个完全独立的构建,只要拥有docker,任何人都可以运行它来使您的应用程序运行。 在某些环境中(例如,您需要与不懂Java的人共享代码),这可能非常有用。 ### 实验功能 Docker 18.06带有一些 [“实验性”功能](https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/experimental.md) ,其中包括一种缓存构建依赖项的方法。 要打开它们,您需要在守护程序中添加一个标志( `dockerd`)以及运行客户端时的环境变量,然后可以在您的计算机上添加第一行魔术 `Dockerfile`: `Dockerfile` ~~~ # syntax=docker/dockerfile:experimental ~~~ 和 `RUN` 指令然后接受一个新的标志 `--mount`。 这是一个完整的例子: `Dockerfile` ~~~ # syntax=docker/dockerfile:experimental FROM openjdk:8-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 openjdk:8-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 openjdk:8-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 openjdk:8-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"] ~~~ 虽然这些功能处于实验阶段,但打开和关闭buildkit的选项取决于版本。 docker您正在使用的。 检查您所拥有版本的文档(上面的示例适用于 docker 18.0.6). ## 安全方面 就像在传统的VM部署中一样,不应在具有root权限的情况下运行进程。 相反,映像应包含运行该应用程序的非root用户。 在一个 `Dockerfile`,这可以通过添加另一层来添加(系统)用户和组,然后将其设置为当前用户(而不是默认用户root)来实现: `Dockerfile` ~~~ FROM openjdk:8-jdk-alpine RUN addgroup -S demo && adduser -S demo -G demo USER demo ... ~~~ 如果有人设法突破您的应用程序并在容器内运行系统命令,这将限制他们的功能(最小特权原则)。 一些进一步 Dockerfile 命令仅以root用户身份运行,因此也许您必须将USER命令进一步向下移动(例如,如果您打算将更多软件包安装到仅以root用户身份运行的容器中)。 其他方法,不使用 Dockerfile,可能会更好一些。 例如,在稍后描述的buildpack方法中,默认情况下,大多数实现将使用非root用户。 另一个考虑因素是,大多数应用程序在运行时可能不需要完整的JDK,因此一旦我们进行了多阶段构建,我们就可以安全地切换到JRE基础映像。 因此,在上面的多阶段构建中,我们可以使用 `Dockerfile` ~~~ FROM openjdk:8-jre-alpine ... ~~~ 以获得最终的可运行图像。 如上所述,这还节省了映像中的一些空间,这些空间将由运行时不需要的工具占用。 ## 构建插件 如果你不想打电话 `docker`直接在您的构建中,有很多针对Maven和Gradle的插件可以为您完成此工作。 这里仅仅是少数。 ### Spring Boot插件 使用Spring Boot 2.3,您可以选择直接使用Spring Boot从Maven或Gradle构建映像。 只要您已经在构建Spring Boot jar文件,您只需要直接调用插件即可。 使用 [Maven](https://docs.spring.io/spring-boot/docs/current/maven-plugin/reference/html/#build-image) : ~~~ $ ./mvnw spring-boot:build-image ~~~ 并与 [Gradle](https://docs.spring.io/spring-boot/docs/current/gradle-plugin/reference/html/#build-image) ~~~ $ ./gradlew bootBuildImage ~~~ 它使用本地docker守护程序(因此必须安装),但不需要 `Dockerfile`。 结果是一个名为 `docker.io/<group>/<artifact>:latest`默认。 您可以使用以下方法在Maven中修改图像名称: ~~~ <project> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <image> <name>myorg.demo</name> </image> </configuration> </plugin> </plugins> </build> </project> ~~~ 并在Gradle中使用 ~~~ bootBuildImage { imageName = "myorg/demo" } ~~~ The image is built using [Cloud Native Buildpacks](https://buildpacks.io/), where the default builder is optimized for a Spring Boot application (you can customize it but the defaults are useful). The image is layered efficiently, like in the examples above. It also uses the CF memory calculator to size the JVM at runtime based on the container resources available, so when you run the image you will see the memory calculator reporting its results: ~~~ $ docker run -p 8080:8080 myorg/demo Container memory limit unset. Configuring JVM for 1G container. Calculated JVM Memory Configuration: -XX:MaxDirectMemorySize=10M -XX:MaxMetaspaceSize=86557K -XX:ReservedCodeCacheSize=240M -Xss1M -Xmx450018K (Head Room: 0%, Loaded Class Count: 12868, Thread Count: 250, Total Memory: 1073741824) ... ~~~ ### Spotify Maven插件 在 [Spotify的Maven插件](https://github.com/spotify/dockerfile-maven) 是一个受欢迎的选择。 它要求应用程序开发人员编写一个 `Dockerfile` 然后运行 `docker`对您来说,就像您在命令行上一样。 docker image标签和其他内容有一些配置选项,但它使您应用程序中的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镜像。 我们可以用 `docker` 现在在命令行上,或使用Maven配置将其设置为 `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插件 该 [真知晶球摇篮插件](https://github.com/palantir/gradle-docker) 可与一 `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胖子罐解包。 `build`目录,这是docker构建的根目录。 然后是多层(不是多阶段) `Dockerfile` 从上面将工作。 ### Spring Boot Maven和Gradle插件 Spring Boot构建插件 [Maven](https://docs.spring.io/spring-boot/docs/2.3.0.RELEASE/maven-plugin/reference/html/#build-image) 和 [Gradle的](https://docs.spring.io/spring-boot/docs/2.3.0.RELEASE/gradle-plugin/reference/html/#build-image) 可用于创建容器映像。 插件会创建一个OCI图像(与以下格式创建的格式相同): `docker build`)使用 [Cloud Native Buildpacks](https://buildpacks.io/) 。 你不需要 `Dockerfile` 但是您确实需要一个docker守护程序,该守护程序可以是本地的(使用docker构建时使用的守护程序),也可以通过DOCKER\_HOST环境变量远程使用。 Maven的示例(不更改 `pom.xml`): ~~~ $ ./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=myorg/myapp ~~~ 和Gradle一起: ~~~ $ ./gradlew bootBuildImage --imageName=myorg/myapp ~~~ 第一次构建可能要花很长时间,因为它必须下载一些容器映像和JDK,但是后续的构建会很快。 如果您运行图像: ~~~ $ docker run -p 8080:8080 -t myorg/myapp 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 ~~~ 您会看到它正常启动。 您可能还会注意到,JVM内存需求是在容器内部计算并设置为命令行选项的。 这与Cloud Foundry构建包中使用多年的内存计算相同。 它代表了对各种JVM应用程序(包括但不限于Spring Boot应用程序)的最佳选择的重大研究,其结果通常比JVM的默认设置要好得多。 您可以自定义命令行选项,并使用环境变量覆盖内存计算器。 ### Jib Maven和Gradle插件 Google有一个名为 开源工具 [Jib的](https://github.com/GoogleContainerTools/jib) ,它相对较新,但出于多种原因却很有趣。 可能最有趣的是,您不需要docker来运行它-它使用与您获得的相同的标准输出来构建映像 `docker build` 但不使用 `docker`除非您要求-否则它可以在未安装docker的环境中运行(在构建服务器中并不罕见)。 您也不需要 `Dockerfile` (无论如何都会被忽略),或者您的任何内容 `pom.xml` 以获得在Maven中构建的图像(Gradle要求您至少在以下位置安装插件: `build.gradle`). Jib的另一个有趣特征是,它对层有看法,并且以与多层略有不同的方式对层进行了优化。 `Dockerfile`在上面创建。 就像在胖子罐中一样,Jib将本地应用程序资源与依赖项分离开来,但它走得更远,而且还将快照依赖项放入一个单独的层中,因为它们更容易发生变化。 有一些配置选项可用于进一步自定义布局。 Maven的示例(不更改 `pom.xml`): ~~~ $ mvn com.google.cloud.tools:jib-maven-plugin:build -Dimage=myorg/myapp ~~~ 要运行以上命令,您将需要具有在以下位置推送到Dockerhub的权限: `myorg`存储库前缀。 如果您已通过 `docker` 在命令行上,它将在您本地 `~/.docker`配置。 您还可以在您的服务器中设置Maven“服务器”身份验证 `~/.m2/settings.xml` (这 `id` 存储库的数量很重要): `settings.xml` ~~~ <server> <id>registry.hub.docker.com</id> <username>myorg</username> <password>...</password> </server> ~~~ 还有其他选项,例如,您可以针对docker守护程序在本地进行构建(例如运行 `docker` 在命令行上),使用 `dockerBuild` 目标而不是 `build`。 还支持其他容器注册表,对于每个容器注册表,您将需要通过docker或Maven设置来设置本地身份验证。 一旦您将gradle插件包含在其中,它便具有类似的功能 `build.gradle`,例如 `build.gradle` ~~~ plugins { ... id 'com.google.cloud.tools.jib' version '1.8.0' } ~~~ 或使用入门指南中使用的较旧样式: `build.gradle` ~~~ buildscript { repositories { maven { url "https://plugins.gradle.org/m2/" } mavenCentral() } dependencies { classpath('org.springframework.boot:spring-boot-gradle-plugin:2.2.1.RELEASE') classpath('com.google.cloud.tools.jib:com.google.cloud.tools.jib.gradle.plugin:1.8.0') } } ~~~ 然后您可以用 ~~~ $ ./gradlew jib --image=myorg/myapp ~~~ 与Maven构建一样,如果您已通过 `docker` 在命令行上,图像推送将从您本地的身份验证 `~/.docker` 配置。 ## 持续集成 如今(或应该如此),自动化已成为每个应用程序生命周期的一部分。 人们用来实现自动化的工具往往非常擅长于从源代码调用构建系统。 因此,如果您得到一个docker映像,并且构建代理中的环境与开发人员自己的环境充分匹配,这可能就足够了。 向Docker注册表进行身份验证可能是最大的挑战,但是所有自动化工具中都有一些功能可以帮助实现这一点。 但是,有时最好将容器创建完全留给自动化层,在这种情况下,可能不需要污染用户的代码。 容器创建很棘手,开发人员有时并不真正在意它。 如果用户代码更简洁,则其他工具更有可能“做正确的事”,应用安全修复程序,优化缓存等。自动化有多种选择,并且这些天都将具有与容器相关的某些功能。 我们只看几个。 ### 大堂 [Concourse](https://concourse-ci.org) 是可用于CI和CD的基于管道的自动化平台。 它在Pivotal内部大量使用,该项目的主要作者在那里工作。 除CLI外,Concourse中的所有内容都是无状态的,并且所有内容都在容器中运行。 由于运行容器是自动化管道的主要业务顺序,因此很好地支持创建容器。 该 [泊坞窗图像资源](https://github.com/concourse/docker-image-resource) 负责保持你构建的输出状态更新,如果它是一个容器图像。 这是为上面的示例构建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 ~~~ 管道的结构非常具有声明性:您可以定义“资源”(输入或输出或两者兼有)和“作业”(使用并向资源应用操作)。 如果任何输入资源发生更改,则会触发新的构建。 如果作业期间任何输出资源发生更改,则将对其进行更新。 可以在与应用程序源代码不同的位置定义管道。 对于一般的构建设置,任务声明也可以集中或外部化。 如果这是滚动的方式,则可以将开发和自动化之间的关注点分离开。 ### 詹金斯 [Jenkins](https://jenkins.io) 是另一种流行的自动化服务器。 它具有广泛的功能,但是,与此处的其他自动化示例最接近的 是 [管道](https://jenkins.io/doc/book/pipeline/docker/) 功能 功能。 这是一个 `Jenkinsfile` 它使用Maven构建Spring Boot项目,然后使用 `Dockerfile` 构建图像并将其推送到存储库: `Jenkinsfile` ~~~ node { checkout scm sh './mvnw -B -DskipTests clean package' docker.build("myorg/myapp").push() } ~~~ 对于需要在构建服务器中进行身份验证的(现实的)泊坞库,您可以将凭证添加到 `docker` 上面使用的对象 `docker.withCredentials(…​)`. ## 构建包 Spring Boot Maven和Gradle插件使用buildpack的方式与 packCLI在以下示例中进行操作。 主要区别在于插件使用 docker 运行构建,而 pack不需要。 给定相同的输入,得到的图像是相同的。 [Cloud Foundry](https://www.cloudfoundry.org/) 多年来一直在内部使用容器,用于将用户代码转换为容器的技术的一部分是Build Packs,该思想最初是从 借来的 [Heroku](https://www.heroku.com/) 。 当前的buildpacks(v2)生成通用二进制输出,该输出由平台组装到容器中。 在 [新一代buildpacks的](https://buildpacks.io/) (V3)是的Heroku和其他公司,包括枢纽之间的合作,并直接和明确地构建容器的图像。 这对于开发人员和操作员而言非常有趣。 开发人员不需要太在乎如何构建容器的细节,但是如果需要,他们可以轻松地创建一个容器。 Buildpacks还具有许多用于缓存构建结果和依赖项的功能,因此,与本地Docker构建相比,Buildpack的运行速度通常要快得多。 操作员可以扫描容器以审核其内容,并对其进行转换以修补它们以进行安全更新。 您可以在本地运行构建包(例如,在开发人员机器上或在CI服务中),也可以在Cloud Foundry之类的平台上运行。 buildpack生命周期的输出是一个容器映像,但是您不需要docker或 `Dockerfile`,因此对CI和自动化友好。 输出映像中的文件系统层由buildpack进行控制,通常,将进行许多优化,而无需开发人员知道或关心它们。 还有一个 [应用程序二进制接口](https://en.wikipedia.org/wiki/Application_binary_interface) 在较低层(如包含操作系统的基础映像)与较高层(如包含中间件和特定于语言的依赖关系)之间 。 如果存在安全更新,那么诸如Cloud Foundry之类的平台就可以修补较低的层,而不会影响应用程序的完整性和功能。 为了让您了解buildpack的功能,这里是一个 使用 的示例 [Pack CLI](https://github.com/buildpack/pack) 从命令行 (它将与我们在本指南中使用的示例应用程序一起使用,不需要 `Dockerfile` 或任何特殊的构建配置): ~~~ $ pack build myorg/myapp --builder=cloudfoundry/cnb:bionic --path=. 2018/11/07 09:54:48 Pulling builder image 'cloudfoundry/cnb:bionic' (use --no-pull flag to skip this step) 2018/11/07 09:54:49 Selected run image 'packs/run' from stack 'io.buildpacks.stacks.bionic' 2018/11/07 09:54:49 Pulling run image 'packs/run' (use --no-pull flag to skip this step) *** DETECTING: 2018/11/07 09:54:52 Group: Cloud Foundry OpenJDK Buildpack: pass | Cloud Foundry Build System Buildpack: pass | Cloud Foundry JVM Application Buildpack: pass *** ANALYZING: Reading information from previous image for possible re-use *** BUILDING: -----> Cloud Foundry OpenJDK Buildpack 1.0.0-BUILD-SNAPSHOT -----> OpenJDK JDK 1.8.192: Reusing cached dependency -----> OpenJDK JRE 1.8.192: Reusing cached launch layer -----> Cloud Foundry Build System Buildpack 1.0.0-BUILD-SNAPSHOT -----> Using Maven wrapper Linking Maven Cache to /home/pack/.m2 -----> Building application Running /workspace/app/mvnw -Dmaven.test.skip=true package ... ---> Running in e6c4a94240c2 ---> 4f3a96a4f38c ---> 4f3a96a4f38c Successfully built 4f3a96a4f38c Successfully tagged myorg/myapp:latest $ docker run -p 8080:8080 myorg/myapp . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.3.0.RELEASE) 2018-11-07 09:41:06.390 INFO 1 --- [main] hello.Application: Starting Application on 1989fb9a00a4 with PID 1 (/workspace/app/BOOT-INF/classes started by pack in /workspace/app) ... ~~~ 这 `--builder`是运行buildpack生命周期的docker映像-通常,它将是所有开发人员或单个平台上所有开发人员的共享资源。 您可以在命令行上设置默认构建器(在 `~/.pack`),然后从后续版本中忽略该标志。 这 cloudfoundry/cnb:bionic 生成器还知道如何从可执行的jar文件生成映像,因此您可以使用 mvnw 首先,然后指向 --path 到jar文件以得到相同的结果。 ## 基尼特语 容器和平台领域的另一个新项目是 [Knative](https://cloud.google.com/knative/) 。 Knative有很多东西,但是如果您不熟悉Knative,则可以将其视为构建无服务器平台的基础。 它基于 构建, [Kubernetes](https://kubernetes.io) 因此最终它会使用容器映像,并将它们转换为平台上的应用程序或“服务”。 但是,它的主要功能之一是能够使用源代码并为您构建容器,从而使其对开发人员和操作员更友好。 [Knative Build](https://github.com/knative/build) 是执行此操作的组件,它本身是一个将用户代码转换为容器的灵活平台-您几乎可以按照自己喜欢的任何方式进行操作。 一些模板提供了常见的模式,例如Maven和Gradle构建,以及使用 多阶段 构建 [Kaniko的 docker](https://github.com/GoogleContainerTools/kaniko) 。 还有一个使用 的模板, [Buildpacks](https://github.com/knative/build-templates/tree/master/buildpack) 这对我们来说很有趣,因为buildpacks一直对Spring Boot都有很好的支持。 ## 闭幕 本指南介绍了许多用于为Spring Boot应用程序构建容器映像的选项。 所有这些都是完全有效的选择,现在由您决定需要哪一个。 您的第一个问题应该是“我真的需要构建容器映像吗?” 如果答案是“是”,那么您的选择可能会受到效率和可缓存性以及关注点分离的影响。 您是否希望使开发人员不必过多地了解如何创建容器映像? 您是否想让需要修补操作系统和中间件漏洞的开发人员负责更新映像? 也许开发人员需要对整个过程进行完全控制,并且他们拥有所需的所有工具和知识。