Your first 15 minutes with a self-hosted build cache
A checklist from nothing to your first FROM-CACHE, the line Gradle prints when it reuses a stored result. The commands take 20 seconds or less; we timed them on a clean GitHub-hosted machine that had Gradle and Java. Still, plan on about 15 minutes in all, because reading, choosing your values and pointing your own project at the server take you longer than the commands.
What we timed
| Step | On a new machine, Gradle's first build | On a machine that has built with Gradle before |
|---|---|---|
| Download the release and verify its signature and checksum | 1.4 seconds | 0.5 seconds |
| Unpack it and start the server, with a password and a size cap | 0.1 seconds | 0.1 seconds |
| Write the project files | under 0.01 seconds | under 0.01 seconds |
Build, clean, build again, and see FROM-CACHE | 18.2 seconds | 7.8 seconds |
| All the steps | 19.7 seconds | 8.4 seconds |
Same commands as the checklist below, with Gradle's own cache on. With it off, as in the tutorial, they took 8.5 seconds on a machine that had built before. The run and its log are public, and so is the script.
The checklist
1. Decide where it will run
- A machine your CI runners and developers can reach. The server is one small program, and disk is what matters. See server sizing.
- Plan on HTTPS for anything beyond a first try. Gradle sends the password with every request.
2. Download it and check it is ours
VER=0.2.2
PLATFORM=darwin_arm64 # or linux_amd64, linux_arm64, darwin_amd64
gh release download v${VER} --repo fosterstack/cache \
-p checksums.txt -p checksums.txt.bundle -p "fscache_${VER}_${PLATFORM}.tar.gz"
cosign verify-blob --bundle checksums.txt.bundle \
--certificate-identity-regexp='^https://github.com/fosterstack/cache/' \
--certificate-oidc-issuer='https://token.actions.githubusercontent.com' checksums.txt
sha256sum -c <(grep "fscache_${VER}_${PLATFORM}.tar.gz" checksums.txt | grep -v sbom)
You should see Verified OK and then ...tar.gz: OK. More checks are in Verify a release yourself. Prefer a container? See Gradle build cache with Docker Compose.
3. Start it with a password and a size cap
tar xzf fscache_${VER}_${PLATFORM}.tar.gz
FSCACHE_USERNAME=gradle FSCACHE_PASSWORD=<a long secret> \
FSCACHE_MAX_BYTES=1073741824 ./fscache
That cap is 1 GiB, in bytes. Real caches are bigger; copy values from the sizing page.
4. Check it is up
curl localhost:8080/healthzshould printok.curl -u gradle:<secret> localhost:8080/statuszshould show"version": "v0.2.2".
5. Point Gradle at it
In settings.gradle.kts, and turn caching on with org.gradle.caching=true in gradle.properties:
// FosterStack Cache — remote Gradle build cache.
// https://github.com/fosterstack/cache
buildCache {
remote<HttpBuildCache> {
url = uri("http://127.0.0.1:8080/")
isPush = true
credentials {
username = "gradle"
password = System.getenv("FSCACHE_PASSWORD")
}
}
}
Use 127.0.0.1 for a first try on the same machine. On Gradle 9.8.0 the name localhost was refused without an opt-in. Maven instead? See the Maven setup.
6. Build twice and look for FROM-CACHE
export FSCACHE_PASSWORD=<a long secret>
gradle build --build-cache
gradle clean
gradle build --build-cache
> Task :compileJava FROM-CACHE
In our runs the first build printed > Task :compileJava and the second > Task :compileJava FROM-CACHE. Gradle's own cache is on in these settings, so the second build reused the copy on your machine. The server's status page showed the stored entries but 0 hits. To see the server do the work, add local { isEnabled = false } inside buildCache in settings.gradle.kts, as the tutorial below does. With it off, the status page showed one entry and one hit. For a fuller walk-through, see the Gradle build cache tutorial. If the second build does not show it, see Gradle remote build cache not working.
7. Before you roll it out to a team
- Put it behind HTTPS and give CI the read-write login and everyone else the read-only one: let CI write, developers only read.
- Watch it: monitoring. Know how it forgets: eviction.
- Know how to empty it: reset, and how to move it: back up and restore.
Gradle build cache tutorial: your first FROM-CACHE
Here is a full run, with its real output. The tutorial turns the local cache off, so a hit can only come from the server.
Start the server
This run used a password and a size cap:
FSCACHE_ADDR=127.0.0.1:8080 FSCACHE_DATA_DIR=./data \
FSCACHE_USERNAME=gradle FSCACHE_PASSWORD=demo-pass \
FSCACHE_MAX_BYTES=1073741824 ./fscache
In this run the port was 18090 instead of 8080. The outputs below do not show the port.
A small project
Four files. settings.gradle.kts says where the cache is:
rootProject.name = "demo"
// FosterStack Cache — remote Gradle build cache.
// https://github.com/fosterstack/cache
buildCache {
local { isEnabled = false } // so a hit can only come from the remote
remote<HttpBuildCache> {
url = uri("http://127.0.0.1:8080/")
isPush = true
credentials {
username = "gradle"
password = System.getenv("FSCACHE_PASSWORD")
}
}
}
Turning the local cache off is only for this test. It means a hit can only have come from the remote. build.gradle.kts is one line, plugins { java }. gradle.properties switches caching on, which the buildCache block does not do by itself:
org.gradle.caching=true
And src/main/java/demo/App.java is any small class.
Build once
export FSCACHE_PASSWORD=demo-pass
gradle build --build-cache
> Task :compileJava
> Task :processResources NO-SOURCE
> Task :classes
> Task :jar
> Task :assemble
> Task :compileTestJava NO-SOURCE
> Task :processTestResources NO-SOURCE
> Task :testClasses UP-TO-DATE
> Task :test NO-SOURCE
> Task :check UP-TO-DATE
> Task :build
BUILD SUCCESSFUL in 5s
2 actionable tasks: 2 executed
Nothing came from the cache, and every cacheable task ran. Gradle stored the compile result on the server. The status page agrees:
$ curl -s -u gradle:demo-pass localhost:8080/statusz | grep -E '"store_entries"|"cache_hits"|"cache_misses"'
"store_entries": 1,
"cache_hits": 0,
"cache_misses": 1,
Clean, and build again
gradle clean
gradle build --build-cache
> Task :compileJava FROM-CACHE
> Task :processResources NO-SOURCE
> Task :classes UP-TO-DATE
> Task :jar
> Task :assemble
> Task :compileTestJava NO-SOURCE
> Task :processTestResources NO-SOURCE
> Task :testClasses UP-TO-DATE
> Task :test NO-SOURCE
> Task :check UP-TO-DATE
> Task :build
BUILD SUCCESSFUL in 5s
2 actionable tasks: 1 executed, 1 from cache
There it is: :compileJava FROM-CACHE and "1 from cache". The server counted the hit:
$ curl -s -u gradle:demo-pass localhost:8080/statusz | grep -E '"store_entries"|"cache_hits"|"cache_misses"'
"store_entries": 1,
"cache_hits": 1,
"cache_misses": 1,
The one thing that tripped us up
Our first attempt failed before any build ran, because we used http://localhost:...:
FAILURE: Build failed with an exception.
* What went wrong:
Using insecure protocols with remote build cache, without explicit opt-in, is unsupported.
Gradle refuses plain http:// unless you set isAllowInsecureProtocol = true. With Gradle 9.8.0, the address 127.0.0.1 was allowed without it, but the name localhost was refused, with or without a credentials block. That is why the file above uses 127.0.0.1. For a real server, use HTTPS and skip the opt-in. Gradle sends the password with every request.
If your second build shows no FROM-CACHE, work through Gradle remote build cache not working. The status codes are explained in Gradle build cache HTTP status codes.
How we checked
The tutorial run used release 0.2.1 of the server and Gradle 9.8.0. The table comes from a separate run: a GitHub-hosted machine, release 0.2.2, Gradle 9.8.0, Java 21. We ran it twice, first on a machine that had never built, then again after it had.
This is a first try, not a production plan. The paid tiers are not built.