diff --git a/build-cache-docker-compose-production/index.html b/build-cache-docker-compose-production/index.html new file mode 100644 index 0000000..e711a06 --- /dev/null +++ b/build-cache-docker-compose-production/index.html @@ -0,0 +1,186 @@ + + + + + +Production build cache: Docker Compose, nginx and TLS + + + + + + + + + + + + + +
+ +
Infrastructure worth keeping — and kept patched.
+
+
+

Production build cache: Docker Compose, nginx and TLS

+

A Compose file for a shared build cache that only answers over HTTPS, with a password, behind nginx. Everything here was run for real on FosterStack Cache 0.2.1 and Gradle 9.8.0. The exact list of what was and was not tested is just below.

+ +

What was tested, and what was not

+ + +

compose.yaml

+
services:
+  fscache:
+    image: ghcr.io/fosterstack/cache:0.2.1
+    restart: unless-stopped
+    # no "ports:" on purpose: only the proxy is reachable from outside
+    volumes:
+      - fscache-data:/home/nonroot
+    environment:
+      FSCACHE_MAX_BYTES: "53687091200"      # 50 GiB
+      FSCACHE_USERNAME: gradle
+      FSCACHE_PASSWORD: change-me           # generate one: openssl rand -base64 24
+
+  proxy:
+    image: nginx:1.29-alpine
+    restart: unless-stopped
+    depends_on:
+      - fscache
+    ports:
+      - "127.0.0.1:8443:443"                # use 443:443 on a real server
+    volumes:
+      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
+      - ./certs:/etc/nginx/certs:ro
+
+volumes:
+  fscache-data:
+

The cache has no ports entry on purpose. Only the proxy is reachable from outside. Generate a real password, for example with openssl rand -base64 24, and never keep change-me.

+ +

nginx.conf

+
server {
+    listen 443 ssl;
+    server_name localhost;
+    ssl_certificate     /etc/nginx/certs/cert.pem;
+    ssl_certificate_key /etc/nginx/certs/key.pem;
+
+    # the cache accepts entries up to 1 GiB by default; nginx's own default is 1 MB
+    client_max_body_size 1g;
+
+    location / {
+        proxy_pass http://fscache:8080;
+        proxy_set_header Host $host;
+    }
+}
+

The client_max_body_size line matters. See the tests below.

+ +

What I checked, and what I saw

+ + + + + + + + + + +
CheckResult
https://localhost:8443/healthz with the certificate trustedok
The same without trusting the certificatecurl refuses (exit 60)
The cache's own port 8080 from the hostnot reachable
Plain http:// to the proxy's TLS port400
Upload with no password, through the proxy401
Upload with the password, through the proxy201, and the value reads back
Uploads of about 1 MB and 2 MB with nginx's default settings413 from nginx, not from the cache
The same uploads, and 20 MB, after client_max_body_size 1g;201 each
+

The size limit is the one to watch. nginx rejects bodies over 1 MB by default, and Gradle's cache entries are often bigger, so uploads of larger entries would be refused. The cache itself accepts up to 1 GiB per entry by default, which is why the example uses 1g. See the status codes page.

+ +

Gradle over HTTPS

+

With a certificate Gradle does not trust, the build still succeeds, but without the cache, and prints:

+
Could not load entry d4f2de693b561fd2c5d60b17a9232d6f from remote build cache: (certificate_unknown) PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
+

For my self-signed test certificate I made Java trust it:

+
keytool -importcert -alias fsprod -file certs/cert.pem -keystore trust.jks -storepass changeit -noprompt
+
+# gradle.properties
+systemProp.javax.net.ssl.trustStore=/path/to/trust.jks
+systemProp.javax.net.ssl.trustStorePassword=changeit
+

With that, the first build ran :compileJava and the second printed > Task :compileJava FROM-CACHE, through the proxy, over HTTPS, with the password. With a certificate from an authority Java already trusts you would not need the truststore lines. The Gradle side of the setup is in the tutorial.

