Infrastructure worth keeping — and kept patched.

Maven remote build cache: setup, on/off switches, and Gradle on the same server

Share Maven build results across machines using Apache's Build Cache Extension and a self-hosted cache server. FosterStack Cache is that server, and one server can also serve Gradle. About ten minutes, mostly editing the files and choosing your values. The two builds in step 4 took 9 to 12 seconds together on a GitHub-hosted machine.

Who owns what

The client is Apache's Maven Build Cache Extension. We maintain the server. If your hit rate looks low, that is usually the extension's own project settings (which files count as inputs, what to skip), not the server. Start with the extension's configuration guide.

1. Add the extension

In .mvn/extensions.xml (create it if it is missing):

<extensions>
  <extension>
    <groupId>org.apache.maven.extensions</groupId>
    <artifactId>maven-build-cache-extension</artifactId>
    <version>1.2.3</version>
  </extension>
</extensions>

Check Maven Central for the current version. Pin one rather than a range. The versions we test against are the ones in our Maven doc; a version outside that list is not something we claim to have tested.

2. Point it at the server, and switch uploads on

Create .mvn/maven-build-cache-config.xml. Two details matter, and both are easy to miss:

<?xml version="1.0" encoding="UTF-8"?>
<cache xmlns="http://maven.apache.org/BUILD-CACHE-CONFIG/1.2.0"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://maven.apache.org/BUILD-CACHE-CONFIG/1.2.0 https://maven.apache.org/xsd/build-cache-config-1.2.0.xsd">
  <configuration>
    <enabled>true</enabled>
    <!-- FosterStack Cache — remote Maven build cache.
         https://github.com/fosterstack/cache -->
    <remote enabled="true" saveToRemote="true" id="fosterstack-cache">
      <url>https://cache.example.com/</url>
    </remote>
  </configuration>
</cache>

Keep the comment: a cache setting that points at an unfamiliar host looks like data theft to a reviewer without it. Keep the trailing slash on the URL. Use HTTPS for a real server.

3. Add the password, if the server has one

In ~/.m2/settings.xml, or your CI system's equivalent. The id must match the id in step 2:

<settings>
  <servers>
    <server>
      <id>fosterstack-cache</id>
      <username>maven</username>
      <password>your-password-or-CI-secret</password>
    </server>
  </servers>
</settings>

If the server has no password set, skip this step. For laptops, the server's optional read-only login lets them read the cache without being able to write to it.

4. Build, then build again

This is a real run: Maven 3.10.0, extension 1.2.3, and a 0.2.1 server, on a one-file project. First build:

mvn package

[INFO] Attempting to restore project demo:demo from build cache
[ERROR] Error downloading cache item: https://cache.example.com//v1.1/demo/demo/ebb9bceba2f7a6bc/buildinfo.xml
[INFO] Remote cache is incomplete or missing, trying local build for demo:demo
[INFO] Saved to remote cache https://cache.example.com//v1.1/demo/demo/ebb9bceba2f7a6bc/demo.jar
[INFO] Saved to remote cache https://cache.example.com//v1.1/demo/demo/ebb9bceba2f7a6bc/buildinfo.xml
[INFO] BUILD SUCCESS
[INFO] Saved to remote cache https://cache.example.com//v1.1/demo/demo/9d9dcaf9-3b6b-40fb-9cb5-da97c975eab5/build-cache-report.xml

The line starting [ERROR] Error downloading cache item is the normal first-time miss, the server saying it does not have that entry. Maven 3.9.9 logs an ordinary miss as [INFO] Cache item not found instead, so the line differs by Maven version (we saw both in our version tests). The build then ran for real and saved its result to the server. The server's status page showed it:

$ curl -s -u maven:demo-pass localhost:8080/statusz | grep -E '"store_entries"|"cache_hits"|"cache_misses"'
  "store_entries": 3,
  "cache_hits": 0,
  "cache_misses": 3,

Now delete target and the extension's local cache folder, so the only copy left is on the server, and build again:

rm -rf target ~/.m2/build-cache   # ~/.m2/build-cache is the extension's local folder, next to your local repository
mvn package

[INFO] Attempting to restore project demo:demo from build cache
[INFO] Found cached build, restoring demo:demo from cache by checksum ebb9bceba2f7a6bc
[INFO] BUILD SUCCESS

"Found cached build, restoring from cache" is the hit. Maven restored the result from the server instead of building it again.

Deleting a build-cache folder inside the project did not help in our runs. Maven used its own copy, and the server counted no hit. After we deleted ~/.m2/build-cache, the server counted the hits.

Maven 3.10 also prints a warning that the credentials for fosterstack-cache are being used for an address with no matching repository entry. In our run the build worked regardless.

If you see no hit, check that the id in the password file matches, that saveToRemote is on for the machine that built first, and that the second run starts from a clean checkout.

Enable or disable the Maven build cache extension

The Apache Maven Build Cache Extension has command-line switches to turn it off, skip the lookup, stop talking to the server, or stop uploading. In our runs, each did this:

Our advice, not a test result: which one to use.

The same job for Gradle is on enable or disable the Gradle build cache.

If you see the [ERROR] Error downloading cache item line on a cache that you know holds the result, check the address, the login and the proxy: see Maven build cache not restoring.

One build cache server for Gradle and Maven

