你想构建一个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容器时,我们经常会遇到类似如下的情况:
来自maven
运行mkdir /app
工作目录/应用程序
收到。/应用程序
运行mvn全新安装
CMD # 34mvn # 34#34;exec:Java # 34;
将它复制到一个名为Dockerfile的文件中,然后构建并运行它。
$ docker构建。-t java应用程序
$ docker run-p 8080:8080 Java-应用程序
简单有效。然而,这面镜子充满了错误。
我们不仅要知道如何正确使用Maven,还要避免像上面的例子那样构建Java容器。
现在,让我们开始一步一步地改进这个Dockerfile,让您的Java应用程序能够生成一个高效、安全的Docker映像。
1.Docker镜像使用确定性标签。
在使用Maven构建Java容器映像时,我们首先需要基于Maven映像。但是,你知道在使用Maven基础镜像时实际引入的是什么吗?
当您使用以下代码行构建映像时,您将获得这个Maven映像的最新版本:
来自maven
这似乎是一个有趣的特性,但是采用Maven默认镜像的策略可能存在一些潜在的问题:
你的Docker版本不是等幂的。这意味着每次构建的结果可能完全不同,今天的最新映像可能与明天或下周的不同,导致你的应用程序的字节码不同,可能会发生意外。因此,在构建镜像时,我们希望具有可再现的确定性行为。Maven Docker映像基于完整的操作系统映像。这将导致许多其他二进制文件出现在最终的产品映像中,但是您不需要这些二进制文件来运行您的Java应用程序。因此,将它们用作Java容器映像的一部分有一些缺点:1 .映像卷变得更大,导致下载和构建时间更长。
2.额外的二进制文件可能会引入安全漏洞。
怎么解决?
用适合你需求的最基本的映像来考虑一下——你需要一个完整的操作系统(包括所有额外的二进制)来运行你的程序吗?如果没有,也许基于alpine mirror或者Debian mirror会更好。使用特定的镜子如果你使用特定的镜子,你已经可以控制和预测一些行为。如果我用的是maven:3.6.3-jdk-11-slim镜像,已经确定我用的是JDK 11和Maven 3.6.3。JDK和Maven的更新将不再影响Java容器的行为。为了更加准确,您还可以使用镜像的SHA256哈希值。使用散列将确保您每次构建映像时都使用完全相同的基础映像。让我们用这些知识更新我们的docker文件:
来自Maven:3 . 6 . 3-JDK-11-slim @ sha 256:68 ce 1 CD 457891 f 48 D1 e 137 c 7 d6a 4493 f 60843 e 84 C9 e 2634 e 3 df1 d 3d 5b 381d 36 crunmkdir/app copy。/apprunmvn干净的包-dskiptests2
以下命令在容器中构建Java程序,包括它的所有依赖项。这意味着源代码和构建系统都将是Java容器的一部分。
运行mvn干净包-DskipTests
我们都知道Java是一种编译语言。这意味着我们只需要由您的构建环境创建的工件,而不是代码本身。这也意味着构建环境不应该是Java映像的一部分。
要运行Java镜像,我们也不需要完整的JDK。一个Java运行时环境(JRE)就足够了。因此,本质上,如果它是一个可运行的JAR,您只需要使用JRE和编译的Java工件来构建映像。
使用Maven在CI管道中构建编译器,然后将JAR复制到镜像中,如下面更新的Dockerfile文件所示:
来自open JDK:11-JRE-slim @ sha 256:31 a5d 3 fa 2942 EEA 891 cf 954 f 7d 07359 e 09 cf 1 B1 F3 d 35 FB 32 fede bb1e 3399 fc9 e
运行mkdir /app
收到。/target/Java-application . jar/app/Java-application . jar
工作目录/应用程序
CMD # 34java # 34#34;-jar # 34;#34;Java-application . jar # 34;
3.查找并修复Java映像中的安全漏洞
通过以上,我们已经开始使用适合我们需求的最小基础映像。但是,我不知道这个基础映像中的二进制文件是否包含问题。让我们使用Snyk CLI等安全工具扫描和测试我们的Docker映像。你可以在这里注册一个免费的Snyk帐号。
使用npm、brew、scoop或从Github下载最新的二进制文件来安装Snyk CLI:
$ npm安装-g snyk
$ snyk验证
$ snyk容器测试打开JDK:11-JRE-slim @ sha 256:31 a5 D3 fa 2942 EEA 891 cf 954 f 7d 07359 e 09 cf 1 B1 F3 d 35 FB 32 fede bb1e 3399 fc9 e-file = docker file
使用我刚刚创建的免费帐户登录。使用snyk容器测试来测试任何Docker图像。另外,我还可以添加Dockerfile来获得更好的建议。
Snyk在这张基础图中发现了58个安全问题。大多数都与Debian Linux发行版中包含的二进制文件有关。根据这些信息,我把基础镜换成了adoptopenjdk提供的open JDK 11:JRE-11 . 0 . 9 . 1 _ 1-alpine mirror。
来自adoptopenjdk/open JDK 11:JRE-11 . 0 . 9 . 1 _ 1-alpine @ sha 256:b6ab 039066382d 39 CFC 843914 ef 1 fc 624 aa 60 e 2 a 16 ede 433509 ccad 6d 995 B1 f
然后,当使用snyk容器命令对此进行测试时,该镜像中没有已知的漏洞。
同样,您可以通过snyk test命令测试项目根目录中的Java应用程序。我建议当您在本地计算机上开发时,请同时测试应用程序和创建的Java容器映像。然后,对CI管道中的映像和应用程序执行相同的自动化测试。
此外,请记住,漏洞会随着时间的推移而被发现。一旦发现新的漏洞,您可能希望得到通知。
还有,通过使用snyk monitor来监控您的应用程序,您将能够在发现新的安全问题时及时采取适当的措施。
此外,您还可以将git存储库连接到Snyk,这样我们就可以帮助发现和修复漏洞。
让我们更新当前的Dockerfile文件:
来自adoptopenjdk/open JDK 11:JRE-11 . 0 . 9 . 1 _ 1-alpine @ sha 256:b6ab 039066382d 39 CFC 843914 ef 1 fc 624 aa 60 e 2 a 16 ede 433509 ccad 6d 995 B1 f
运行mkdir /app
收到。/target/Java-application . jar/app/Java-application . jar
工作目录/usr/src/项目
CMD # 34java # 34#34;-jar # 34;#34;Java-application . jar # 34;
4.使用多阶段构建Java映像
在本文的前面,我们谈到了我们不需要在容器中构建Java应用程序。然而,在某些情况下,将我们的应用程序构建为Docker映像的一部分是很方便的。
我们可以把Docker形象的构建分为多个阶段。我们可以使用构建应用程序所需的所有工具来构建镜像,并在最后阶段创建实际的生产镜像。
来自maven:3 . 6 . 3-JDK-11-slim @ sha 256:68 ce 1 CD 457891 f 48 D1 e 137 c 7 d6a 4493 f 60843 e 84 C9 e 2634 e 3 df 1d 3 D5 b 381d 36 c AS build
运行mkdir/项目
收到。/项目
工作方向/项目
运行mvn干净包-DskipTests
来自adoptopenjdk/open JDK 11:JRE-11 . 0 . 9 . 1 _ 1-alpine @ sha 256:b6ab 039066382d 39 CFC 843914 ef 1 fc 624 aa 60 e 2 a 16 ede 433509 ccad 6d 995 B1 f
运行mkdir /app
COPY-from = build/project/target/Java-application . jar/app/Java-application . jar
工作目录/应用程序
CMD # 34java # 34#34;-jar # 34;#34;Java-application . jar # 34;
防止敏感信息泄露
在创建Java应用和Docker映像时,很有可能需要连接到私有仓库,settings.xml这样的配置文件往往会泄露敏感信息。但是当使用多阶段构建时,可以安全地将settings.xml复制到构建容器中。带有凭据的设置将不会出现在您的最终图像中。此外,如果将凭据用作命令行参数,则可以在镜像构造中安全地执行此操作。
通过多阶段构建,您可以创建多个阶段,并且只将结果复制到最终生产镜像中。这种分离是确保生产环境中没有数据泄漏的一种方式。
哦,顺便说一下,使用docker history命令查看Java映像的输出:
$ docker历史Java-应用程序
输出只显示了来自容器映像的信息,而不是构建映像的过程。
5.不要以Root用户身份运行容器。
在创建Docker容器时,你需要应用最小特权原则来防止攻击者出于某种原因入侵你的应用程序,所以你不希望他们能够访问所有的内容。
拥有多层安全性可以帮助您减少系统威胁。因此,您必须确保您不是以root用户身份运行应用程序。
但是默认情况下,当您创建Docker容器时,您将以root用户身份运行它。虽然这便于开发,但是您不想在生产镜像中使用它。假设出于某种原因,攻击者可以访问终端或执行代码。在这种情况下,它对正在运行的容器有很大的特权,并访问主机文件系统。
解决办法很简单。创建一个具有有限权限的特定用户来运行您的应用程序,并确保该用户可以运行该应用程序。最后,在运行应用程序之前,不要忘记使用新创建的用户。
让我们相应地更新我们的docker文件。
来自maven:3 . 6 . 3-JDK-11-slim @ sha 256:68 ce 1 CD 457891 f 48 D1 e 137 c 7 d6a 4493 f 60843 e 84 C9 e 2634 e 3 df 1d 3 D5 b 381d 36 c AS build
运行mkdir/项目
收到。/项目
工作方向/项目
运行mvn干净包-DskipTests
来自adoptopenjdk/open JDK 11:JRE-11 . 0 . 9 . 1 _ 1-alpine @ sha 256:b6ab 039066382d 39 CFC 843914 ef 1 fc 624 aa 60 e 2 a 16 ede 433509 ccad 6d 995 B1 f
运行mkdir /app
运行add group-system javauser adduser-S-S/bin/false-G javauser javauser
COPY-from = build/project/target/Java-application . jar/app/Java-application . jar
工作目录/应用程序
运行chown -R javauser:javauser /app
用户javauser
CMD # 34java # 34#34;-jar # 34;#34;Java-application . jar # 34;
6.Java应用程序不应使用PID为1的进程。
在许多例子中,我看到了使用构建环境启动容器化Java应用程序的常见错误。
上面,我们学习了在Java容器中使用Maven或Gradle的重要性,但是使用下面的命令会有不同的效果:
Cmd mvn,exec: Java cmd [mvn,spring-boot run] cmd gradle,boot run cmd run-app.sh在Docker中运行一个应用程序时,第一个应用程序会以进程ID 1(PID = 1)运行。Linux内核将以一种特殊的方式处理PID为1的进程。通常,PID上进程号为1的进程是初始化进程。如果我们使用Maven运行Java应用程序,我们如何确定Maven会将类似SIGTERM的信号转发给Java进程?
如果像下面的例子一样运行Docker容器,Java应用程序将有一个PID为1的进程。
CMD " Java " "-jar " " application . jar "
注意,docker kill和docker stop命令只向PID为1的容器进程发送信号。例如,如果您正在运行Java应用程序的shell脚本,/bin/sh不会将信号转发给子进程。
更重要的是,在Linux中,PID为1的容器进程还有一些其他的职责。他们在文章“Docker和僵尸进程的问题”中有很好的描述。因此,在某些情况下,您不希望应用程序是PID为1的进程,因为您不知道如何处理这些问题。一个好的解决方案是使用dumb-init。
运行apk add dumb-init
CMD # 34哑初始化 # 34;#34;java # 34#34;-jar # 34;#34;Java-application . jar # 34;
当你像这样运行Docker容器时,dumb-init会用PID 1占用容器进程,并承担所有责任。您的Java进程不再需要考虑这一点。
我们更新的docker文件现在看起来像这样:
来自maven:3 . 6 . 3-JDK-11-slim @ sha 256:68 ce 1 CD 457891 f 48 D1 e 137 c 7 d6a 4493 f 60843 e 84 C9 e 2634 e 3 df 1d 3 D5 b 381d 36 c AS build
运行mkdir/项目
收到。/项目
工作方向/项目
运行mvn干净包-DskipTests
来自adoptopenjdk/open JDK 11:JRE-11 . 0 . 9 . 1 _ 1-alpine @ sha 256:b6ab 039066382d 39 CFC 843914 ef 1 fc 624 aa 60 e 2 a 16 ede 433509 ccad 6d 995 B1 f
运行apk add dumb-init
运行mkdir /app
运行add group-system javauser adduser-S-S/bin/false-G javauser javauser
COPY-from = build/project/target/Java-code-workshop-0 . 0 . 1-snapshot . jar/app/Java-application . jar
工作目录/应用程序
运行chown -R javauser:javauser /app
用户javauser
CMD # 34哑初始化 # 34;#34;java # 34#34;-jar # 34;#34;Java-application . jar # 34;
7.优雅的离线Java应用程序
当您的应用程序收到关闭信号时,理想情况下,我们希望一切正常关闭。根据您开发应用程序的方式,中断信号(SIGINT)或CTRL+C可能会导致进程立即终止。
这可能不是您想要的,因为像这样的事情可能会导致意外的行为甚至数据丢失。
当您将应用程序作为Web服务器(如Payara或Apache Tomcat)的一部分运行时,Web服务器可能会正常关闭。对于一些支持可运行应用的框架来说也是如此。例如,Spring Boot有一个嵌入式Tomcat版本,可以有效地处理关机问题。
当您创建一个独立的Java应用程序或者手动创建一个可运行的JAR时,您必须自己处理这些中断信号。
解决办法很简单。添加一个退出挂钩,如下例所示。在接收到类似SIGINT的信号后,优雅的离线应用程序的进程将被启动。
Runtime.getRuntime()。addShutdownHook(new Thread(){ @ Override public void run(){ system . out . println( # 34;里面加关机挂钩 # 34;);}});诚然,与Dockerfile相关的问题相比,这是一个一般的Web应用问题,但在容器环境中更为重要。
8.使用。dockerignore文件
为了防止不必要的文件污染git存储库,可以使用。gitignore文件。
对于Docker图像,我们有类似的东西。dockerignore文件。类似于git的ignore files,是为了防止不需要的文件或目录出现在Docker镜像中。同时,我们不希望敏感信息泄露到我们的Docker映像中。
看吧。对于下面的例子:
。dockerignore * */*。忽略使用。docker忽略文件有:
仅出于测试目的跳过依赖关系。以防止您将密钥或凭证信息泄露到Java Docker镜像的文件中。此外,日志文件还可能包含您不想公开的敏感信息。保持Docker镜子美观整洁,本质上就是让镜子变小。此外,它还有助于防止意外行为。9.确保Java版本支持容器。
Java虚拟机(JVM)是一个神奇的东西。它会根据自己运行的系统进行自我调整。基于行为调整,堆大小可以动态优化。但是,在Java 8和Java 9等旧版本中,JVM无法识别容器设置的CPU限制或内存限制。这些旧Java版本的JVM会看到主机系统上的所有内存和所有CPU容量。Docker设置的限制将被忽略。
随着Java 10的发布,JVM现在可以感知容器,并且可以识别容器设置的约束。函数UseContainerSupport是一个JVM标志,默认情况下设置为活动状态。Java 10中发布的容器感知功能也被移植到Java-8u191中。
对于Java 8之前的版本,您可以手动尝试使用这个-Xmx标志来限制堆大小,但是这是一个痛苦的练习。其次,堆的大小不等于Java使用的内存。对于Java-8u131和Java 9,容器感知是实验性的,您必须主动激活它。
-XX:+UnlockExperimentalVMOptions-XX:+UseCGroupMemoryLimitForHeap
最好的选择是将Java更新到版本10或更高,这样默认支持容器。不幸的是,许多公司仍然严重依赖Java8。这意味着你应该在Docker镜像中更新到Java的最新版本,或者确保至少使用Java 8 update 191或更高版本。
10.小心使用容器自动生成工具。
您可能会偶然发现用于构建系统的优秀工具和插件。除了这些插件,还有一些很棒的工具可以帮助您创建Java容器,甚至可以根据需要自动发布应用程序。
从开发人员的角度来看,这看起来很棒,因为您不必在创建实际应用程序的同时花费精力维护Dockerfile。
JIB就是这种插件的一个例子。如下图,我只需要调用mvn jib:dockerBuild命令来构建镜像。
lt插件 gt ltgroupId gtcom . Google . cloud . tools lt;/groupId gt; ltartifactId gtjib-maven-plugin lt;/artifact id gt; lt版本 gt2 . 7 . 1 lt;/version gt; lt配置 gt ltto gt ltimage gtmyimage lt/image gt; lt/to gt; lt/configuration gt; lt/plugin gt;它会毫不费力地为我建立一个指定名称的Docker映像。
使用2.3版和更高版本时,可以通过调用mvn命令进行操作:
mvn spring-boot:构建映像
无论哪种情况,系统都会自动为我创建一个Java映像。这些镜像仍然相对较小,这是因为它们使用非分布镜像或构建包作为镜像的基础。但是,不管图像大小如何,你怎么知道这些容器是安全的呢?你需要进行更深入的调查。即便如此,你也不确定以后会不会保持这种状态。
我并不是说在创建Java Docker时不应该使用这些工具。但是,如果您计划发布这些映像,您应该研究Java映像安全性的所有方面。镜像扫描将是一个良好的开端。从安全性的角度来看,我的观点是,以完全可控的、正确的方式创建Dockerfile是一种更好、更安全的创建图像的方式。
欢迎大家关注。如果有什么好的学习经验,可以在评论区分享。如果文章内容不全面,也可以在评论区提出来。