【问题标题】:Docker cache gradle dependenciesDocker 缓存 gradle 依赖项
【发布时间】:2014-11-10 12:14:20
【问题描述】:

我正在尝试使用 docker 将我们的 java web 应用程序部署到 aws elastic beanstalk,其想法是能够在本地运行容器以进行开发和测试,并最终使用 git 将其推送到生产环境。

我创建了一个安装了 tomcat8 和 java8 的基础镜像,执行 gradle 构建的镜像继承自这个基础镜像,加快了构建过程。

一切正常,除了使用 docker 构建的继承应用程序容器似乎没有缓存 gradle 依赖项,它每次都会下载它,包括 gradlew。我们使用以下命令构建我们的 Web 应用程序:

./gradlew war

有什么方法可以缓存~/.gradle 中的文件,这将大大加快我的构建速度。

这在 beanstalk 上不是什么大问题,但对于尝试在本地构建和运行的开发人员来说是一个大问题,因为这确实需要很多时间,正如您可以想象的那样。

基础镜像dockerfile:

FROM phusion/baseimage
EXPOSE 8080
RUN apt-get update
RUN add-apt-repository ppa:webupd8team/java
RUN apt-get update
RUN echo oracle-java8-installer shared/accepted-oracle-license-v1-1 select true | sudo /usr/bin/debconf-set-selections
RUN apt-get -y install oracle-java8-installer
RUN java -version
ENV TOMCAT_VERSION 8.0.9
RUN wget --quiet --no-cookies http://archive.apache.org/dist/tomcat/tomcat-8/v${TOMCAT_VERSION}/bin/apache-tomcat-${TOMCAT_VERSION}.tar.gz -O /tmp/catalina.tar.gz
# Unpack
RUN tar xzf /tmp/catalina.tar.gz -C /opt
RUN mv /opt/apache-tomcat-${TOMCAT_VERSION} /opt/tomcat
RUN ln -s /opt/tomcat/logs /var/log/tomcat
RUN rm /tmp/catalina.tar.gz
# Remove unneeded apps
RUN rm -rf /opt/tomcat/webapps/examples
RUN rm -rf /opt/tomcat/webapps/docs
RUN rm -rf /opt/tomcat/webapps/ROOT
ENV CATALINA_HOME /opt/tomcat
ENV PATH $PATH:$CATALINA_HOME/bin
ENV CATALINA_OPTS $PARAM1
# Start Tomcat
CMD ["/opt/tomcat/bin/catalina.sh", "run"]

应用程序dockerfile:

FROM <tag name here for base image>
RUN mkdir ~/.gradle
# run some extra stuff here to add things to gradle.properties file
# Add project Source
ADD . /var/app/myapp
# Compile and Deploy Application, this is what is downloading gradlew and all the maven dependencies every time, if only there was a way to take the changes it makes to ~/.gradle and persist it as a cache layer
RUN cd /var/app/myapp/ && ./gradlew war
RUN mv /var/app/myapp/build/libs/myapp.war /opt/tomcat/webapps/ROOT.war
# Start Tomcat
CMD ["/opt/tomcat/bin/catalina.sh", "run"]

【问题讨论】:

  • 你能共享基础镜像和应用镜像的 Dockerfile 吗?
  • 我已将 docker 文件内容添加到描述中
  • 您需要将项目源代码和构建工件 (myapp.war) 放在一个映像中吗?

标签: java docker gradlew amazon-elastic-beanstalk


【解决方案1】:

我遇到了这个问题。正如您可能同意的那样,在构建 docker 映像时单独下载依赖项是一个单独的步骤,这是一个最佳实践。使用 gradle 变得有点棘手,因为没有直接支持仅下载依赖项。

选项 1:使用 docker-gradle Docker 映像


我们可以使用预先构建的 gradle docker 镜像来构建应用程序。这确保它不是本地系统构建,而是在干净的 docker 映像上完成的构建。

docker volume create --name gradle-cache
docker run --rm -v gradle-cache:/home/gradle/.gradle -v "$PWD":/home/gradle/project -w /home/gradle/project gradle:4.7.0-jdk8-alpine gradle build
ls -ltrh ./build/libs
  • gradle 缓存在此处作为卷加载。因此后续构建将重用下载的依赖项。
  • 之后,我们可以有一个 Dockerfile 来获取这个工件并生成应用程序特定的映像来运行应用程序。
  • 这样,不需要构建器映像。应用程序构建流程和应用程序运行流程分开。
  • 由于挂载了 gradle-cache 卷,我们可以在不同的 gradle 项目中重复使用下载的依赖项。

选项 2:多阶段构建


-- Dockerfile -----

FROM openjdk:8 AS TEMP_BUILD_IMAGE
ENV APP_HOME=/usr/app/
WORKDIR $APP_HOME
COPY build.gradle settings.gradle gradlew $APP_HOME
COPY gradle $APP_HOME/gradle
RUN ./gradlew build || return 0 
COPY . .
RUN ./gradlew build

FROM openjdk:8
ENV ARTIFACT_NAME=your-application.jar
ENV APP_HOME=/usr/app/
WORKDIR $APP_HOME
COPY --from=TEMP_BUILD_IMAGE $APP_HOME/build/libs/$ARTIFACT_NAME .
EXPOSE 8080
CMD ["java","-jar",$ARTIFACT_NAME]

