【问题标题】:How to cache maven dependencies in Docker如何在 Docker 中缓存 Maven 依赖项
【发布时间】:2019-05-10 12:48:11
【问题描述】:

我正在开发一个具有约 200MB 依赖项的项目,并且由于带宽有限,我想避免无用的上传。

当我推送我的 Dockerfile(稍后我会附上它)时,即使我没有接触 pom.xml,我也总是有大约 200MB 的上传:

FROM maven:3.6.0-jdk-8-slim

WORKDIR /app

ADD pom.xml /app

RUN mvn verify clean --fail-never

COPY ./src /app/src

RUN mvn package

ENV CONFIG_FOLDER=/app/config
ENV DATA_FOLDER=/app/data
ENV GOLDENS_FOLDER=/app/goldens
ENV DEBUG_FOLDER=/app/debug

WORKDIR target

CMD ["java","-jar","-Dlogs=/app/logs", "myProject.jar"]

这个 Dockerfile 应该创建一个 200MB 的 fatJAR,包括所有依赖项,这就是为什么每次都会上传 ~200MB 的原因。我想要实现的是构建一个包含所有依赖项的层,并“告诉”打包阶段不要将依赖项 JAR 包含到 fatJAR 中,而是在给定目录中搜​​索它们。

我想在构建过程之前构建一个执行mvn dependency:copy-dependencies的脚本,然后将目录复制到容器中;然后构建一个“非胖”JAR,所有这些依赖项仅链接而不实际复制到其中。

这可能吗?

编辑: 我发现容器的 Maven 本地存储库位于 /root/.m2 下。所以我结束了这样一个非常简单的脚本:

BuildDocker.sh

mvn verify -clean --fail-never
mv ~/.m2 ~/git/myProjectRepo/.m2

sudo docker build -t myName/myProject:"$1"

并编辑 Dockerfile 喜欢:

# Use an official Python runtime as a parent image
FROM maven:3.6.0-jdk-8-slim

# Copy my Mavne Local Repository into the container thus creating a new layer
COPY ./.m2 /root/.m2

# Set the working directory to /app
WORKDIR /app

# Copy the pom.xml
ADD pom.xml /app

# Resolve and Download all dependencies: this will be done only if the pom.xml has any changes
RUN mvn verify clean --fail-never

# Copy source code and configs 
COPY ./src /app/src

# create a ThinJAR
RUN mvn package


# Run the jar
...

在构建过程之后,我声明/root/.m2 拥有我所有的目录,但是一旦我启动 JAR,我就会得到:

Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/log4j/Priority
    at myProject.ThreeMeans.calculate(ThreeMeans.java:17)
    at myProject.ClusteringStartup.main(ClusteringStartup.java:7)
Caused by: java.lang.ClassNotFoundException: org.apache.log4j.Priority
    at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 2 more

也许我不应该通过java -jar 运行它?

【问题讨论】:

  • 我个人没有用过这个,但也许 Jib 可以帮忙:github.com/GoogleContainerTools/jib
  • 看看spring.io/blog/2018/11/08/spring-boot-in-a-container 它适用于 Spring Boot,但您可以对每个 maven 项目使用相同的方法。基本上你必须创建具有多个层的 Docker 文件,因此在构建过程中 Docker 可以缓存未更改的层。
  • 在您编辑的问题中,您引入了类似于@MyTwoCents 建议的命令COPY ./.m2 /root/.m2(因此可以将其视为执行RUN mvn dependency:go-offline -B 的标准解决方案的替代方案,即使COPY ./.m2 /root/.m2 更少便携,因为它需要在您的主机上安装 Maven),但我不确定这是否可以解决您关于与 fat jar 的推送相关的 ~200MB upload 的主要问题......cf .我的另一个comment

标签: java maven docker


【解决方案1】:

documentation of the official Maven Docker images 还指出了实现更好地缓存依赖项的不同方法。

基本上,他们建议要么将本地 maven 存储库作为卷挂载并在 Docker 映像中使用它,要么使用特殊的本地存储库 (/usr/share/maven/ref/),其内容将在容器启动时复制.

