Reuse Gradle results in Docker CI, even in fresh containers
A new container with an empty Gradle home can take its compile result from the cache, so containers that keep nothing still skip work. We checked this in real Linux container runs, with a different mount path and with no password passed to the container.
The short answer
- A new container with an empty Gradle home restored the compile result from the cache, even from a different folder path.
- A container started without the password still built, but ran without the cache and printed a 401 line.
- Inside one container, a password set after Gradle's background process had started was used by the next build.
What we ran
The cache server ran on a Mac, with a username and password. Gradle ran in the gradle:9.8.0-jdk21 image (Linux, aarch64, Java 21) under Docker Desktop on that same Mac. Every build was a new docker run --rm with no volumes for the Gradle home, so the container started empty and kept nothing. The project folder was mounted from the Mac. The local build cache was off. The only Gradle state shared between builds was the cache server; the image, the Docker VM and the Mac were the same for every build. Each build is one run, not an average. Each used a new copy of the project's files. Our first set of runs started from a project folder that still held build output from an earlier check on the Mac; we then repeated every run from a clean copy and got the same results.
docker run --rm -e FSCACHE_PASSWORD=<your password> \
-v "$PWD/project:/work/one" -w /work/one \
gradle:9.8.0-jdk21 gradle :core:compileJava --console=plain --no-daemon
The project is the four-module build from Gradle build cache for a monorepo. We looked at the :core:compileJava task.
| Run | What changed | Result |
|---|---|---|
| 1 | First container | Ran |
| 2 | New container, same code, same mount path | FROM-CACHE |
| 3 | New container, same code, mounted at a different path | FROM-CACHE |
| 4 | New container, a line of main code edited | Ran |
| 5 | New container, no password passed | Ran. A 401 line, then the remote cache disabled. BUILD SUCCESSFUL |
Runs 1 to 4 ended with BUILD SUCCESSFUL and printed no 401 line. The server's address was host.docker.internal, which is how a container on Docker Desktop reaches the Mac.
The settings file
This is the cache part of settings.gradle.kts we used. It reads the password from an environment variable, so the password is not in the project. We used plain HTTP because the test ran on one machine; use HTTPS across a network.
buildCache {
local { isEnabled = false }
remote<HttpBuildCache> {
url = uri("http://host.docker.internal:18250/")
isPush = true
isAllowInsecureProtocol = true
credentials { username = "ci"; password = System.getenv("FSCACHE_PASSWORD") }
}
}
What this means for CI
- In our runs no Gradle home or build output had to be kept between builds. The cache lives on the server, so an empty container is enough (runs 2 and 3).
- In our runs the folder path did not matter for this task. The project was mounted at
/work/onein run 2 and/work/twoin run 3, and both restored. - A missing password did not fail the build. Run 5 printed this and carried on without the cache, so look for it in your CI log when builds suddenly get slow (the lines in between are left out):
Could not load entry e3c6ab11d62f857d59bb3a92b7941080 from remote build cache: Loading entry from 'http://host.docker.internal:18250/e3c6ab11d62f857d59bb3a92b7941080' response status 401: Unauthorized
...
The remote build cache was disabled during the build due to errors.
For more on this error, see Gradle remote build cache authentication and 401 errors.
The last test ran two builds inside one container with Gradle's background process on. The first build had no password and printed the 401 line. Then we set the password in the same shell, deleted core/build, and the second build printed :core:compileJava FROM-CACHE. So the background process picked up the later password; no restart was needed. We saw the same in an earlier run on the Mac itself, with no container.
We did not test CI products other than GitHub Actions, a cache on another host, HTTPS, or Windows. The first runs used the Linux inside Docker Desktop on a Mac; the second set, in the note at the end, used a GitHub-hosted Linux machine.
If it does not come from the cache
Work through why the hit rate is low and Jenkins and CI build cache. For read-only developer logins, see CI writes, developers read.
Run on Gradle 9.8.0 (image gradle:9.8.0-jdk21, Linux aarch64, Java 21) with FosterStack Cache 0.2.1, Docker Desktop 4.87 on macOS (arm64).
We ran the five runs again on a GitHub-hosted Linux machine (amd64) with FosterStack Cache 0.2.2 and the same image tag (gradle:9.8.0-jdk21, its amd64 build), with the cache server on the same machine and each run on a fresh copy of the project folder. The results were the same: the 401 line appeared in run 5, run 5 ended with BUILD SUCCESSFUL, and the background-process test behaved as described above. On a Linux machine without Docker Desktop, we added --add-host=host.docker.internal:host-gateway to the docker run command so the container could reach the server by that name. Start each test build from a clean copy of the project. When we ran the same command again on a copy of the same folder, build output included, the build printed :core:compileJava UP-TO-DATE, not FROM-CACHE: Gradle found its own earlier output in the folder, so the cache was never asked and the test showed nothing. The run and its log are public.