Is this a good way to take advantage of layer cach...
# community-support
m
Is this a good way to take advantage of layer caching with Docker + Gradle? 1. Download Gradle. 2. Build buildSrc. 3. Build everything.
Copy code
# syntax=docker/dockerfile:1
FROM eclipse-temurin:21-jdk AS build
WORKDIR /src
COPY gradle/ gradle/
COPY gradle.properties gradlew ./
RUN ./gradlew --no-daemon --version
COPY buildSrc/ buildSrc/
RUN touch settings.gradle.kts
RUN ./gradlew --no-daemon build
COPY . .
RUN ./gradlew --no-daemon installDist

FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /src/app/build/install/app/ ./
COPY --from=build /src/docker/config/ config/
CMD ["bin/app", "server", "config/config.yml"]
Aside: it still seems like the
buildSrc
tasks take a significant, non-zero amount of time in the
RUN ./gradlew --no-daemon installDist
layer, but overall, this layer does seem to be faster if you build
buildSrc
in an earlier layer.
t
Fwiw, unless you're using a custom distribution, I would use gradle:XXX-jdk21 as a base image for the first stage (and then make sure you use
gradle
and not
gradlew
). The only downside is that you have to keep the versions in the Dockerfile and gradle/wrapper/gradle-wrapper.properties in sync. I for one probably wouldn't split the buildSrc vs. actual build but if it makes a big difference then why not… How about using a --mount=type=cache to share the dependencies (at a minimum) between builds?
t
I personally would say: you are somewhat spending a lot of effort to manually reemulate something gradle already provides. So Instead of manually coping buildSrc around, I would add
buildSrc/build
and
buildSrc/.gradle
as a cached mounts like Thomas said. That way buildsrc compilation would not get skipped not because of dockers layer cache, but gradle would skipping compiling buildSrc as it would be UP-TO-DATE. Same but differerent.. You also should add your GRADLE_USER_HOME as a cache dir. Other than that: if you really want to use dockers layer caching instead of gradle dependendency tracking, I dont think there is a correct answer, your dockerfile might work for some trivial gradle projects but might fail for more complex ones.. But its a bit intresting, gradle dockerhub page unfortunately does not have information on how to utilize cached mount effectively
m
So this is the basic template of how the build stage should look like, right? (Though you can add additional cache mounts on top of this.)
Copy code
# syntax=docker/dockerfile:1
FROM gradle:9.1.0-jdk21-noble AS build
WORKDIR /src
COPY . .
RUN --mount=type=cache,target=/home/gradle/.gradle \
    gradle --no-daemon installDist
t
"should" is a strong word here, if it works for you its ok I guess đŸ˜‰ So a few things to consider: 1. if your gradle build has build cache enabled: https://docs.gradle.org/current/userguide/build_cache.html than gradle should be able to restore buildSrc FROM-CACHE and you can skip buildSrc compilation. If your build does not have build cache enabled you will have to recompile it 2. If your gradle build has configuration cache enabled this setup probably wont utilize it (by default the configuration cache goes into
.gradle
3. if you do some include outside of your src build like
includeBuild("../xjc-gradle-plugin")
you will have to replicate them in your docker build 4. the
--no-deamon
part is not needed. if you use
--no-deamon
gradle will terminate its deamon. If you omit
--no-deamon
the deamon gets terminated by docker finishing the build 5. Docker recommends using bind mounts instead of
COPY . .
in this case you would need your bind mount writeable, unless your gradle build doesnt write anything into its src dir, which would be fairly rare but probably possible... 6. If you are building many (docker) builds in parallel this could have some funny side effects, because than these builds may or may not share a single GRADLE_USER_HOME which could or could not cause gradle to wait until a build has finished in order to get a lock
m
Perhaps it helps to split this into two scenarios: 1. CI (e.g., a job to build and push an image) 2. local dev/test (e.g., building an image with local changes, E2E tests where you build your code, deploy it to a container, and test against that container). For CI, you can probably safely bind-mount your source code, since you're working from a clean environment. You likely don't even need to think about caching either, since you're just running a single gradle command from a clean slate. For local dev/test, though, it gets more complicated. You may want to use
COPY . .
(+
.dockerignore
)--or else you can bring in a
build
folder that was built locally--but you also don't want to do a full build every time a small piece of code changes. I'm not sure it's even safe to run
installDist
locally and copy the
install
folder into a test-only image here; I'll just assume that's not safe.
t
Well not all CI Environments use clean slates, there are many ways to utilize caching of some sort. I would say one extreme is that your gradle build and cache is always trustworthy and reliable, in which case there is probably no point in using docker at all, you can just use gradle to build no matter what state you are in and it will be fine. Or you can't trust anything, like your dependency artifacts can change without version changes, your build has broken up to date checks in which case you might want to cache nothing. Anything in between is ofc also possible.
m
This seems like a well-balanced solution: 1. Build the image--either locally or in a CI job to push an image--via the normal
Dockerfile
. a. Use
COPY . .
+
Dockerfile.dockerignore
to ensure that no local build artifacts get copied into the image. b. Add some cache mounts to speed up rebuilds for local development. 2. Test the code--either locally or in a CI job--via a special
Dockerfile.test
that copies locally-built files from
build/install
(e.g., an E2E test that deploys the app and all its dependencies in a Docker network). a. It ensures you only build the code once. b. For a CI job, this is probably safe enough if the base OS is consistent, e.g.,
FROM gradle:9.1.0-jdk21-noble AS build
for
Dockerfile
and
runs-on: ubuntu-24.04
for the Github workflow. For testing, we can also use avast's docker compose plugin, e.g.:
Copy code
dockerCompose {
    useComposeFiles = listOf("$projectDir/docker-compose.yml")
    isRequiredBy(tasks.test)
}

tasks.named("composeBuild") {
    dependsOn(tasks.installDist)
}