Infrastructure worth keeping — and kept patched.

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

StepOn a new machine, Gradle's first buildOn a machine that has built with Gradle before
Download the release and verify its signature and checksum1.4 seconds0.5 seconds
Unpack it and start the server, with a password and a size cap0.1 seconds0.1 seconds
Write the project filesunder 0.01 secondsunder 0.01 seconds
Build, clean, build again, and see FROM-CACHE18.2 seconds7.8 seconds
All the steps19.7 seconds8.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

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

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

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.

Gradle build cache tutorial FosterStack home