+ +

Related: Gradle build cache with Docker Compose and who can write to a remote build cache.

+
+ Your first 15 minutes + FosterStack home +
+
+ + + diff --git a/build-cache-node-migration-checklist/index.html b/build-cache-node-migration-checklist/index.html new file mode 100644 index 0000000..b0660ab --- /dev/null +++ b/build-cache-node-migration-checklist/index.html @@ -0,0 +1,153 @@ + + + + + +Build Cache Node migration checklist for Gradle + + + + + + + + + + + + + +
+ +
Infrastructure worth keeping — and kept patched.
+
+
+

Build Cache Node migration checklist for Gradle

+

A short checklist for moving builds from the Develocity Build Cache Node to FosterStack Cache, with a real run to back it: the same project built against an old node, then against FosterStack Cache 0.2.1, then back. Read why the node is going away in Build Cache Node end of life.

+ +

What I tested

+

Gradle 9.8.0 and a small Java project, built twice against each server. The old side was a Build Cache Node image (version 21.2, built June 2025), run locally and set up to allow anonymous reads and writes for the test. The new side was a FosterStack Cache 0.2.1 server with no password. I did not test Maven in this run; for Maven see the setup page.

+ +

The checklist

+

1. Find every place your builds point at the old node

+

Search your repositories and CI settings for HttpBuildCache and for the node's address. The old node's address ends in /cache/. Write the list down: each entry is one line to change.

+ +

2. Start the new server

+

Follow your first 15 minutes. Turn on a password before other machines can reach it.

+ +

3. Change one line

+
buildCache {
+    remote<HttpBuildCache> {
+-       url = uri("http://old-node.example.com:5071/cache/")
++       url = uri("https://cache.example.com/")
+        isPush = true
+    }
+}
+

If the new server has a password, also add a credentials block. See authentication and 401 errors.

+ +

4. Expect the first build to miss

+

Nothing is copied over, and nothing needs to be: a cache holds results your builds can always rebuild. The first build on the new server runs everything and fills it; the second hits.

+ +

5. Decide about the /cache/ part of the URL

+

You can drop it, as above, or keep it. FosterStack Cache uses the whole request path as the key, so a prefix works. Entries under different paths are separate, so pick one and use it everywhere.

+ +

6. Keep the old node running until you are sure

+

With Gradle, rolling back is the same one-line change in reverse; I tested that.

+ +

7. Retire the old node, and check the new one

+

When builds are hitting the new server, stop the old node. Watch the new one with monitoring, and size its disk with server sizing.

+ +

What happened in the run

+

Each row is two builds of the same project with gradle clean in between and Gradle's own local cache turned off, so a hit can only come from the server:

+ + + + + + +
Pointed atFirst buildSecond build
The old node, URL ending in /cache/:compileJava ran:compileJava FROM-CACHE
FosterStack Cache, only the URL changed (no /cache/):compileJava ran: the new server was empty:compileJava FROM-CACHE
FosterStack Cache, URL kept with /cache/:compileJava ran: separate entries:compileJava FROM-CACHE
Back to the old node (rollback):compileJava FROM-CACHE: its entries were still therenot run
+

The new server's status page agreed: two entries stored, two hits, two misses. One small difference you may notice: a successful upload returns 200 from the old node and 201 from FosterStack Cache. Gradle accepted both.

+ +

Related: questions and answers and what to check in a replacement build cache.

+
+ Gradle and Maven setup + FosterStack home +
+
+ + + diff --git a/build-cache-permission-denied-data-directory/index.html b/build-cache-permission-denied-data-directory/index.html new file mode 100644 index 0000000..9723d3f --- /dev/null +++ b/build-cache-permission-denied-data-directory/index.html @@ -0,0 +1,137 @@ + + + + + +Build cache server exits: permission denied on its data + + + + + + + + + + + + + +
+ +
Infrastructure worth keeping — and kept patched.
+
+
+

Build cache server exits with "permission denied" on its data

+

