构建Java镜像的10个最佳实践

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: 构建Java镜像的10个最佳实践

目录

构建一个简单的Java容器镜像

1.Docker镜像使用确定性的标签

2.在Java镜像中仅安装需要的内容

3. 查找并修复Java镜像中的安全漏洞

4.使用多阶段构建Java镜像

防止敏感信息泄漏

5.不要以Root用户运行容器

6.Java应用程序不要使用PID为1的进程

7.优雅下线Java应用程序

8.使用 .dockerignore文件

9.确保Java版本支持容器

10.谨慎使用容器自动化生成工具



你想构建一个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容器时,我们经常会有如下类似的内容:

FROM maven

RUN mkdir /app

WORKDIR /app

COPY . /app

RUN mvn clean install

CMD "mvn" "exec:java"

Copy that to a file named Dockerfile, then build and run it.

$ docker build . -t java-application

$ docker run -p 8080:8080 java-application

这很简单,而且有效。但是,此镜像充满错误。

我们不仅应该了解如何正确使用Maven,而且还应避免像上述示例那样构建Java容器。

下面,让我们开始逐步改进这个Dockerfile,使你的Java应用程序生成高效,安全的Docker镜像。


1.Docker镜像使用确定性的标签

当使用Maven构建Java容器镜像时,我们首先需要基于Maven镜像。但是,你知道使用Maven基本镜像时实际上引入了哪些内容吗?

当你使用下面的代码行构建镜像时,你将获得该Maven镜像的最新版本:

FROM maven

这似乎是一个有趣的功能,但是这种采用Maven默认镜像的策略可能存在一些潜在问题:

  • 你的Docker构建不是幂等的。这意味着每次构建的结果可能会完全不同,今天的最新镜像可能不同于明天或下周的最新镜像,导致你的应用程序的字节码也是不同的,并且可能发生意外。因此,构建镜像时,我们希望具有可复制的确定性行为。
  • Maven Docker镜像是基于完整的操作系统镜像。这样会导致许多其他二进制文件出现在最终的生产镜像中,但是运行你的Java应用程序不需要很多这些二进制文件。因此,将它们作为Java容器镜像的一部分存在一些缺点:
  • 镜像体积变大,导致更长的下载和构建时间。
  • 额外的二进制文件可能会引入安全漏洞。

如何解决呐?

  • 使用适合你需求的最小基础镜像 考虑一下-你是否需要一个完整的操作系统(包括所有额外的二进制文件)来运行你的程序?如果没有,也许基于alpine镜像或Debian的镜像会更好。
  • 使用特定的镜像 如果使用特定的镜像,则已经可以控制和预测某些行为。如果我使用maven:3.6.3-jdk-11-slim镜像,则已经确定我正在使用JDK 11和Maven 3.6.3。JDK和Maven的更新,将不再影响Java容器的行为。为了更加精确,你也可以使用镜像的SHA256哈希值。使用哈希将确保你每次构建镜像时都使用完全相同的基础镜像。

让我们用这些知识更新我们的Dockerfile:

FROM maven:3.6.3-jdk-11-slim@sha256:68ce1cd457891f48d1e137c7d6a4493f60843e84c9e2634e3df1d3d5b381d36c

RUN mkdir /app

WORKDIR /app

COPY . /app

RUN mvn clean package -DskipTests


2.在Java镜像中仅安装需要的内容

以下命令会在容器中构建Java程序,包括其所有依赖项。这意味着源代码和构建系统都将会是Java容器的一部分。

RUN mvn clean package -DskipTests

我们都知道Java是一种编译语言。这意味着我们只需要由你的构建环境创建的工件,而不需要代码本身。这也意味着构建环境不应成为Java镜像的一部分。

要运行Java镜像,我们也不需要完整的JDK。一个Java运行时环境(JRE)就足够了。因此,从本质上讲,如果它是可运行的JAR,那么只需要使用JRE和已编译的Java构件来构建镜像。

使用Maven在CI流水线中都构建编译程序,然后将JAR复制到镜像中,如下面的更新的Dockerfile中所示:

