1. 概述

本文将展示如何在Docker中构建Maven项目。首先,我们将从一个简单的单模块Java项目开始,并展示如何利用Docker中的多阶段构建优化构建过程,同时利用多层构建的优势。接下来,我们将展示如何使用Buildkit在多个构建之间缓存依赖项。最后,我们将覆盖如何在多模块应用中利用层缓存。

2. 多阶段多层构建

对于本文,我们将创建一个简单的Java应用,其中包含Guava作为依赖项。我们将使用maven-assembly插件创建一个胖JAR文件。代码和Maven配置将在本文中被简略描述,因为它们并不是主要主题。

多阶段构建是优化Docker构建过程的一种好方法。它使我们能够在一个文件中保持整个流程,并且帮助我们尽可能地保持Docker镜像的体积最小。 在第一阶段,我们将运行Maven构建并生成我们的胖JAR文件,而在第二阶段,我们将复制JAR文件并定义入口点:

FROM maven:alpine as build
ENV HOME=/usr/app
RUN mkdir -p $HOME
WORKDIR $HOME
ADD . $HOME
RUN mvn package

FROM openjdk:8-jdk-alpine 
COPY --from=build /usr/app/target/single-module-caching-1.0-SNAPSHOT-jar-with-dependencies.jar /app/runner.jar
ENTRYPOINT java -jar /app/runner.jar

这种方法允许我们在最终的Docker镜像更小的情况下运行,因为它不会包含Maven执行程序或我们的源代码。

让我们创建Docker镜像:

docker build -t maven-caching .

接下来,让我们从镜像启动容器:

docker run maven-caching

当我们在代码中进行更改并重新运行构建时,我们会注意到所有在Maven package任务之前的命令都被缓存并立即执行。由于我们的代码比项目依赖项更新得更快,我们可以利用Docker层缓存分离依赖下载和代码编译来提高构建时间

FROM maven:alpine as build
ENV HOME=/usr/app
RUN mkdir -p $HOME
WORKDIR $HOME
ADD pom.xml $HOME
RUN mvn verify --fail-never
ADD . $HOME
RUN mvn package

FROM openjdk:8-jdk-alpine 
COPY --from=build /usr/app/target/single-module-caching-1.0-SNAPSHOT-jar-with-dependencies.jar /app/runner.jar
ENTRYPOINT java -jar /app/runner.jar

当我们仅更改代码时运行后续构建将会非常快速,因为Docker会从缓存中获取层。

3. 使用Buildkit缓存

Docker版本18.09引入了Buildkit,作为现有构建系统的全面改造。改造的想法是为了改进性能、存储管理和安全性。我们可以利用Buildkit在多个构建之间保持状态。这样,Maven就不会每次都下载依赖项,因为我们有永久存储。为了启用Docker安装中的Buildkit,我们需要编辑daemon.json文件:

...
{
"features": {
    "buildkit": true
}}
...

启用Buildkit后,我们可以更改Dockerfile为:

FROM maven:alpine as build
ENV HOME=/usr/app
RUN mkdir -p $HOME
WORKDIR $HOME
ADD . $HOME
RUN --mount=type=cache,target=/root/.m2 mvn -f $HOME/pom.xml clean package

FROM openjdk:8-jdk-alpine
COPY --from=build /usr/app/target/single-module-caching-1.0-SNAPSHOT-jar-with-dependencies.jar /app/runner.jar
ENTRYPOINT java -jar /app/runner.jar

当我们更改代码或pom.xml文件时,Docker将始终执行ADD和运行Maven命令。构建时间在首次运行时最长,因为Maven需要下载依赖项。后续运行将使用本地依赖项并执行得更快。

这种方法需要维护Docker卷作为依赖项的存储。有时,我们可能需要强制Maven使用Dockerfile中的-U标志更新依赖项。

4. 多模块Maven项目的缓存

在前面的部分,我们展示了如何利用不同的方法加快单模块Maven项目Docker镜像的构建时间。对于更复杂的应用程序,这些方法并不理想。多模块Maven项目通常有一个作为应用入口点的模块。一个或更多模块包含我们的逻辑,并作为依赖项列出。

由于子模块作为依赖项列出,这将阻止Docker利用层缓存并触发Maven再次下载所有依赖项。在大多数情况下,使用Buildkit的此解决方案是好的但正如我们所说的,它可能需要定期强制更新子模块以获取更新。为了避免这种情况,我们可以将项目分解为层次结构并使用Maven增量构建:

FROM maven:alpine as build
ENV HOME=/usr/app
RUN mkdir -p $HOME
WORKDIR $HOME

ADD pom.xml $HOME
ADD core/pom.xml $HOME/core/pom.xml
ADD runner/pom.xml $HOME/runner/pom.xml

RUN mvn -pl core verify --fail-never
ADD core $HOME/core
RUN mvn -pl core install
RUN mvn -pl runner verify --fail-never
ADD runner $HOME/runner
RUN mvn -pl core,runner package

FROM openjdk:8-jdk-alpine
COPY --from=build /usr/app/runner/target/runner-0.0.1-SNAPSHOT-jar-with-dependencies.jar /app/runner.jar
ENTRYPOINT java -jar /app/runner.jar

在这个Dockerfile中,我们复制所有pom.xml文件并依次构建每个子模块,然后在末尾打包整个应用程序。一般来说,我们应该在链的较晚部分构建那些变化更频繁的子模块。

5. 结论

在本文中,我们覆盖了如何使用Docker构建Maven项目。首先,我们介绍了如何利用层化来缓存不经常改变的部分。接下来,我们介绍了如何使用Buildkit在构建之间保持状态。最后,我们展示了如何使用增量构建构建多模块Maven项目。如往常一样,完整的代码可以在GitHub上找到