If your company builds some projects with Gradle and others with Maven, you do not need two cache servers. FosterStack Cache serves both, with one login.

The repository

mixed-repo/
  gradle-app/    settings.gradle.kts, build.gradle.kts, gradle.properties, src/
  maven-app/     pom.xml, settings.xml, src/, .mvn/extensions.xml, .mvn/maven-build-cache-config.xml

Two small Java apps, built by two different tools.

One server, one login

FSCACHE_USERNAME=build FSCACHE_PASSWORD=<a long secret> ./fscache

Both tools send that same login, each in its own way. The secret comes from one environment variable, FSCACHE_PASSWORD.

The Gradle side

gradle-app/settings.gradle.kts, with org.gradle.caching=true in gradle.properties:

rootProject.name = "gradle-app"

// FosterStack Cache — remote Gradle build cache.
// https://github.com/fosterstack/cache
buildCache {
    remote<HttpBuildCache> {
        url = uri("https://cache.example.com/")
        isPush = true
        credentials {
            username = "build"
            password = System.getenv("FSCACHE_PASSWORD")
        }
    }
}

The Maven side

maven-app/.mvn/maven-build-cache-config.xml has the same remote block as step 2 above. In maven-app/settings.xml, the server id must match the remote's. In our run, Maven filled in the password from the same environment variable:

<settings>
  <servers>
    <server>
      <id>fosterstack-cache</id>
      <username>build</username>
      <password>${env.FSCACHE_PASSWORD}</password>
    </server>
  </servers>
</settings>

How we checked

Timed run. We followed steps 1 to 4 in a new project on a GitHub-hosted machine, with release 0.2.2, Maven 3.10.0, extension 1.2.3 and Java 21. The two builds took 11.8 seconds with an empty ~/.m2 and 8.8 seconds with one that had built before. Writing the three files took milliseconds. A runner times commands, not people, so the ten minutes is mostly editing and choosing. The run and its log are public, and the script is bin/bench-setup-pages.sh.

Setup run (step 4). Maven 3.10.0, extension 1.2.3, a 0.2.1 server and a one-file project, as described in step 4.

Switch run. A small Maven project with the extension set up as described in steps 1 to 3. Every build used a fresh copy of the project and an empty local build cache, so the only shared thing was the server. All seven builds ended with BUILD SUCCESS. Maven 3.10.0, maven-build-cache-extension 1.2.3 and FosterStack Cache 0.2.1, macOS (arm64).

BuildAdded to the commandResult
1nothing (empty server)Compiled, and saved the result to the server
2nothing (another machine)Restored from the cache, no compile
3-Dmaven.build.cache.enabled=falseCompiled. The cache was off
4-Dmaven.build.cache.skipCache=trueCompiled, although a result was stored
5-Dmaven.build.cache.remote.enabled=falseCompiled. It did not ask the server at all
6-Dmaven.build.cache.remote.save.enabled=false (empty server)Compiled. The server still had 0 entries
7same as 6, againCompiled again, because nothing was ever stored

What Maven prints for the first two switches:

enabled=false:
[INFO] Cache disabled by command line flag, project will be built fully and not cached

skipCache=true:
[INFO] Project demo:demo is marked as requiring force rebuild, will skip lookup in build cache

In our run, with remote.enabled=false there was no download attempt in the log. With remote.save.enabled=false the build still looked in the server, found nothing, and built locally.

Mixed run. A repository with a Gradle app and a Maven app, both pointed at one 0.2.1 server, on Gradle 9.8.0, Maven 3.10.0 and extension 1.2.3. Each app was built twice. For Gradle, we ran gradle clean in between. For Maven, we deleted target and the extension's local cache folder, so the only copy left was on the server. (We also turned Gradle's local cache off.)

BuildResultServer afterwards
Gradle, first:compileJava ran1 entry
Gradle, second> Task :compileJava FROM-CACHE, "1 from cache"1 hit
Maven, firstbuilt, then three lines of Saved to remote cache4 entries in all
Maven, secondFound cached build, restoring demo:maven-app from cache3 hits in all

The counts are the server's own, from its status page. Gradle stores its work under keys of a single long hex number. The Maven extension stores its files under a path. Here is what the server held on disk afterwards:

blobs/8a/8a2fb682ab19c1d434fd2d1ee3e8b27a                              (Gradle)
blobs/v1.1/demo/maven-app/3aa3eaca44f8d0fa/ma/maven-app.jar            (Maven)
blobs/v1.1/demo/maven-app/3aa3eaca44f8d0fa/bu/buildinfo.xml            (Maven)
blobs/v1.1/demo/maven-app/e17172d0-.../bu/build-cache-report.xml       (Maven)
blobs/v1.1/demo/maven-app/14c158a7-.../bu/build-cache-report.xml       (Maven)

They sit side by side in one store, under one size cap, watched by one set of metrics. See server sizing and monitoring.

Not tested: anything about speed (the apps in the mixed run are tiny, and the point was that one server handled both tools), and, in the mixed run, Gradle, Maven or extension versions other than those named above.

Mixed Gradle and Maven shop? Both can use the same server: see one server for Gradle and Maven, Gradle and Maven setup and the Gradle build cache tutorial. For CI runners, see Jenkins and CI build cache.

Gradle and Maven setup FosterStack home