You start the container and it stops straight away. The log ends with permission denied. The cause I reproduced is who owns the data directory. This page shows the failure and the fix, reproduced for real with the 0.2.1 image.

+ +

What you see

+

The container exits with status 1 within a few seconds, and its log says:

+
{"level":"ERROR","msg":"fscache: fatal","error":"write shutdown marker: open data/.unclean-shutdown: permission denied"}
+ +

Why it happens

+

The server does not run as root. It runs as user 65532, and it keeps its files in a data folder inside /home/nonroot. If that folder already exists and belongs to someone else, the server cannot write to it and stops.

+

I reproduced it with a volume that already held data owned by root, which is what you get after restoring a backup as root:

+
/d       0:0   drwxr-xr-x      (owner:group, mode)
+/d/data  0:0   drwxr-xr-x
+ +

The fix

+

Give the whole tree to user 65532. The image has no shell, so use a throwaway container to do it:

+
docker run --rm -v fscache-data:/d ubuntu:24.04 chown -R 65532:65532 /d
+

After that the same image started normally: /healthz answered ok and an upload returned 201.

+ +

What does not cause it

+ + +

What I did not test

+ + +

Related: back up and restore a build cache, where the restore step has to keep the ownership, and Gradle build cache with Docker Compose.

+
+ Your first 15 minutes + FosterStack home +
+
+ + + diff --git a/gradle-build-cache-corporate-proxy/index.html b/gradle-build-cache-corporate-proxy/index.html new file mode 100644 index 0000000..af5a242 --- /dev/null +++ b/gradle-build-cache-corporate-proxy/index.html @@ -0,0 +1,157 @@ + + + + + +Gradle build cache behind a corporate proxy + + + + + + + + + + + + + +
+ +
Infrastructure worth keeping — and kept patched.
+
+
+

Gradle build cache behind a corporate proxy

+

Many companies make builds reach the network through a proxy. This page shows how Gradle's remote build cache behaves behind one: the settings, the ways it goes wrong, and how to tell a proxy problem from a cache problem. Everything was run for real with Gradle 9.8.0 and FosterStack Cache 0.2.1.

+ +

What was tested, and what was not

+ + +

The settings

+

Gradle takes proxy settings from Java system properties. Put them in ~/.gradle/gradle.properties or the project's gradle.properties, with the systemProp. prefix:

+
systemProp.http.proxyHost=proxy.example.com
+systemProp.http.proxyPort=3128
+systemProp.https.proxyHost=proxy.example.com
+systemProp.https.proxyPort=3128
+systemProp.http.nonProxyHosts=cache.internal.example.com|localhost|127.*
+

Use the https pair for an https:// cache URL and the http pair for plain HTTP. The last line lists hosts that should skip the proxy.

+ +

What I saw

+

For the plain-HTTP rows I set isAllowInsecureProtocol = true, which Gradle requires for non-loopback http://. That is for the test rig only; use HTTPS for real.

+ + + + + + + + +
SetupResultWhat the proxy logged
No proxy settings, cache at <cache-ip>second build FROM-CACHEnothing (not used)
Proxy settings, same plain-HTTP URLFROM-CACHEGET http://<cache-ip>:18130/<key>, one per build
Proxy settings, but the cache URL is 127.0.0.1FROM-CACHEnothing: Java skips the proxy for loopback addresses
Proxy settings, with the cache's address in nonProxyHostsFROM-CACHEnothing
Proxy settings, HTTPS cache, tunnel allowedFROM-CACHECONNECT <cache-ip>:18443, one per build
Proxy settings, proxy refuses the cache hostthe build succeeds but without the cache (log below)the request, answered 403
+ +

Three things that catch people

+

1. Testing against localhost skips the proxy. With the cache at 127.0.0.1, Gradle never used the proxy even though it was configured. A setup that works on your laptop against a local cache proves nothing about the proxy path. Test against the real address.

+

2. A refusing proxy looks like a cache error. When the proxy answered 403 to the tunnel, Gradle printed:

