使用Maven构建Docker - 如何避免重新下载依赖项

22

我希望基础镜像mavenDeps仅在依赖项发生更改时下载依赖项并重新构建,第二个镜像mavenBuild则会在代码更改时重新构建。 但是,在执行docker build .命令时,两个maven命令都会下载所有依赖项。我可能误解了堆栈方式或者复制文件的位置。

我尝试过:显式地将第一个容器中的所有内容复制到第二个容器中:COPY / /以及一些更具体的COPY目标,如.m2,从maven基础镜像中构建第二个容器,就像第一个容器一样,然后将第一个容器中的所有内容复制到第二个容器中。

Dockerfile:

FROM maven:3.5-jdk-8 as mavenDeps
COPY pom.xml pom.xml
RUN mvn dependency:resolve

FROM mavenDeps as mavenBuild
RUN mvn install

FROM java:8
COPY --from=mavenBuild ./target/*.jar ./
ENV JAVA_OPTS ""
CMD [ "bash", "-c", "java ${JAVA_OPTS} -jar *.jar -v"]
我正在使用Docker桌面版2.2.2.0(引擎19.03.5)在MacOS上进行构建。
编辑2020.03.04:
@ gcallea的答案有效地防止了重新下载在pom文件中列出的依赖项+1。 但是,安装步骤仍会在每次由代码更改触发的构建中获取100多个工件。 这些是maven-resources-plugin,maven-compiler-plugin和其他几个插件的瞬态依赖项,它们没有明确列在任何地方。
有时我需要脱机工作,并且希望预加载所有依赖项,因此在代码更改后不会拉取任何依赖项。

请查看我的解决方案,网址为 https://dev59.com/N1gQ5IYBdhLWcg3wynCn#71066133。 - Antonio Petricca
2个回答

19

在告诉你我如何处理之前,先解释一下你遇到的问题。

你的 Dockerfile 依赖于构建多阶段特性。这里的阶段被视为中间层,在最终镜像中不作为层保留。如果要在层之间保留文件/文件夹,必须显式地复制它们,就像你所做的那样。

具体来说,在下面的指令中:maven 解析 pom.xml 中指定的所有依赖项,并将它们存储在该阶段的层上的本地仓库中:

FROM maven:3.5-jdk-8 as mavenDeps
COPY pom.xml pom.xml
RUN mvn dependency:resolve

但是,正如所说的那样,默认情况下不保留舞台内容。因此,由于您从未将其复制到下一个阶段中,因此本地maven存储库中下载的所有依赖项都将丢失:

FROM mavenDeps as mavenBuild
RUN mvn install

由于本地仓库中该镜像为空:mvn install会重新下载所有依赖项。


如何进行处理?

你有很多种方式。
最佳选择取决于你的要求。
但无论哪种方式,以Docker层为基础的构建策略如下:

构建阶段(Maven镜像):

  • pom文件复制到镜像中
  • 下载插件。
    关于这一点,mvn dependency:resolve-plugins链接到mvn dependency:resolve可能可以完成任务,但并不总是可行的。
    为什么?因为这些插件和package执行可能依赖不同的构件/插件,甚至对于相同的构件/插件,它们仍然可能拉取不同的版本。 因此,一个更安全但可能更慢的方法是通过执行mvn package命令来解析依赖项(这将精确地拉取您需要的依赖项),但通过跳过源代码编译并删除目标文件夹来加快处理速度并避免任何不必要的层更改检测到该步骤。
  • 将源代码复制到镜像中
  • 打包应用程序

运行阶段(JDK或JRE镜像):

  • 从上一个阶段复制jar文件

1)没有明确的Maven依赖项缓存:直接但在pom频繁更改时很烦人

如果每次pom.xml更改都重新下载所有依赖项是可接受的。

例如,通过使用你的脚本开始:

########build stage########
FROM maven:3.5-jdk-8 as maven_build
WORKDIR /app

COPY pom.xml .
# To resolve dependencies in a safe way (no re-download when the source code changes)
RUN mvn clean package -Dmaven.main.skip -Dmaven.test.skip && rm -r target

# To package the application
COPY src ./src
RUN mvn clean package -Dmaven.test.skip

########run stage########
FROM java:8
WORKDIR /app

COPY --from=maven_build /app/target/*.jar

#run the app
ENV JAVA_OPTS ""
CMD [ "bash", "-c", "java ${JAVA_OPTS} -jar *.jar -v"]

那种解决方案的缺点是什么? 任何pom.xml中的更改都意味着重新创建整个层以下载并存储maven依赖项。
对于具有许多依赖项的应用程序来说,这通常是不可接受的,尤其是在镜像构建期间不使用maven存储库管理器的情况下。

2)为maven依赖项显式设置缓存:需要更多配置和使用buildkit,但这更有效,因为只会下载所需的依赖项。

唯一变化的是,在docker构建器缓存中缓存了maven依赖项的下载:

# syntax=docker/dockerfile:experimental
########build stage########
FROM maven:3.5-jdk-8 as maven_build
WORKDIR /app

COPY pom.xml .    
COPY src ./src

RUN --mount=type=cache,target=/root/.m2 mvn clean package  -Dmaven.test.skip

########run stage########
FROM java:8
WORKDIR /app

COPY --from=maven_build /app/target/*.jar

#run the app
ENV JAVA_OPTS ""
CMD [ "bash", "-c", "java ${JAVA_OPTS} -jar *.jar -v"]
为了启用BuildKit,必须设置环境变量DOCKER_BUILDKIT=1(您可以在任何地方进行设置:bashrc、命令行、Docker守护程序JSON文件...)。

1
感谢您的解释,davidxxx。已经尝试了第一种方法,将所有3个resolveresolve-pluginsgo-offline放在一个RUN中。但是,在package步骤中仍然会拉取所有的resourcescompilersurefirejar插件依赖项 - 这完全令人困惑。将尝试第二种方法。 - kostja
如果在 mvn install 过程中仍然下载依赖项,第二种方法在这里无法帮助。它改进了先前的 Maven 命令(解析),而不是 install。在你的情况下,我能给你的最好建议就是在没有 Docker 的情况下进行测试,以检查是否存在 Docker 构建问题。在本地:清除你的本地 Maven 存储库(或只需重命名),然后执行 mvn dependency:resolve-plugins && mvn dependency:resolve。最后执行 mvn install。这个安装命令是否重新下载了很多东西? - davidxxx
1
不用客气 :) 很高兴你的方法可行(而且是最有效的方式),但这次轮到我对第一种方法重新下载的原因感到困惑了。如果这是公共代码,我会非常有兴趣测试它。关于buildkit和docker-compose,在你的.bashrc文件中导出这两行:export DOCKER_BUILDKIT=1 export COMPOSE_DOCKER_CLI_BUILD=1,使用source刷新你的shell,这样docker和docker-compose就可以使用buildkit而无需任何其他配置。 - davidxxx
1
@kostja 感谢您提供的出色用例,让我理解了一个新的东西。关于依赖项解析,我进行了更新以使其更清晰(昨天写得有点快)。错别字已经修正。关于清洁行为,这是预期的,因为如果两个Maven执行中的任何一个需要它,那么必须至少下载一次。 - davidxxx
有一个名为https://github.com/qaware/go-offline-maven-plugin的项目,它实际上做了`mvn dependency:go-offline`应该做的事情。作者在自述文件中非常好地解释了问题及其解决方案。 - ivant
显示剩余4条评论

5

您不需要将生成阶段分为两个不同的阶段 mavenDepsmavenBuild。您可以利用 Docker 层将单个 buildstage 包含在内,以达到相同的目的。

您可以根据自己的需求构建 Dockerfile,如下所示:

#----
# Build stage
#----
FROM maven:3.5-jdk-8 as buildstage
# Copy only pom.xml of your projects and download dependencies
COPY pom.xml .
RUN mvn -B -f pom.xml dependency:go-offline
# Copy all other project files and build project
COPY . .
RUN mvn -B install

#----
# Final stage
#----
FROM java:8
COPY --from=buildstage ./target/*.jar ./
ENV JAVA_OPTS ""
CMD [ "bash", "-c", "java ${JAVA_OPTS} -jar *.jar -v"]

只有在 pom.xml 文件发生更改时才会重新下载依赖项,否则与命令 RUN mvn -B -f pom.xml dependency:go-offline 相关的 Docker 层将被重用作缓存。

感谢 @gcallea。我刚试了一下,似乎部分工作正常。install 步骤似乎没有下载依赖项,但仍然下载了许多构件 - maven-resources-pluginmaven-compiler-plugin 和其他几个插件的瞬时依赖项,总共有 100 多个构件。这些能在单独的步骤中获取吗? - kostja
那么,如果您不做任何更改并执行另一个构建,哪些层将从缓存中重用?只有 mvn -B install 吗? - gregorycallea
当我仅更改代码时,每次都会下载所有Maven插件工件,而不是应用程序依赖项。我也想避免这种情况,以便能够离线工作。 - kostja
这是正确的,因为当您更改代码时,会使与命令COPY . .相关的Docker层无效,因此所有后续命令都将被重新执行而不是从缓存中获取。关于构件的任何问题都不严格与docker相关,因为通常在连续构建中难以使用缓存的构件(例如,请参见以下线程https://dev59.com/hnjZa4cB1Zd3GeqPfpf1)。 - gregorycallea
所以,即使我的解决方案可以避免重新下载依赖项,正如您在问题中提出的那样,您应该调查如何改善连续的Maven构建性能。 - gregorycallea
1
好的,是和不是 :) 根据我的假设,POM 文件中的依赖关系不会重新下载,所以从这个角度来看是“是”,并给出一个加分项。但是其他依赖仍然需要重新下载,从我的问题的角度来看是“否”。我之前不知道还有这些附加依赖关系,会在问题中添加这些信息。 - kostja

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接