Docker cache gradle dependencies Docker cache gradle dependencies docker docker

Docker cache gradle dependencies


I faced this issue. As you might agree, it is a best practice to download dependencies alone as a separate step while building the docker image. It becomes little tricky with gradle, since there is no direct support for downloading just dependencies.

Option 1 : Using docker-gradle Docker image


We can use pre-built gradle docker image to build the application. This ensures that it's not a local system build but a build done on a clean docker image.

docker volume create --name gradle-cachedocker run --rm -v gradle-cache:/home/gradle/.gradle -v "$PWD":/home/gradle/project -w /home/gradle/project gradle:4.7.0-jdk8-alpine gradle buildls -ltrh ./build/libs
  • gradle cache is loaded here as a volume. So subsequent builds will reuse the downloaded dependencies.
  • After this, we could have a Dockerfile to take this artifact and generate application specific image to run the application.
  • This way, the builder image is not required. Application build flow and Application run flow is separated out.
  • Since the gradle-cache volume is mounted, we could reuse the downloaded dependencies across different gradle projects.

Option 2 : Multi-stage build


----- Dockerfile -----

FROM openjdk:8 AS TEMP_BUILD_IMAGEENV APP_HOME=/usr/app/WORKDIR $APP_HOMECOPY build.gradle settings.gradle gradlew $APP_HOMECOPY gradle $APP_HOME/gradleRUN ./gradlew build || return 0 COPY . .RUN ./gradlew buildFROM openjdk:8ENV ARTIFACT_NAME=your-application.jarENV APP_HOME=/usr/app/WORKDIR $APP_HOMECOPY --from=TEMP_BUILD_IMAGE $APP_HOME/build/libs/$ARTIFACT_NAME .EXPOSE 8080CMD ["java","-jar",$ARTIFACT_NAME]

In the above Dockerfile

  • First we try to copy the project's gradle files alone, likebuild.gradle, gradlew etc.,
  • Then we copy the gradle directory itself
  • And then we try to run the build. At this point, there is no othersource code files exists in the directory. So build will fail. Butbefore that it will download the dependencies. 
  • Since we expect thebuild to fail, I have tried a simple technique to return 0 and allowthe docker to continue execution
  • this will speed up the subsequent build flows, since all the dependencies are downloaded and docker cached this layer. Comparatively, Volume mounting the gradle cache directory is still the best approach.
  • The above example also showcases multi-stage docker image building, which avoid multiple docker build files.


I

Add resolveDependencies task in build.gradle:

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

and update Dockerfile:

ADD build.gradle /opt/app/WORKDIR /opt/appRUN gradle resolveDependenciesADD . .RUN gradle build -x test --parallel && \    touch build/libs/api.jar

II

Bellow is what I do now:

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/appRUN gradle downloadReposADD . /opt/appRUN gradle build -Pspeed=true


You might want to consider splitting your application image to two images: one for building the myapp.war and the other for running your application. That way, you can use docker volumes during the actual build and bind the host's ~/.gradle folder into the container performing the build. Instead of only one step to run your application, you would have more steps, though. Example:

builder image

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.propertiesADD . /var/app/myappRUN mkdir -p /root/.gradleENV HOME /root# declare shared volume pathVOLUME /root/.gradleWORKDIR /var/app/myapp/ # Compile onlyCMD ["./gradlew", "war"]

application image

FROM <tag name here for application base image>ADD ./ROOT.war /opt/tomcat/webapps/ROOT.war# Start TomcatCMD ["/opt/tomcat/bin/catalina.sh", "run"]

How to use in your project root, assuming the builder Dockerfile is located there and the application Dockerfile is located at the webapp subfolder (or any other path you prefer):

$ 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

I haven't tested the shown code, but I hope you get the idea. The example might even be improved by using data volumes for the .gradle/ cache, see the Docker user guide for details.