在上面的 Dockerfile 中

  • 首先我们尝试单独复制项目的 gradle 文件,例如 build.gradle、gradlew 等,
  • 然后我们复制 gradle 目录本身
  • 然后我们尝试运行构建。在这一点上,没有其他 源代码文件存在于目录中。所以构建会失败。但 在此之前它将下载依赖项。
  • 由于我们期望 构建失败,我尝试了一种简单的技术来返回 0 并允许 docker 继续执行
  • 这将加快后续构建流程,因为所有依赖项都已下载并且 docker 缓存了这一层。相比之下,卷挂载 gradle 缓存目录仍然是最好的方法。
  • 上面的例子还展示了multi-stage docker image的构建,它避免了多个docker构建文件。

【讨论】:

  • 关于选项 1,这可能会由于 Gradle 文件锁定而导致失败。 “Gradle 进程在没有竞争的情况下会持有锁(以获得性能)。竞争是通过进程间通信宣布的,当进程在 Docker 容器中隔离时,这不起作用。”见stackoverflow.com/a/41719563/281469
  • 为什么在选项 2 中你不 gradle image?
  • 对我来说 gradlew build 失败并出现 /usr/bin/env:'sh\r': No such file or directory
  • @DanielJeney 尝试更改 gradlew 文件的行尾。我在 Windows 上遇到了同样的问题,将文件更改为 LF 工作正常。
  • 选项 #2 是正确的解决方案,因为它的行为就像您正在复制锁定文件(yarn.lock、requirements.txt)然后安装依赖项。这很直观,我一定会采用这种方法。谢谢!我什至会更好地使用./gradlew build 2&gt;/dev/null || true 来忽略错误,因为我们知道它即将到来,而true 而不是return 0,在我看来它更直观,但我想这是一个见仁见智的问题。
【解决方案2】:

在 build.gradle 中添加 resolveDependencies 任务:

task resolveDependencies {
    doLast {
        project.rootProject.allprojects.each { subProject ->
            subProject.buildscript.configurations.each { configuration ->
                configuration.resolve()
            }
            subProject.configurations.each { configuration ->
                configuration.resolve()
            }
        }
    }
}

并更新 Dockerfile:

ADD build.gradle /opt/app/
WORKDIR /opt/app
RUN gradle resolveDependencies

ADD . .

RUN gradle build -x test --parallel && \
    touch build/libs/api.jar

下面是我现在要做的:

build.gradle

ext {
    speed = project.hasProperty('speed') ? project.getProperty('speed') : false
    offlineCompile = new File("$buildDir/output/lib")
}

dependencies {
    if (speed) {
        compile fileTree(dir: offlineCompile, include: '*.jar')
    } else {
        // ...dependencies
    }
}

task downloadRepos(type: Copy) {
    from configurations.all
    into offlineCompile
}

Dockerfile

ADD build.gradle /opt/app/
WORKDIR /opt/app

RUN gradle downloadRepos

ADD . /opt/app
RUN gradle build -Pspeed=true

【讨论】:

  • 从 Gradle 3.3 开始,某些配置可能会引发IllegalStateException,这将导致构建失败。这可以通过 Gradle 3.4 中的Configuration#isCanBeResolved() 进行检查。参考:docs.gradle.org/3.4/release-notes.html
  • 选项 2 对我来说效果很好。我已经创建了示例来尝试一下(以防万一):github.com/yb172/experiments/tree/master/java-multistage
  • 选项 1 应该检查配置是否可以在循环中实际解析:if (configuration.canBeResolved) configuration.resolve()
【解决方案3】:

您可能需要考虑将应用程序映像拆分为两个映像:一个用于构建 myapp.war,另一个用于运行您的应用程序。这样,您可以在实际构建期间使用 docker 卷并将主机的 ~/.gradle 文件夹绑定到执行构建的容器中。但是,您将有更多的步骤,而不是只需要一步来运行您的应用程序。示例:

建造者形象

FROM <tag name here for base image including all build time dependencies>

# Add project Source
# -> you can use a project specific gradle.properties in your project root
# in order to override global/user gradle.properties
ADD . /var/app/myapp

RUN mkdir -p /root/.gradle
ENV HOME /root
# declare shared volume path
VOLUME /root/.gradle
WORKDIR /var/app/myapp/ 

# Compile only
CMD ["./gradlew", "war"]

应用图片

FROM <tag name here for application base image>

ADD ./ROOT.war /opt/tomcat/webapps/ROOT.war

# Start Tomcat
CMD ["/opt/tomcat/bin/catalina.sh", "run"]

如何在您的项目根目录中使用,假设构建器 Dockerfile 位于那里并且应用程序 Dockerfile 位于 webapp 子文件夹(或您喜欢的任何其他路径):

$ docker build -t builder .
$ docker run --name=build-result -v ~/.gradle/:/root/.gradle/ builder
$ docker cp build-result:/var/app/myapp/myapp.war webapp/ROOT.war
$ cd webapp
$ docker build -t application .
$ docker run -d -P application

我没有测试显示的代码,但我希望你能明白。甚至可以通过为 .gradle/ 缓存使用数据卷来改进该示例,有关详细信息,请参阅 Docker user guide

【讨论】:

  • 谢谢。只需在构建之间保留 gradle 文件夹即可显着提高后续构建的速度。 -v /opt/myapp-docker-gradle-cache:/root/.gradle
【解决方案4】:

尝试更改 gradle 用户主目录

运行 mkdir -p /opt/gradle/.gradle
ENV GRADLE_USER_HOME=/opt/gradle/.gradle

【讨论】:

    猜你喜欢
    • 2022-12-18
    • 1970-01-01
    • 1970-01-01
    • 2019-03-24
    • 2016-10-10
    • 1970-01-01
    • 1970-01-01
    • 2023-03-15
    • 2017-07-01
    相关资源
    最近更新 更多