+
Could not load entry d4f2de693b561fd2c5d60b17a9232d6f from remote build cache: Loading entry from 'https://<cache-ip>:18443/d4f2de693b561fd2c5d60b17a9232d6f' response status 403: Forbidden
+

That is the same text you would get from the cache refusing a write with the read-only login (see the status codes). To tell them apart, take Gradle out of it and try the same route with curl:

+
$ curl -sS -x http://proxy.example.com:3128 https://cache.example.com/healthz
+curl: (56) CONNECT tunnel failed, response 403      # the proxy said no
+$ curl -sS --noproxy '*' https://cache.example.com/healthz
+ok                                                   # the cache is fine
+

I substituted example hostnames in those commands; the two output lines are the real ones, from my test proxy refusing and from the cache answering directly. If the first one fails and the second works, ask whoever runs the proxy to allow the cache's host and port.

+

3. Over HTTPS the proxy only sees the address. A CONNECT tunnel shows the proxy the host and port and nothing inside, so the password and the cache keys are not visible to it. That also means any rule on the path cannot be applied. This is how the proxy log looked in my run: one CONNECT line per build.

+ +

Checklist

+ + +

Related: Gradle remote build cache not working and authentication and 401 errors.

+
+ Gradle build cache tutorial + FosterStack home +
+
+ + + diff --git a/maven-build-cache-corporate-proxy/index.html b/maven-build-cache-corporate-proxy/index.html new file mode 100644 index 0000000..193171e --- /dev/null +++ b/maven-build-cache-corporate-proxy/index.html @@ -0,0 +1,171 @@ + + + + + +Maven build cache extension behind a corporate proxy + + + + + + + + + + + + + +
+ +
Infrastructure worth keeping — and kept patched.
+
+
+

Maven build cache extension behind a corporate proxy

+

Maven has its own proxy settings, and the Apache Build Cache Extension follows them. This page shows what the extension sent through a proxy, how it differs from Gradle, and what you see when the proxy says no. All of it was run for real with Maven 3.10.0, extension 1.2.3 and FosterStack Cache 0.2.1.

+ +

What was tested, and what was not

+ + +

The settings

+

In ~/.m2/settings.xml, next to the cache's password entry:

+
<settings>
+  <servers>
+    <server>
+      <id>fosterstack-cache</id>
+      <username>maven</username>
+      <password>your-password-or-CI-secret</password>
+    </server>
+  </servers>
+  <proxies>
+    <proxy>
+      <id>corp</id>
+      <active>true</active>
+      <protocol>http</protocol>
+      <host>proxy.example.com</host>
+      <port>3128</port>
+      <nonProxyHosts>cache.internal.example.com|localhost</nonProxyHosts>
+    </proxy>
+  </proxies>
+</settings>
+

The cache itself is set up as in the Maven setup. The server id must still match the remote's.

+ +

What went through the proxy

+

With the proxy configured, all of the extension's cache traffic went through it: the lookups, the download that restores a build, and the uploads. This is what the proxy logged for two builds, the first filling the cache and the second restoring from it (the proxy also saw Maven's own traffic to repository.apache.org, which is not shown):

+
build 1 (stores)
+GET http://<cache-ip>:18132/v1.1/demo/demo/<id>/buildinfo.xml
+GET http://<cache-ip>:18132/v1.1/demo/demo/<id>/buildinfo.xml
+PUT http://<cache-ip>:18132/v1.1/demo/demo/<id>/demo.jar
+PUT http://<cache-ip>:18132/v1.1/demo/demo/<id>/buildinfo.xml
+PUT http://<cache-ip>:18132/v1.1/demo/demo/<id>/build-cache-report.xml
+
+build 2 (restores)
+GET http://<cache-ip>:18132/v1.1/demo/demo/<id>/buildinfo.xml
+GET http://<cache-ip>:18132/v1.1/demo/demo/<id>/buildinfo.xml
+GET http://<cache-ip>:18132/v1.1/demo/demo/<id>/demo.jar
+PUT http://<cache-ip>:18132/v1.1/demo/demo/<id>/build-cache-report.xml
+