【讨论】:

    【解决方案2】:

    如果我正确理解您想要实现的目标,那么问题是避免在每个 Docker 构建中创建一个包含所有 Maven 依赖项的 fat jar(以减轻 Docker 层的大小重建后推动)。

    如果是,您可能对Spring Boot Thin Launcher 感兴趣,它也适用于非 Spring-Boot 项目。相应 GitHub 存储库的 README.md 中提供了一些综合文档: https://github.com/dsyer/spring-boot-thin-launcher#readme

    总而言之,在您的pom.xml 中添加以下插件声明就足够了:

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <!--<version>${spring-boot.version}</version>-->
                <dependencies>
                    <dependency>
                        <groupId>org.springframework.boot.experimental</groupId>
                        <artifactId>spring-boot-thin-layout</artifactId>
                        <version>1.0.19.RELEASE</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>
    

    理想情况下,此解决方案应与标准 Dockerfile 设置结合使用,以从 Docker 的缓存中受益(请参阅下面的典型示例)。

    为 Java/Maven 项目利用 Docker 的缓存机制

    如果仅触及源代码文件 (src/*),Dockerfile 的原型可以避免在每次构建时重新下载所有 Maven 依赖项:
    https://whitfin.io/speeding-up-maven-docker-builds/

    更准确地说,提议的Dockerfile如下:

    # our base build image
    FROM maven:3.5-jdk-8 as maven
    
    WORKDIR /app
    
    # copy the Project Object Model file
    COPY ./pom.xml ./pom.xml
    
    # fetch all dependencies
    RUN mvn dependency:go-offline -B
    
    # copy your other files
    COPY ./src ./src
    
    # build for release
    # NOTE: my-project-* should be replaced with the proper prefix
    RUN mvn package && cp target/my-project-*.jar app.jar
    
    
    # smaller, final base image
    FROM openjdk:8u171-jre-alpine
    # OPTIONAL: copy dependencies so the thin jar won't need to re-download them
    # COPY --from=maven /root/.m2 /root/.m2
    
    # set deployment directory
    WORKDIR /app
    
    # copy over the built artifact from the maven image
    COPY --from=maven /app/app.jar ./app.jar
    
    # set the startup command to run your binary
    CMD ["java", "-jar", "/app/app.jar"]
    

    请注意,它依赖于 Docker 的所谓 multi-stage build feature(存在两个 FROM 指令),这意味着最终映像将比 maven 基础映像本身小得多。
    (如果您在开发阶段对该功能不感兴趣,可以删除FROM openjdk:8u171-jre-alpineCOPY --from=maven /app/app.jar ./app.jar 行。)

    在这种方法中,Maven 依赖项通过RUN mvn dependency:go-offline -B before COPY ./src ./src 行获取(以受益于 Docker 的缓存)。

    但是请注意,dependency:go-offline 标准目标并不“完美”,因为一些动态依赖项/插件可能仍会在mvn package 步骤触发一些重新下载。 如果这对您来说是个问题(例如,如果在某些时候您真的想离线工作),您可以查看其他 SO answer,它建议使用提供 de.qaware.maven:go-offline-maven-plugin:resolve-dependencies 目标的专用插件。

    【讨论】:

    • 这很有趣,我肯定会读一读,但在尝试切换到此解决方案之前,我会尽我所能,只使用 Dockerfile。我现在不想为另一个复杂程度付出代价。但是,这肯定是 +1。
    • @L.Don 这取决于您想要实现的目标:从您最初的问题来看,似乎有两个正交问题需要解决:(1) 避免 ~200MB 下载在每个docker build,以及 (2) 在docker build … &amp;&amp; docker push 之后避免~200MB 上传(由.jar 的大尺寸引起)。我在回答中提到了这两个方面(为了解决第 (2) 点,似乎有必要调整 .jar 的构建方式,因此需要一个这样的 Maven 插件)。但是为了以可重复的方式解决第 (1) 点,确实可以在Dockerfile 级别完成所有操作。我将编辑我的答案以对此进行扩展。
    【解决方案3】:

    在一般的 Dockerfile 容器构建中,在层中工作,每次构建这些层时都可以在 catch 中使用,并且在没有更改的情况下使用。 理想情况下,它应该以相同的方式工作。

    默认情况下,Maven 通常在位于 Ubuntu /home/username/ 中用户的主目录中的 .m2 文件夹中查找依赖项

    如果依赖的 jar 不可用,那么它将这些 jar 下载到 .m2 并使用它。

    现在您可以在 1 次成功构建后压缩并复制此 .m2 文件夹并将其移动到 Docker 容器用户的主目录中。

    在运行构建命令之前执行此操作

    注意:您可能需要替换 docker 中现有的 .m2 文件夹

    所以你的 Docker 文件应该是这样的

    FROM maven:3.6.0-jdk-8-slim
    
    WORKDIR /app
    
    COPY .m2.zip /home/testuser/
    
    ADD pom.xml /app
    
    RUN mvn verify clean --fail-never
    
    COPY ./src /app/src
    
    RUN mvn package
    ...
    

    【讨论】:

    • 感谢您的回答,我会试一试,我会告诉你!所以基本上你的方法是不向 JAR 提供任何类型的库绑定的任何知识,而是直接替换容器中的 .m2 文件夹?
    • 我没有尝试您关于~/.m2.jar 文件的建议,所以我不确定它是否有效……但它是否记录在某处?我在maven.apache.org中没有找到这样的提及
    • 抱歉 dockerfile 中的拼写错误,它是 .m2.zip。当您第一次运行 mvn build/mvn install 时,将生成 .m2 文件夹,其中所有 jar 都以某种方式缓存。文件夹结构如下 ~/.m2/repository/com/oracle/ojdbc7/12.1.0.1/ojdbc7-12.1.0.1.jar。更多细节在这里:baeldung.com/maven-local-repository
    • 你解压了吗? /home/testuser 中 docker 内的 m2 文件夹。在运行构建命令之前
    • 我删除了之前的评论,因为我错了,现在它似乎符合我的要求,好吧。 @MyTwoCents 当我现在拥有包含所有依赖项的容器 .m2 文件夹时,我的应用程序仍然无法找到它们,从而导致 NoClassDefFoundError。我将编辑我的问题以获取更多信息。再次感谢您的帮助。
    猜你喜欢
    • 2022-12-18
    • 2019-03-24
    • 2017-07-01
    • 2023-03-14
    • 2014-11-10
    • 2019-07-23
    • 2018-06-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多