你想构建一个Java应用程序并在Docker中运行它吗?你知道用Docker构建Java容器的最佳实践是什么吗?在下面的备忘单中,我将为您提供构建生产级Java容器的最佳实践,旨在优化和保护用于生产的Docker映像。1.Docker镜像使用确定性标签2.仅安装Java镜像中需要的内容3.查找并修复Java镜像中的安全漏洞4.使用Java镜像的多阶段构建5.不要以root身份运行Java应用程序6.不要为Java应用程序使用PID为1的进程7.优雅地离线Java应用程序8.使用.dockerignore文件9.确保Java版本支持容器10.谨慎使用容器自动化生成工具来构建一个简单的Java容器镜像让我们从一个简单的Dockerfile,在构建Java容器的时候,我们经常会有类似下面这样的东西:8080java-application这很简单,而且有效。然而,这个镜子充满了错误。我们不仅应该了解如何正确使用Maven,还应该避免像上面的示例那样构建Java容器。接下来,让我们开始一步步完善这个Dockerfile,让你的Java应用生成一个高效安全的Docker镜像。1.Docker镜像使用确定性标签在使用Maven构建Java容器镜像时,我们首先需要基于Maven镜像。但是,您知道在使用Maven基础映像时实际拉入的是什么吗?当您使用以下代码行构建映像时,您将获得该Maven映像的最新版本:FROMmaven这似乎是一个有趣的功能,但这种采用Maven默认映像的策略可能存在一些潜在问题:您的Dockerbuild不是幂等的。这意味着每次构建的结果可能完全不同,今天的最新镜像可能与明天或下周的最新镜像不同,导致您的应用程序的字节码不同,并且可能会发生意外。因此,在构建图像时,我们希望具有可重现的、确定性的行为。MavenDocker镜像基于完整的操作系统镜像。这会导致许多其他二进制文件出现在最终生产映像中,但其中许多二进制文件并不是运行Java应用程序所必需的。因此,将它们作为Java容器镜像的一部分使用有一些缺点:1.镜像尺寸变大,导致下载和构建时间变长。2.额外的二进制文件可能会引入安全漏洞。如何解决?使用适合您需要的最小基础镜像想一想——您是否需要一个完整的操作系统(包括所有额外的二进制文件)来运行您的程序?如果没有,也许基于alpine的镜像或Debian镜像会更好。使用特定图像如果您使用特定图像,您已经可以控制和预测某些行为。如果我使用maven:3.6.3-jdk-11-slim图像,我已经确定我正在使用JDK11和Maven3.6.3。JDK和Maven的更新将不再影响Java容器的行为。为了更精确,您还可以使用图像的SHA256哈希值。Usingahashwillensurethatyouusetheexactsamebaseimageeverytimeyoubuildtheimage.让我们用这些知识更新我们的Dockerfile:FROMmaven:3.6.3-jdk-11-slim@sha256:68ce1cd457891f48d1e137c7d6a4493f60843e84c9e2634e3df1d3d5b381d36cRUNmkdir/appWORKDIR/appCOPY./appRUNmvncleanpackage-DskipTests2.在Java镜像中只安装需要的内容以下命令会在容器中构建一个Java程序,包括它的所有依赖项。这意味着源代码和构建系统都将成为Java容器的一部分。RUNmvncleanpackage-DskipTests我们都知道Java是一种编译型语言。这意味着我们只需要您的构建环境创建的工件,而不是代码本身。这也意味着构建环境不应是Java映像的一部分。要运行Java映像,我们也不需要完整的JDK。Java运行时环境(JRE)就足够了。所以本质上,如果它是一个可运行的JAR,您只需要使用JRE和编译的Java工件来构建图像。使用Maven在CI管道中构建编译器,然后将JAR复制到图像中,如下面更新的Dockerfile所示:FROMopenjdk:11-jre-slim@sha256:31a5d3fa2942eea891cf954f7d07359e09cf1b1f3d35fb32fedebb1e3399fc9eRUNmkdir/appjavaCOPY。jar/app/java-application.jarWORKDIR/appCMD"java""-jar""java-application.jar"3.查找并修复Java镜像中的安全漏洞综上所述,我们已经开始使用适合的最小基础镜像我们的需求但是,我不知道这个基础镜像中的二进制文件是否包含问题。让我们使用SnykCLI等安全工具扫描测试我们的Docker镜像。您可以在此处注册一个免费的Snyk帐户。使用npm、brew、scoop安装SnykCLI或从Github下载最新的二进制文件:可以用snykcontainertest来测试。我也可以添加Dockerfile以获得更好的建议。Snyk在这个基础镜像中发现了58个安全问题。其中大部分与DebianLinux发行版附带的二进制文件有关。根据这些信息,我将基础镜像切换为adoptopenjdk提供的openjdk11:jre-11.0.9.1_1-alpine镜像。FROMadoptopenjdk/openjdk11:jre-11.0.9.1_1-alpine@sha256:b6ab039066382d39cfc843914ef1fc624aa60e2a16ede433509ccadd6d995b1f使用snyk容器命令进行测试时,此图像没有已知漏洞。以类似的方式,您可以通过snyktest命令在项目的根目录中测试Java应用程序。我建议在本地机器上开发时,测试应用程序和您创建的Java容器映像。接下来,对CI管道中的图像和应用程序执行相同的测试自动化。另外,请记住漏洞是随着时间的推移而被发现的。您可能希望在发现新漏洞时收到通知。另外,使用snykmonitor来监控你的应用程序,当发现新的安全问题时,你将能够及时采取适当的措施。Alternatively,youcanconnectyourgitrepositorytoSnyksowecanhelpfindandremediatevulnerabilities.让我们更新当前的Dockerfile:FROMadoptopenjdk/openjdk11:jre-11.0.9.1_1-alpine@sha256:b6ab039066382d39cfc843914ef1fc624aa60e2a16ede433509ccadd6d995b1fRUNmkdir/appCOPY./target/java-application.jar/app/java-application.jarWORKDIR/usr/src/projectCMD"java""-jar""java-application.jar"4.使用多阶段构建Java图像在本文前面,我们谈到了这样一个事实我们不需要在容器中构建Java应用程序。但是,在某些情况下,将我们的应用程序构建为Docker映像的一部分会很方便。我们可以将Docker镜像的构建分为多个阶段。我们可以使用构建应用程序所需的所有工具来构建镜像,并在最后阶段创建实际的生产镜像。FROMmaven:3.6.3-jdk-11-slim@sha256:68ce1cd457891f48d1e137c7d6a4493f60843e84c9e2634e3df1d3d5b381d36cASbuildRUNmkdir/projectCOPY./projectWORKDIR/projectRUNmvncleanpackage-DskipTestsFROMadoptopenjdk/openjdk11:jre-11.0.9.1_1-alpine@sha256:b6ab039066382d39cfc843914ef1fc624aa60e2a16ede433509ccadd6d995b1fRUNmkdir/appCOPY--from=build/project/target/java-application.jar/app/java-application.jarWORKDIR/appCMD"java""-jar""java-application.jar"防止敏感信息泄露在创建Java应用和Docker镜像时,很有可能需要为了连接私有仓库,settings.xml之类的配置文件经常会泄露敏感信息。但是当使用多阶段构建时,您可以安全地将settings.xml复制到您的构建容器中。带有凭据的设置不会出现在您的最终图像中。此外,如果您使用凭据作为命令行参数,则可以在构建映像中安全地执行此操作。使用多阶段构建,您可以创建多个阶段,并且只将结果复制到最终生产映像中。这就是分离是一种确保数据不会在生产环境中泄漏的方法。哦,对了,使用dockerhistory命令查看Java镜像的输出:$dockerhistoryjava-application输出只显示容器镜像的信息,并没有构建镜像的过程。5.不要以Root用户身份运行容器在创建Docker容器时,您需要应用最小权限原则来防止攻击者由于某种原因能够侵入您的应用程序,然后您不希望他们有访问权限对一切。拥有多层安全性可帮助您减少系统威胁。因此,您必须确保您没有以root身份运行该应用程序。但默认情况下,当您创建Docker容器时,您将以root身份运行它。虽然这对于开发很方便,但您不想在生产图像中使用它。假设由于某种原因,攻击者可以访问终端或可以执行代码。在这种情况下,它对正在运行的容器和对主机文件系统的访问具有重要的特权。解决方法很简单。创建一个具有有限权限的特定用户来运行您的应用程序,并确保该用户可以运行该应用程序。最后,不要忘记在运行应用程序之前使用新创建的用户。让我们相应地更新我们的Dockerfile。FROMmaven:3.6.3-jdk-11-slim@sha256:68ce1cd457891f48d1e137c7d6a4493f60843e84c9e2634e3df1d3d5b381d36cASbuildRUNmkdir/projectCOPY./projectWORKDIR/projectRUNmvncleanpackage-DskipTestsFROMadoptopenjdk/openjdk11:jre-11.0.9.1_1-alpine@sha256:b6ab039066382d39cfc843914ef1fc624aa60e2a16ede433509ccadd6d995b1fRUNmkdir/appRUNaddgroup--systemjavauser&&adduser-S-s/bin/false-GjavauserjavauserCOPY--from=build/project/target/java-application.jar/app/java-application.jarWORKDIR/appRUNchown-Rjavauser:javauser/appUSERjavauserCMD"java""-jar""java-application.jar"6.java应用程序不使用PID为1的进程在许多示例中,我看到了使用构建环境启动容器化Java应用程序的常见错误。以上,我们了解了在Java容器中使用Maven或者Gradle的重要性,但是使用下面的命令会有不同的效果:CMD"mvn""exec:java"CMD["mvn","spring-bootrun"]CMD"gradle""bootRun"CMD"run-app.sh"在Docker中运行应用程序时,第一个应用程序将以进程ID1(PID=1)运行。Linux内核以一种特殊的方式处理PID为1的进程。通常,进程号为1的PID上的进程是初始化进程。如果我们使用Maven来运行Java应用程序,我们如何确定Maven会将像SIGTERM这样的信号转发给Java进程呢?如果你像下面的例子一样运行一个Docker容器,你的Java应用程序将有一个PID为1的进程。CMD"java""-jar""application.jar"请注意,dockerkill和dockerstop命令只向容器发送信号PID为1的进程。例如,如果您正在运行Java应用程序shell脚本,/bin/sh不会将信号转发给子进程。更重要的是,在Linux中,PID为1的容器进程还有一些其他职责。它们在文章“《Docker和僵尸进程问题》”中有很好的描述。所以,在某些情况下,你不希望应用程序是PID为1的进程,因为你不知道如何处理这些。一个好的解决方案是使用dumb-init。RUNapkadddumb-initCMD"dumb-init""java""-jar""java-application.jar"当你像这样运行一个Docker容器时,dumb-init会接管PID为1的容器进程并承担所有责任。您的Java进程不再需要考虑这一点。我们更新后的Dockerfile现在看起来像这样:FROMmaven:3.6.3-jdk-11-slim@sha256:68ce1cd457891f48d1e137c7d6a4493f60843e84c9e2634e3df1d3d5b381d36cASbuildRUNmkdir/projectCOPY./projectWORKDIR/projectRUNmvncleanpackage-DskipTestsFROMadoptopenjdk/openjdk11:jre-11.0.9.1_1-alpine@sha256:b6ab039066382d39cfc843914ef1fc624aa60e2a16ede433509ccadd6d995b1fRUNapkadddumb-initRUNmkdir/appRUNaddgroup--systemjavauser&&adduser-S-s/bin/false-GjavauserjavauserCOPY--from=build/project/target/java-code-workshop-0.0.1-SNAPSHOT.jar/app/java-application.jarWORKDIR/appRUNchown-Rjavauser:javauser/appUSERjavauserCMD"dumb-init""java""-jar""java-application.jar"7.优雅地离线Java应用程序当你的应用程序收到一个关闭信号时,理想情况下我们希望一切都能正常关闭。根据您开发应用程序的方式,中断信号(SIGINT)或CTRL+C可能会导致进程立即终止。这可能不是您想要的,因为这样的事情会导致意外行为甚至数据丢失。当您将应用程序作为Payara或ApacheTomcat等Web服务器的一部分运行时,该Web服务器很可能会正常关闭。一些支持可运行应用程序的框架也是如此。例如,SpringBoot有一个嵌入式版本的Tomcat,可以有效地处理关闭。当您创建一个独立的Java应用程序或手动创建一个可运行的JAR时,您必须自己处理这些中断信号。解决方法很简单。添加退出挂钩(hook),如下例所示。收到SIGINT这样的信号后,就会开始优雅的离线申请流程。Runtime.getRuntime().addShutdownHook(newThread(){@Overridepublicvoidrun(){System.out.println("InsideAddShutdownHook");}});不可否认,与Dockerfile相关问题相比,这是一个通用的Web应用程序,但在容器环境中更是如此。8、使用.dockerignore文件为了防止不必要的文件污染git仓库,可以使用.gitignore文件。对于Docker镜像,我们有类似的东西——.dockerignore文件。与git的ignorefile类似,是为了防止Docker镜像中出现不需要的文件或目录。同时,我们不希望敏感信息泄露到我们的Docker镜像中。参见.dockerignore例如:.dockerignore**/*.logDockerfile.git.gitignore使用.dockerignore文件的要点是:跳过仅用于测试目的的依赖项。避免将密钥或凭据信息泄露到JavaDocker映像中的文件。此外,日志文件可能包含您不想公开的敏感信息。保持Docker镜像美观和干净本质上会使它们更小。除其他外,它有助于防止意外行为。9.确保Java版本支持容器Java虚拟机(JVM)是??个好东西。它会根据运行的系统进行自我调整。基于行为的调整可以动态优化堆的大小。但是,在Java8和Java9等旧版本中,JVM不知道容器设置的CPU限制或内存限制。这些旧Java版本的JVM会看到主机系统上的所有内存和所有CPU容量。Docker设置的限制将被忽略。随着Java10的发布,JVM现在是容器感知的,可以识别容器设置的约束。UseContainerSupport功能是一个JVM标志,默认情况下处于活动状态。Java10中发布的容器感知功能也已移植到Java-8u191。对于Java8之前的版本,您可以使用-Xmx标志手动尝试限制堆大小,但这是一个痛苦的练习。接下来,堆大小不等于Java使用的内存。对于Java-8u131和Java9,容器感知功能是实验性的,必须由您主动激活。-XX:+UnlockExperimentalVMOptions-XX:+UseCGroupMemoryLimitForHeap最好的选择是将Java更新到10以上的版本,这样默认就支持容器了。不幸的是,许多公司仍然严重依赖Java8。这意味着您应该在Docker映像中更新到最新版本的Java,或者确保您至少使用Java8update191或更高版本。10.谨慎使用容器自动化构建工具你可能会偶然发现适合你的构建系统的优秀工具和插件。除了这些插件之外,还有一些很棒的工具可以帮助您创建Java容器,甚至可以根据需要自动发布应用程序。从开发人员的角度来看,这看起来很棒,因为您不必在创建实际应用程序的同时花费精力维护Dockerfile。这种插件的一个例子是JIB。如下图,我只需要调用mvnjib:dockerBuild命令构建镜像
