【问题标题】:docker build with maven - how to prevent re-downloading dependenciesdocker build with maven - 如何防止重新下载依赖项
【发布时间】:2020-06-16 18:02:00
【问题描述】:

我希望基础映像 mavenDeps 下载依赖项并仅在依赖项更改时重建,而第二个映像 mavenBuild 在代码更改时重建。但是,在 docker build . 上,两个 maven 命令都下载所有依赖项。我可能误解了堆叠的工作原理或在哪里复制什么。

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

Docker 文件:

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"]

我正在 MacOS 上使用 Docker Desktop 2.2.2.0(引擎 19.03.5)进行构建。

编辑 2020.03.04:

来自@gcallea 的回答有效地防止重新下载pom 文件+1 中列出的依赖项。但是,install 步骤仍会在由代码更改触发的每个构建中提取 100 多个工件。这些是maven-resources-pluginmaven-compiler-plugin 和其他几个插件的临时依赖项,它们没有在任何地方明确列出。

我有时需要离线工作并且想预加载所有依赖项,因此在代码更改后不会提取任何依赖项。

【问题讨论】:

    标签: java maven docker


    【解决方案1】:

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

    您的 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,但这更有效,因为只下载所需的依赖项

    这里唯一改变的是 maven 依赖项下载缓存在 docker builder 缓存中:

    # 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 daemon json 文件...)

    【讨论】:

    • 谢谢你的解释,davidxxx。已尝试将所有 3 个resolveresolve-pluginsgo-offline 合为一个RUNpackage 步骤仍然会拉动所有 resourcescompilersurefirejar 插件依赖项 - 完全令人费解。将尝试第二种方法。
    • 如果在 mvn install 时,它仍然下载依赖项,第二种方法在这里没有帮助。这改进了之前的 maven 命令(解析),而不是 install。在您的情况下,我可以给您的最佳建议是在没有 docker 的情况下进行测试,以检查您是否没有 docker 构建问题。在本地:擦除本地 maven 存储库(或只是重命名),然后执行 mvn dependency:resolve-plugins && mvn dependency:resolve。最后执行mvn install。 install 命令会重新下载很多东西吗?
    • 惊喜!无 Docker 构建完美运行 - 没有依赖于 package。第二种方法也很有效。我确实希望有一种健全的方法可以在主机上强制执行DOCKER_BUILDKIT=1,因此我的docker-compose 文件仍然是可移植的,但是问题因此得到了解答。再次感谢,davidxxx。
    • 不客气 :) 很高兴它可以工作(这是最有效的方式),但这次轮到我对第一种方式重新下载的原因感到困惑了。如果那是公共代码,我将非常有兴趣对其进行测试。关于 buildkit 和 docker-compose,在你的 .bashrc 中导出这两行:export DOCKER_BUILDKIT=1 export COMPOSE_DOCKER_CLI_BUILD=1,用 source 刷新你的 shell,docker 和 docker-compose 都可以使用 buildkit 而无需任何额外的配置。
    • @kostja 感谢您提供的出色用例,让我了解了新事物。关于依赖关系的解决,我更新得更清楚了(昨天我写的有点快)。错字已修复。关于干净的行为,这是预期的,因为如果两个 maven 执行中的任何一个都需要这样做,则必须至少下载一次。
    【解决方案2】:

    您不需要将构建阶段分为 2 个不同的阶段 ma​​venDepsma​​venBuild。您可以包含一个 buildstage,利用 Docker 层实现相同目的。

    您可以根据自己的目的构建 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 多个工件。这些可以在单独的前一步中获取吗?
    • 所以如果你什么都不做改变并执行另一个构建,哪些层会从缓存中重用?只是mvn -B install 一个?
    • 当我只更改代码时,每次都会下载所有 maven 插件工件,而不是应用程序依赖项。我也想避免这种情况,以便能够离线工作。
    • 这是正确的,因为当您更改代码时,会使与命令COPY . . 相关的 Docker 层失效,因此所有下一个命令都会重新执行,而不是从缓存中获取。无论如何,关于工件的问题与 docker 并不严格相关,因为通常在连续构建中使用缓存的工件并不容易(例如,参见以下线程 stackoverflow.com/questions/19696053/…
    • 好吧,是的,也不是 :) 来自 pom 的依赖项没有重新下载,所以我的假设和 +1 的 PoV 中的“是”。其他依赖项仍在重新下载,所以从我的问题的 PoV 中“不”。我不知道这些额外的依赖关系,并将将此信息添加到问题中。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多