FROM openjdk:11-jre-slim@sha256:31a5d3fa2942eea891cf954f7d07359e09cf1b1f3d35fb32fedebb1e3399fc9e

RUN mkdir /app

COPY ./target/java-application.jar /app/java-application.jar

WORKDIR /app

CMD "java" "-jar" "java-application.jar"


3. 查找并修复Java镜像中的安全漏洞

通过上面,我们已经开始使用适合我们需求的最小基础镜像了,但是,我不知道此基本镜像中的二进制文件是否包含问题。让我们使用安全工具(如 Snyk CLI)扫描测试我们的Docker镜像。你可以在此处注册免费的Snyk帐户。

使用npm,brew,scoop或从Github下载最新的二进制文件来安装Snyk CLI:

$ npm install -g snyk

$ snyk auth

$ snyk container test openjdk:11-jre-slim@sha256:31a5d3fa2942eea891cf954f7d07359e09cf1b1f3d35fb32fedebb1e3399fc9e --file=Dockerfile

使用我刚刚创建的免费帐户来登录。使用snyk container test可以测试任何Docker镜像。此外,我还可以添加Dockerfile以获得更好的建议。

Snyk在此基本镜像中发现了58个安全问题。它们中的大多数与Debian Linux发行版附带的二进制文件有关。 根据此信息,我将基础镜像切换为由adoptopenjdk提供的openjdk11:jre-11.0.9.1_1-alpine镜像。

FROM adoptopenjdk/openjdk11:jre-11.0.9.1_1-alpine@sha256:b6ab039066382d39cfc843914ef1fc624aa60e2a16ede433509ccadd6d995b1f

然后使用snyk container命令对此进行测试时,此镜像没有已知的漏洞。

以类似的方式,你可以通过snyk test命令在项目的根目录中测试Java应用程序。我建议你在本地计算机上进行开发时,请同时测试应用程序和创建的Java容器镜像。紧接着,对CI流水线中的镜像和应用程序执行相同的测试自动化。

另外,请记住,随着时间的推移会发现漏洞。一旦发现新漏洞,你可能希望得到通知。

还有,使用snyk monitor监视你的应用程序,你将能够及时发现新的安全问题时采取适当的措施。

另外,你也可以将git存储库连接到Snyk,这样我们就可以帮助查找和补救漏洞。

让我们更新当前的Dockerfile:

FROM adoptopenjdk/openjdk11:jre-11.0.9.1_1-alpine@sha256:b6ab039066382d39cfc843914ef1fc624aa60e2a16ede433509ccadd6d995b1f

 

RUN mkdir /app

COPY ./target/java-application.jar /app/java-application.jar

WORKDIR /usr/src/project

CMD "java" "-jar" "java-application.jar"


4.使用多阶段构建Java镜像

在本文的前面,我们谈到了我们不需要在容器中构建Java应用程序。但是,在某些情况下,将我们的应用程序构建为Docker镜像的一部分很方便。

我们可以将Docker镜像的构建分为多个阶段。我们可以使用构建应用程序所需的所有工具来构建镜像,并在最后阶段创建实际的生产镜像。

FROM maven:3.6.3-jdk-11-slim@sha256:68ce1cd457891f48d1e137c7d6a4493f60843e84c9e2634e3df1d3d5b381d36c AS build

RUN mkdir /project

COPY . /project

WORKDIR /project

RUN mvn clean package -DskipTests

 

 

FROM adoptopenjdk/openjdk11:jre-11.0.9.1_1-alpine@sha256:b6ab039066382d39cfc843914ef1fc624aa60e2a16ede433509ccadd6d995b1f

RUN mkdir /app

COPY --from=build /project/target/java-application.jar /app/java-application.jar

WORKDIR /app

CMD "java" "-jar" "java-application.jar"


防止敏感信息泄漏

在创建Java应用程序和Docker镜像时,很有可能需要连接到私有仓库,类似settings.xml的配置文件经常会泄露敏感信息。但在使用多阶段构建时,你可以安全地将settings.xml复制到你的构建容器中。带有凭据的设置将不会出现在你的最终镜像中。此外,如果是将凭据用作命令行参数,则可以在构建镜像中安全地执行此操作。