The second build logged Found cached build, restoring demo:demo from cache.

+ +

Other cases

+ + + + + + +
SetupWhat happened
Proxy configured, cache URL is 127.0.0.1Still went through the proxy (4 requests logged). Unlike Gradle, Maven does not skip the proxy for loopback.
Proxy configured, the cache's address in <nonProxyHosts>Nothing reached the proxy. The build still restored from the cache.
Proxy refuses the cache's hostNothing was stored or restored. The build succeeded without the cache, and logged Error downloading cache item with HTTP Status: 403.
Proxy not running at allThe lookup failed with Connect to 127.0.0.1:18131 ... Connection refused. The build succeeded without the cache, and nothing was uploaded.
+ +

Things that catch people

+ + +

Related: Maven build cache extension not restoring and a production setup with TLS.

+
+ Maven remote cache setup + FosterStack home +
+
+ + + diff --git a/sitemap.xml b/sitemap.xml index 315bc16..bb5703c 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -81,4 +81,28 @@ https://fosterstack.com/first-15-minutes-build-cache/ 2026-10-08 + + https://fosterstack.com/build-cache-node-migration-checklist/ + 2026-10-08 + + + https://fosterstack.com/build-cache-docker-compose-production/ + 2026-10-08 + + + https://fosterstack.com/gradle-build-cache-corporate-proxy/ + 2026-10-08 + + + https://fosterstack.com/upgrade-build-cache-server/ + 2026-10-08 + + + https://fosterstack.com/build-cache-permission-denied-data-directory/ + 2026-10-08 + + + https://fosterstack.com/maven-build-cache-corporate-proxy/ + 2026-10-08 + diff --git a/upgrade-build-cache-server/index.html b/upgrade-build-cache-server/index.html new file mode 100644 index 0000000..81e0cc3 --- /dev/null +++ b/upgrade-build-cache-server/index.html @@ -0,0 +1,143 @@ + + + + + +Upgrade a self-hosted build cache and keep its data + + + + + + + + + + + + + +
+ +
Infrastructure worth keeping — and kept patched.
+
+
+

Upgrade a self-hosted build cache and keep its data

+

Can you move a build cache to a newer release without losing what it holds? I tested it with the only two published releases: I stored entries on 0.1.0, started 0.2.1 on the same data volume, wrote more, and then went back to 0.1.0. This is what happened.

+ +

What was tested, and what was not

+ + +

The upgrade

+
docker pull ghcr.io/fosterstack/cache:0.2.1
+docker stop fscache && docker rm fscache
+docker run -d --name fscache -p 127.0.0.1:8080:8080 \
+  -v fscache-data:/home/nonroot \
+  ghcr.io/fosterstack/cache:0.2.1
+

Use the same volume and the same settings (environment variables for the password and size cap) as the old container. Pin the exact tag you want, as above, rather than latest.

+ +

What I saw

+ + + + + + +
StepResult
0.1.0: store three entries on a fresh volume201 each; its metrics show 3 entries, 29 bytes
Stop 0.1.0, start 0.2.1 on the same volumestarts normally; the status page shows "version": "v0.2.1", 3 entries, 29 bytes; all three values read back
0.2.1: store two more201 each; 5 entries
Stop 0.2.1, start 0.1.0 on the same volume againstarts normally; all values read back, including the two written by 0.2.1
+

So in my test an upgrade lost nothing, and a rollback to the old release could still read what the new one had written.

+ +

Two differences between the releases

+ + +

If you want a safety net

+

A build cache can always be rebuilt, so losing one costs time, not work. If refilling would hurt, take a copy first: back up and restore a build cache. To start clean instead: clear or reset a build cache.

+ +

Related: how releases are built and signed and verify a release yourself, so you can check the new version before you run it.

+
+ Changelog + FosterStack home +
+
+ + +