使用多阶段构建,你可以创建多个阶段,仅将结果复制到最终的生产镜像中。这是分离是确保在生产环境中不泄漏数据的一种方法。

哦,顺便说一句,使用docker history命令查看Java镜像的输出:

$ docker history java-application

输出仅显示来自容器镜像的信息,而不显示构建镜像的过程。


5.不要以Root用户运行容器

创建Docker容器时,你需要应用最小特权原则,防止由于某种原因攻击者能够入侵你的应用程序,则你不希望他们能够访问所有内容。

拥有多层安全性,可以帮助你减少系统威胁。因此,必须确保你不以root用户身份运行应用程序。

但默认情况下,创建Docker容器时,你将以root身份运行它。尽管这对于开发很方便,但是你不希望在生产镜像中使用它。假设由于某种原因,攻击者可以访问终端或可以执行代码。在那种情况下,它对正在运行的容器具有显著的特权,并且访问主机文件系统。

解决方案非常简单。创建一个有限特权的特定用户来运行你的应用程序,并确保该用户可以运行该应用程序。最后,在运行应用程序之前,不要忘记使用新创建的用户。

让我们相应地更新我们的Dockerfile。

FROM maven:3.6.3-jdk-11-slim@sha256:68ce1cd457891f48d1e137c7d6a4493f60843e84c9e2634e3df1d3d5b381d36c AS build

RUN mkdir /project

COPY . /project

WORKDIR /project

RUN mvn clean package -DskipTests

 

 

FROM adoptopenjdk/openjdk11:jre-11.0.9.1_1-alpine@sha256:b6ab039066382d39cfc843914ef1fc624aa60e2a16ede433509ccadd6d995b1f

RUN mkdir /app

RUN addgroup --system javauser && adduser -S -s /bin/false -G javauser javauser

COPY --from=build /project/target/java-application.jar /app/java-application.jar

WORKDIR /app

RUN chown -R javauser:javauser /app

USER javauser

CMD "java" "-jar" "java-application.jar"


6.Java应用程序不要使用PID为1的进程

在许多示例中,我看到了使用构建环境来启动容器化Java应用程序的常见错误。

上面,我们了解了要在 Java容器中使用Maven或Gradle的重要性,但是使用如下命令,会有不同的效果:

  • CMD “mvn” “exec:java”
  • CMD [“mvn”, “spring-boot run”]
  • CMD “gradle” “bootRun”
  • CMD “run-app.sh”

在Docker中运行应用程序时,第一个应用程序将以进程ID为1(PID=1)运行。Linux内核会以特殊方式处理PID为1的进程。通常,进程号为1的PID上的过程是初始化过程。如果我们使用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。

RUN apk add dumb-init

CMD "dumb-init" "java" "-jar" "java-application.jar"

当你像这样运行Docker容器时,dumb-init会占用PID为1的容器进程并承担所有责任。你的Java流程不再需要考虑这一点。

我们更新后的Dockerfile现在看起来像这样:

FROM maven:3.6.3-jdk-11-slim@sha256:68ce1cd457891f48d1e137c7d6a4493f60843e84c9e2634e3df1d3d5b381d36c AS build

RUN mkdir /project

COPY . /project

WORKDIR /project

RUN mvn clean package -DskipTests

 

FROM adoptopenjdk/openjdk11:jre-11.0.9.1_1-alpine@sha256:b6ab039066382d39cfc843914ef1fc624aa60e2a16ede433509ccadd6d995b1f

RUN apk add dumb-init

RUN mkdir /app

RUN addgroup --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

WORKDIR /app

RUN chown -R javauser:javauser /app

USER javauser

CMD "dumb-init" "java" "-jar" "java-application.jar"


7.优雅下线Java应用程序

当你的应用程序收到关闭信号时,理想情况下,我们希望所有内容都能正常关闭。根据你开发应用程序的方式,中断信号(SIGINT)或CTRL + C可能导致立即终止进程。

这可能不是你想要的东西,因为诸如此类的事情可能会导致意外行为,甚至导致数据丢失。

当你将应用程序作为Payara或Apache Tomcat之类的Web服务器的一部分运行时,该Web服务器很可能会正常关闭。对于某些支持可运行应用程序的框架也是如此。例如,Spring Boot具有嵌入式Tomcat版本,可以有效地处理关机问题。

当你创建一个独立的Java应用程序或手动创建一个可运行的JAR时,你必须自己处理这些中断信号。

解决方案非常简单。添加一个退出钩子(hook ),如下面的示例所示。收到类似SIGINT信号后,优雅下线应用程序的进程将会被启动。

Runtime.getRuntime().addShutdownHook(new Thread() {

  @Override

  public void run() {

      System.out.println("Inside Add Shutdown Hook");

  }

});

诚然,与Dockerfile相关的问题相比,这是一个通用的Web应用程序问题,但在容器环境中更重要。


8.使用 .dockerignore文件

为了防止不必要的文件污染git存储库,你可以使用.gitignore文件。

对于Docker镜像,我们有类似的东西-.dockerignore文件。类似于git的忽略文件,它是为了防止Docker镜像中出现不需要的文件或目录。同时,我们也不希望敏感信息泄漏到我们的Docker镜像中。

请参阅以下示例的.dockerignore:

.dockerignore

**/*.log

Dockerfile

.git

.gitignore

使用.dockerignore文件的要点是:

  • 跳过仅用于测试目的的依赖项。
  • 使你免于泄露密钥或凭据信息进入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命令可以构建镜像

<plugin>

  <groupId>com.google.cloud.tools</groupId>

  <artifactId>jib-maven-plugin</artifactId>

  <version>2.7.1</version>

  <configuration>

      <to>

          <image>myimage</image>

      </to>

  </configuration>

</plugin>

它将为我构建一个具有指定名称的Docker镜像,而没有任何麻烦。

使用2.3及更高版本时,可以通过调用mvn命令进行操作:

mvn spring-boot:build-image

在种情况下,系统都会自动为我创建一个Java镜像。这些镜像还比较小,那是因为他们正在使用非发行版镜像或buildpack作为镜像的基础。但是,无论镜像大小如何,你如何知道这些容器是安全的?你需要进行更深入的调查,即使这样,你也不确定将来是否会保持这种状态。

我并不是说你在创建Java Docker时不应使用这些工具。但是,如果你打算发布这些镜像,则应研究Java镜像所有方面的安全。镜像扫描将是一个好的开始。从安全性的角度来看,我的观点是,以完全控制和正确的方式创建Dockerfile,是创建镜像更好,更安全的方式。


译文链接: https://snyk.io/blog/best-practices-to-build-java-containers-with-docker/



相关实践学习
通过容器镜像仓库与容器服务快速部署spring-hello应用
本教程主要讲述如何将本地Java代码程序上传并在云端以容器化的构建、传输和运行。
Kubernetes极速入门
Kubernetes(K8S)是Google在2014年发布的一个开源项目,用于自动化容器化应用程序的部署、扩展和管理。Kubernetes通常结合docker容器工作,并且整合多个运行着docker容器的主机集群。 本课程从Kubernetes的简介、功能、架构,集群的概念、工具及部署等各个方面进行了详细的讲解及展示,通过对本课程的学习,可以对Kubernetes有一个较为全面的认识,并初步掌握Kubernetes相关的安装部署及使用技巧。本课程由黑马程序员提供。 &nbsp; 相关的阿里云产品:容器服务 ACK 容器服务 Kubernetes 版(简称 ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情:&nbsp;https://www.aliyun.com/product/kubernetes
目录
相关文章
|
1月前
|
存储 Java 关系型数据库
高效连接之道:Java连接池原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。频繁创建和关闭连接会消耗大量资源,导致性能瓶颈。为此,Java连接池技术通过复用连接,实现高效、稳定的数据库连接管理。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接池的基本操作、配置和使用方法,以及在电商应用中的具体应用示例。
53 5
|
27天前
|
人工智能 前端开发 Java
基于开源框架Spring AI Alibaba快速构建Java应用
本文旨在帮助开发者快速掌握并应用 Spring AI Alibaba,提升基于 Java 的大模型应用开发效率和安全性。
基于开源框架Spring AI Alibaba快速构建Java应用
|
18天前
|
Java 数据库连接 开发者
Java中的异常处理机制及其最佳实践####
在本文中,我们将探讨Java编程语言中的异常处理机制。通过深入分析try-catch语句、throws关键字以及自定义异常的创建与使用,我们旨在揭示如何有效地管理和响应程序运行中的错误和异常情况。此外,本文还将讨论一些最佳实践,以帮助开发者编写更加健壮和易于维护的代码。 ####
|
22天前
|
Java
Java 异常处理下篇:11 个异常处理最佳实践
本文深入探讨了 Java 异常处理的最佳实践,包括早抛出晚捕获、只捕获可处理的异常、不要忽略捕获的异常、抛出具体检查性异常、正确包装自定义异常、记录或抛出异常但不同时执行、避免在 `finally` 块中抛出异常、避免使用异常进行流程控制、使用模板方法处理重复的 `try-catch`、尽量只抛出与方法相关的异常以及异常处理后清理资源。通过遵循这些实践,可以提高代码的健壮性和可维护性。
|
20天前
|
安全 Java 编译器
Java多线程编程的陷阱与最佳实践####
【10月更文挑战第29天】 本文深入探讨了Java多线程编程中的常见陷阱,如竞态条件、死锁、内存一致性错误等,并通过实例分析揭示了这些陷阱的成因。同时,文章也分享了一系列最佳实践,包括使用volatile关键字、原子类、线程安全集合以及并发框架(如java.util.concurrent包下的工具类),帮助开发者有效避免多线程编程中的问题,提升应用的稳定性和性能。 ####
46 1
|
23天前
|
Java 编译器 开发者
Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面
本文探讨了Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面,帮助开发者提高代码质量和程序的健壮性。
44 2
|
28天前
|
存储 Java 关系型数据库
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接创建、分配、复用和释放等操作,并通过电商应用实例展示了如何选择合适的连接池库(如HikariCP)和配置参数,实现高效、稳定的数据库连接管理。
52 2
|
28天前
|
Java 数据库连接 数据库
如何构建高效稳定的Java数据库连接池,涵盖连接池配置、并发控制和异常处理等方面
本文介绍了如何构建高效稳定的Java数据库连接池,涵盖连接池配置、并发控制和异常处理等方面。通过合理配置初始连接数、最大连接数和空闲连接超时时间,确保系统性能和稳定性。文章还探讨了同步阻塞、异步回调和信号量等并发控制策略,并提供了异常处理的最佳实践。最后,给出了一个简单的连接池示例代码,并推荐使用成熟的连接池框架(如HikariCP、C3P0)以简化开发。
48 2
|
28天前
|
存储 Java 开发者
成功优化!Java 基础 Docker 镜像从 674MB 缩减到 58MB 的经验分享
本文分享了如何通过 jlink 和 jdeps 工具将 Java 基础 Docker 镜像从 674MB 优化至 58MB 的经验。首先介绍了选择合适的基础镜像的重要性,然后详细讲解了使用 jlink 构建自定义 JRE 镜像的方法,并通过 jdeps 自动化模块依赖分析,最终实现了镜像的大幅缩减。此外,文章还提供了实用的 .dockerignore 文件技巧和选择安全、兼容的基础镜像的建议,帮助开发者提升镜像优化的效果。
|
1月前
|
存储 缓存 Java
Java应用瘦身记:Docker镜像从674MB优化至58MB的实践指南
【10月更文挑战第22天】 在容器化时代,Docker镜像的大小直接影响到应用的部署速度和运行效率。一个轻量级的Docker镜像可以减少存储成本、加快启动时间,并提高资源利用率。本文将分享如何将一个Java基础Docker镜像从674MB缩减到58MB的实践经验。
59 1