+
+
+Install a build cache on an air-gapped network
+
+
+
+
+
+
+
+
+
+
fosterstack
+
Infrastructure worth keeping — and kept patched.
+
+
+
Install a build cache on an air-gapped network
+
The server is one static program with nothing to download at install time and nothing it reaches out for at startup. So an air-gapped install is short: move the files across, verify them, run.
+
+
What to bring across
+
On a machine with internet access, download these from the release page:
+
+
The archive for your platform, for example fscache_X.Y.Z_linux_amd64.tar.gz.
+
checksums.txt and checksums.txt.bundle, which is the signature over the checksums.
+
cosign, the signature checker. It is one static binary and will not be on a fresh isolated machine.
+
If you run containers: the image as a tar file, for example crane pull ghcr.io/fosterstack/cache:X.Y.Z fscache-X.Y.Z.tar. Verify the image with cosign verify before you export it, while you still have registry access (the image verification guide has the command). Images are published for both linux/amd64 and linux/arm64. Across a gap you pick one by hand, so pull on a machine of the destination type, or add --platform.
+
+
+
Verify before you cross the gap
+
Do this on the connected side, where the signature check can reach its public log:
+
# 1. Prove the checksums file is ours
+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
+
+# 2. Prove your archive matches it
+sha256sum -c <(grep "fscache_X.Y.Z_linux_amd64.tar.gz" checksums.txt)
+
Both steps matter. The checksum alone only shows the archive matches a file you carried beside it. The signature is what ties that file to us.
+
+
Install on the isolated side
+
For the binary: unpack the archive and follow the install guide from the unpack step, including the systemd service. There is no separate offline procedure.
Real deployments terminate HTTPS at a proxy in front of the server, because Gradle sends the password with every request. The offline install guide has the full detail, including what to do for later upgrades.
+
+
It does not call out
+
The server sends no telemetry and has no update or licence check, so it behaves the same with no route to the internet.
Tiny and quiet. A distroless image with one static program and no shell. It sends no telemetry and makes no outbound connections.
A FIPS build. A FIPS variant uses Go's validated FIPS 140-3 cryptographic module.
Patched for everyone. We aim to ship dependency fixes within 48 hours of disclosure. That is a target, not a promise. Security patches are never withheld from the free tier.
Build Cache Node replacement: questions and answers
+
Short answers about the end of the free Build Cache Node and about FosterStack Cache.
+
+
What is happening to the Develocity Build Cache Node?
+
Gradle Inc. says the free standalone Build Cache Node "will no longer be distributed, supported or available after December 31, 2026." Read the vendor's official notice. We have a short page on what that means.
+
+
Do I have to move my cached data?
+
No. A build cache only holds results your build can always rebuild. Start the new server empty and your first builds fill it. If the cache is down or empty, builds are slower, not wrong.
+
+
What is FosterStack Cache?
+
A self-hosted remote build cache. It is one static binary or one container image, MIT licensed. It speaks Gradle's own HttpBuildCache protocol, and also the remote mode of Apache's Maven Build Cache Extension.
+
+
What makes FosterStack Cache's security different?
+
Every release is signed and carries build provenance, an SBOM and published vulnerability statements. Two independent scanners check each release candidate before it ships, and every published image is scanned again daily. Images are minimal, with one static program and no shell, and the server sends no telemetry. See the security policy and how we build.
+
+
Does it work with Gradle?
+
Yes. Gradle talks to it with its normal remote build cache setting, so moving from the old node is mostly a URL change. Our CI builds a real multi-module Gradle project against it on every change and checks that a second build gets real cache hits. See the setup page.
+
+
Does it work with Maven?
+
Yes, through the Apache Maven Build Cache Extension. The Maven client is Apache's Build Cache Extension, not ours.
+
+
Is it free?
+
Yes. Everything you can download today is the free MIT core, including eviction, size limits and metrics. Paid tiers are described for the future but are not built or sold yet.
+
+
Is it ready for production?
+
It is early. A cache fails safe, since a missing cache means slower builds, but decide for yourself with that in mind. The README lists what has shipped and what has not.
+
+
Who can write to the cache?
+
Anyone holding the read-write password. Turn on Basic Auth anywhere untrusted people could reach the port, and share that password only with CI and developers you trust to change your code. You can also set an optional read-only login for everyone else: it can read but any write is refused. Telling Gradle not to upload is a client choice, not a server rule. Per-person identity is not built.
+
+
Does it have a web page like the old node?
+
No settings page, on purpose. You configure it with flags and environment variables. A read-only status page shows version, size, entries and hit counts, and metrics are available for Prometheus. To empty the cache, remove its volume and start again.
+
+
How do I check a release is the real one?
+
Container images are signed and releases carry build provenance, a software bill of materials and vulnerability statements. The commands are under "Verifying a release" in RELEASING.md. No account is needed for the signature check.
+
+
Does the server send anything out?
+
No. The server sends no telemetry and needs no connection to FosterStack. It can run on a network with no route to the internet; see the disconnected network guide.
+
+
Can it store its data in S3 or other object storage?
+
No. It stores cache entries on a local file system volume, with a small index file beside them. It runs as a single instance, so object storage is not used or supported today. Size the volume from the cache size cap. See server sizing.
+
+
Can I run it on Kubernetes?
+
Yes, with plain manifests in the Kubernetes guide. There is no Helm chart yet.
+
+
+
+
diff --git a/build-cache-server-sizing/index.html b/build-cache-server-sizing/index.html
new file mode 100644
index 0000000..ce8cbfb
--- /dev/null
+++ b/build-cache-server-sizing/index.html
@@ -0,0 +1,131 @@
+
+
+
+
+
+How big a server does a build cache need? CPU, RAM and disk
+
+
+
+
+
+
+
+
+
+
fosterstack
+
Infrastructure worth keeping — and kept patched.
+
+
+
How big a server does a build cache need? CPU, RAM and disk
+
You are sizing a disk, not a compute tier. The server is one small program that idles in tens of megabytes, so the question is how much cache you want to keep.
+
+
Starting sizes
+
+
vCPU
RAM
Disk
+
Absolute minimum
1
1 GB
cache cap + about 5 GB for the system
+
Recommended start, about 5 to 20 developers plus CI
1 to 2
2 GB
25 to 50 GB SSD
+
Larger CI farms
2 to 4
4 to 8 GB
cache cap + about 20%
+
+
These are starting points from our deployment guide. Your build graph decides the rest. It is hard to be limited by CPU, because the busy path is reading and writing stored files, not computing.
+
+
Set a size cap
+
Choose how much cache you want, set it with FSCACHE_MAX_BYTES, and make the disk about 20% bigger. When the cache reaches the cap it removes the least recently used entries and never grows past it. The extra room covers file system overhead and uploads in progress.
+
The value is in bytes. It is easy to be wrong by a factor of 1024, so copy one:
+
+
Cap
FSCACHE_MAX_BYTES
+
10 GiB
10737418240
+
20 GiB
21474836480
+
50 GiB
53687091200
+
100 GiB
107374182400
+
+
+
Two mistakes that look like "the cache doesn't help"
+
+
A cap that is far too small, such as 20 MiB when you meant 20 GiB, makes it evict constantly, so the hit rate never climbs. The status page at /statusz shows the cap it read next to current usage.
+
Leaving the cap at its default of 0, which means no limit, on a small disk is the one setup that will fill the disk.
+
+
+
Put it near the runners
+
On bigger setups the network matters more than the processor. A small server in the same region, ideally the same network, as your CI runners beats a large one far away.
The release notes from GitHub, quoted as published. Newest first. For anything else, the releases page is the source.
+
+
v0.2.1
+
Release name: fscache v0.2.1. Published 2026-09-21.
+
See release-manifest.json for the full evidence bundle: image digests (GHCR canonical, Docker Hub mirror — identical digests), archive checksums, requirements baseline, per-AC acceptance results, and the Rekor log index of every verified statement in the release chain.
No release was published for v0.2.0: the tag exists, but its release run failed. Use v0.2.1.
+
+
v0.1.0
+
Published 2026-08-22.
+
+
The v0.1.0 notes begin with a list of the nine commits in the release. That list is on the GitHub release page. The rest of the notes follow here, unedited.
+
Note on this release's evidence (added Sep 2026). v0.1.0 was built and published by the pre-remediation release pipeline: a snapshot build was scanned, and a separate build of the same commit was published, so these published images were not scanned before publication. They have been rescanned since at every severity with the published VEX applied — see the rescan runs for verdicts against the exact published digests. The signatures and provenance on this release are accurate about what they say — which CI run produced which digest — and say nothing about scanning. The next release is the first to carry the full evidence chain: one build, scanned and tested by digest, promoted without a rebuild.
What changed since: the v0.2.1 release notes point to its release-manifest.json evidence bundle. The release process, as RELEASING.md describes it, is "One build, one set of digests, promoted without a rebuild", with every scanner run "against the exact candidate digests".
+
+
+
+
diff --git a/fips-140-3-build-cache/index.html b/fips-140-3-build-cache/index.html
new file mode 100644
index 0000000..7518525
--- /dev/null
+++ b/fips-140-3-build-cache/index.html
@@ -0,0 +1,132 @@
+
+
+
+
+
+FIPS 140-3 build cache: run the FIPS image and confirm it
+
+
+
+
+
+
+
+
+
+
fosterstack
+
Infrastructure worth keeping — and kept patched.
+
+
+
FIPS 140-3 build cache: run the FIPS image and confirm it
+
FosterStack Cache has a FIPS build that uses Go's validated FIPS 140-3 cryptographic module. This page shows how to run it and how to see for yourself that it is in FIPS mode. The outputs below are from the real 0.2.1 images.
+
+
What the FIPS build is
+
It is the same source as the standard build, compiled so that it uses Go's validated FIPS 140-3 module (CMVP certificate #5247). It is available as a container image tag ending in -fips, and as Linux binaries for amd64 and arm64. There is no macOS FIPS build.
+
+
Run it
+
docker run -d -p 127.0.0.1:8080:8080 -v fscache-data:/home/nonroot \
+ ghcr.io/fosterstack/cache:0.2.1-fips
+
Pin an exact version, as here, rather than a floating tag. To check you pulled the real image, use the steps in Verify a release yourself. The FIPS image's digest in the release manifest for 0.2.1 is sha256:e125fe3f6ed2c18c478453300ba2b787a42999edf66a39403877efb39865ab1b, and that is what the pull returned.
+
+
Confirm it is in FIPS mode
+
The build says so when it starts, so the property can be seen and not just trusted:
It shows the running program is using Go's validated module. The validation is of that cryptographic module.
+
Our own tests run the whole suite against the FIPS build with the module set to refuse any algorithm it does not approve. That proves the code paths the tests reach. It does not exercise the released FIPS image itself, which no test currently runs.
+
FosterStack Cache is not "FedRAMP compliant" or "CMMC compliant". Those attach to your service and your organisation, never to a component you deploy. The FIPS build is validated crypto for your compliance program, not a substitute for it.
Run a shared Gradle build cache for your team in one Compose file, with a password and a size limit. This is the shape we suggest once you go past a first trial.
+
+
Try it first, on one machine
+
docker run -d -p 127.0.0.1:8080:8080 -v fscache-data:/home/nonroot ghcr.io/fosterstack/cache:latest
+
The 127.0.0.1: keeps it reachable from this machine only. A bare -p 8080:8080 opens the port to every network interface, and on a cloud host that is a cache anyone can write to the moment the command returns. Pin a version tag instead of latest for anything real.
+
+
The Compose file
+
This one is open to your CI runners, so it has a password on purpose. A cache that untrusted people can write to can feed files into your builds.
+
services:
+ fscache:
+ image: ghcr.io/fosterstack/cache:latest # pin a version tag
+ restart: unless-stopped
+ ports:
+ - "8080:8080" # all interfaces, so auth is on
+ volumes:
+ - fscache-data:/home/nonroot
+ environment:
+ FSCACHE_MAX_BYTES: "53687091200" # 50 GiB, size to your CI volume
+ FSCACHE_USERNAME: gradle
+ FSCACHE_PASSWORD: change-me # generate one: openssl rand -base64 24
+
+volumes:
+ fscache-data:
+
+
The data volume mounts at /home/nonroot, not /data. The server runs as a non-root user.
+
FSCACHE_MAX_BYTES is in bytes. 50 GiB is 53687091200. See server sizing for more values.
+
Put an HTTPS proxy in front for real use. Gradle sends the password with every request.
+
+
+
Check it works, with no Gradle project
+
curl -s localhost:8080/healthz # ok
+curl -s -X PUT --data-binary 'hello' localhost:8080/testkey123 # 201
+curl -s localhost:8080/testkey123 # hello
+
Add -u gradle:your-password to those calls once you have set a password.
The container image has no shell, so there is no health check inside the Compose file. Probe /healthz from outside: your orchestrator's HTTP check, a monitor, or curl -f http://localhost:8080/healthz from the host.
Gradle build cache HTTP status codes: 401, 403, 404 and 413
+
Gradle talks to the cache with plain web requests, and it does not fail the build when the cache says no. So the status code is often the only clue. This is what each one means for a FosterStack Cache server.
+
+
The short table
+
+
Status
When you get it
What to do
+
200
A stored entry was found and sent.
Nothing. This is a cache hit.
+
201
An upload was stored.
Nothing.
+
404
That key is not stored.
Usually nothing. It is a cache miss. With a correct password it means the key just is not there yet.
+
401
No password, or the wrong one.
Fix the credentials block. See below.
+
403
A write sent with the read-only login.
Use the read-write login for machines that upload, or set the client not to upload.
+
413
An upload bigger than the server's size limit for one entry.
Raise FSCACHE_MAX_BODY_BYTES if the entry is meant to be cached.
+
+
+
Tested on a real server
+
These were all run against a 0.2.1 server started with a read-write login, a read-only login, and the body limit set to 1024 bytes. Your numbers for the limit will differ, since the default is 1 GiB.
+
GET /healthz (no credentials) 200
+GET /metrics (no credentials) 200
+GET key, no credentials 401
+GET key, wrong password 401
+GET missing key, read-write login 404
+PUT 5 bytes, read-write login 201
+GET stored key, read-write login 200
+GET stored key, read-only login 200
+HEAD stored key, read-only login 200
+PUT, no credentials 401
+PUT, read-only login 403
+PUT 2048 bytes (limit set to 1024), read-write login 413
+GET the too-large key afterwards 404
+/statusz, no credentials 401
+
The health and metrics paths stay open without a password, so monitoring works. The status page does not.
+
+
401 is the quiet one
+
Gradle treats a remote cache failure as non-fatal, so a 401 does not break the build and shows nothing at the default log level. The build just never uses the cache. Run with -i to see it. The usual causes are no credentials block, a password that did not reach Gradle's background daemon, or a typo. To test the server directly:
A 401 here means the credentials are wrong. A 404 means they are right and the key simply is not stored. More steps are in Gradle remote build cache not working.
+
+
403 means the login worked but may not write
+
The server can have an optional read-only login. A client holding it can read (GET and HEAD return 200 as normal) but any write gets a 403. That is the intended way to give laptops a cache they can read and not change. See who can write to a remote build cache.
+
+
413 and entry size
+
A single upload over the limit is refused with 413 and nothing is stored, so a later request for that key is a 404. The limit is FSCACHE_MAX_BODY_BYTES and defaults to 1 GiB per entry.
Gradle build cache on Kubernetes: plain manifests, no Helm chart yet
+
Run a shared build cache in your cluster with plain manifests: one Deployment, one volume, one Service. There is no Helm chart yet. It has not been through a beta on a production cluster, so treat it that way.
+
+
Is there a Helm chart?
+
Not yet. Today the supported way to deploy on Kubernetes is the plain manifests in the Kubernetes guide: a Secret, a volume claim, a Deployment and a Service. Copy them into one file and apply it with kubectl apply -f.
+
+
Run exactly one replica
+
Do not scale this out. The cache keeps its index in a database file that one process holds a lock on. A second pod on the same volume does not share the cache; it waits on the lock or fails to start. Two pods on different volumes give you two half-filled caches.
+
+
replicas: 1.
+
Update strategy Recreate, not the default rolling update, so the new pod never starts while the old one holds the lock.
+
A volume claim with ReadWriteOnce.
+
+
A cache is safe to lose for a moment. If it is down during a rollout, builds miss the cache and run slower, and nothing breaks. High availability across nodes is a paid-tier idea that is not built, so do not improvise it with two replicas.
+
+
Size it in Kubernetes terms
+
+
Requests of about 100m CPU and 128Mi memory, and a memory limit of 512Mi.
+
No CPU limit, on purpose. A CPU limit throttles instead of killing, and throttling a cache during a burst is the opposite of what you want.
+
A node with 1 GB does not have 1 GB for you. The smallest practical node on managed Kubernetes is 2 GB.
+
Disk is the real variable. Size the volume from the cache size cap plus some room. See server sizing.
+
+
+
Locked down by default
+
The manifests run as a non-root user with a read-only root file system, drop all capabilities, forbid privilege escalation, and use the runtime default seccomp profile. There is no privileged mode, no host networking and no host path. They satisfy the Pod Security restricted profile. The volume must be writable by user 65532, which is what the fsGroup setting is for.
+
+
Runners in the same cluster
+
If your CI runners already run in this cluster, including Jenkins agents, GitHub Actions runner controller and GitLab runners, you need no load balancer and no ingress. Point Gradle at the cluster-internal name, for example http://fscache.fscache-ns.svc.cluster.local/, with isAllowInsecureProtocol = true. Gradle requires that for plain http:// addresses. In our test with Gradle 9.8.0, only the literal 127.0.0.1 was let through without it. Here it is reasonable because the traffic never leaves the cluster network. See Jenkins and CI build cache.
+
+
Get the manifests
+
The four manifests (Secret, volume claim, Deployment, Service) and the steps to reach the cache from outside the cluster are in the Kubernetes guide on GitHub. Copy them into one file and apply it.
+
+
+
+
diff --git a/gradle-build-cache-tutorial/index.html b/gradle-build-cache-tutorial/index.html
new file mode 100644
index 0000000..40fb942
--- /dev/null
+++ b/gradle-build-cache-tutorial/index.html
@@ -0,0 +1,184 @@
+
+
+
+
+
+Gradle build cache tutorial: see FROM-CACHE with a self-hosted cache
+
+
+
+
+
+
+
+
+
+
fosterstack
+
Infrastructure worth keeping — and kept patched.
+
+
+
Gradle build cache tutorial: see FROM-CACHE with a self-hosted cache
+
Start a cache server, point a small Gradle project at it, build twice, and watch the second build take its work from the cache. Everything below was run for real: release 0.2.1 of the server and Gradle 9.8.0.
+
+
1. Start the server
+
Download and verify the release as shown in Verify a release yourself, unpack it, and run it with a password and a size cap:
Prefer a container? Gradle build cache with Docker Compose has the same settings. (My test run used port 18090 instead of 8080; the outputs below do not show the port.)
+
+
2. 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.
My first attempt failed before any build ran, because I 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.
Gradle remote build cache not working: no FROM-CACHE
+
Your build runs, nothing fails, and nothing comes from the remote cache. Gradle treats a broken remote cache as non-fatal, so it fails quietly. Work down this list.
+
+
1. Is the build cache switched on?
+
The buildCache block in settings.gradle(.kts) says where the cache is. It does not turn caching on. Without this line the block does nothing:
+
# gradle.properties
+org.gradle.caching=true
+
Or pass --build-cache on each run. This is the most common cause.
+
+
2. Is the local cache hiding the problem?
+
Gradle's local cache is on by default and gives hits even when the remote is failing. So FROM-CACHE on its own does not prove the remote works. Take the local cache out of the picture and show remote errors:
Run it twice. The second run is the one that tells you something.
+
+
3. Is the password reaching Gradle?
+
If the server uses a password and Gradle sends none, every request gets a 401, silently. Two things to check:
+
+
Keep a credentials block in the config. A server with no password ignores it, so one config works everywhere.
+
Gradle's background daemon reads its environment once, when it starts. A password you exported weeks ago can stop working after a reboot. Run ./gradlew --stop, or put the password in ~/.gradle/gradle.properties (never in the repo).
+
+
To test the server's password directly, a 401 means the credentials are wrong and a 404 means they are right and the key just is not there yet:
Keep the trailing slash on the URL. Gradle adds the cache key straight onto it. Gradle also refuses plain http:// unless you set isAllowInsecureProtocol = true. In our test with Gradle 9.8.0, the address 127.0.0.1 was allowed without it, but the name localhost was refused. Use HTTPS in real deployments, because Gradle sends the password with every request.
+
One trap in test setups: some sample projects run a check that fails any build containing an http:// URL, including the cache URL. That is the check, not the cache.
Run a build and run this again. If neither counter moves, the requests are not arriving. If misses move and hits do not, the cache is working and has not been filled yet, or something in the task inputs changes on every build.
+
+
6. Is this client allowed to upload?
+
If you set isPush to false, or only to true on CI, a laptop only reads. Nothing fills the cache until a machine that may upload has run the build.
+
+
Not the same thing: the configuration cache
+
Gradle may suggest enabling its "configuration cache". That is a different, local feature. It has nothing to do with a remote build cache.
+
+
+
+
diff --git a/how-we-build/index.html b/how-we-build/index.html
new file mode 100644
index 0000000..0157ca3
--- /dev/null
+++ b/how-we-build/index.html
@@ -0,0 +1,138 @@
+
+
+
+
+
+How FosterStack Cache is built and signed
+
+
+
+
+
+
+
+
+
+
fosterstack
+
Infrastructure worth keeping — and kept patched.
+
+
+
How FosterStack Cache is built and signed
+
Every release is built by an automated pipeline, checked, signed, and published with files you can verify yourself. This page says what that means today.
+
+
Releases are built only in CI
+
No release file is ever made on a person's laptop, ours included. A release starts when a signed tag is pushed, and there is no manual release path. That is what makes the signatures mean something: nobody can forge a release that GitHub's own build machines never built.
+
+
The steps a release goes through
+
+
Source check. The tag and the commit must be signed by an allowed signer, sit on the main branch, and have every required check green.
+
Build. The binaries are built, with checksums and a software bill of materials for each archive.
+
Container images. Each image is assembled once, from the unmodified distroless base plus one binary.
+
Reproducibility check. The image is assembled a second time on a separate machine. The results must be identical, or the release fails.
+
Scans and tests. Every scanner on our list runs against the exact images. A finding blocks the release unless a published VEX statement explains why it does not apply. The Gradle and Maven acceptance tests run against the same images.
+
Authorization. One step checks that every earlier step produced signed proof for these exact files. Anything missing stops the release.
+
Publish. The published images are the same digests that were scanned and tested, promoted without a rebuild, and checked again the way a customer would check them.
+
+
The full description, as the pipeline actually runs, is in RELEASING.md.
+
+
What you get with each release
+
+
Container images on GitHub's registry, signed without a stored key (Sigstore), with build provenance.
+
Plain binaries and a checksums.txt with its own signature file.
+
A software bill of materials (SBOM) for each binary archive.
+
A VEX file saying which known vulnerabilities do and do not affect the release.
+
A release-manifest.json listing every file and its digest.
+
+
All of it is public. You do not need an account or a purchase to check any of it. The commands are under "Verifying a release" in RELEASING.md.
+
+
Kept patched
+
Every published image is scanned again every day, so a vulnerability disclosed against something we already shipped raises an issue without anyone having to remember to look.
+
+
What we do not claim
+
+
We aim to ship fixes for dependency vulnerabilities within 48 hours of public disclosure. That is an intention, not a promise you can hold us to.
+
The paid tiers are not built. Everything here is the free MIT core.
+
+
+
+
diff --git a/index.html b/index.html
index 6c172ae..9b8e3d2 100644
--- a/index.html
+++ b/index.html
@@ -86,6 +86,7 @@
Gradle Inc. has deprecated the free Develocity Build Cache Node: it "will no longer be
distributed, supported or available after December 31, 2026." The migration path they
offer requires a commercial Develocity subscription.
+ What that means and your options.
Your build cache shouldn't die with its vendor.
@@ -93,7 +94,7 @@
Your build cache shouldn't die with its vendor.
FosterStack is a self-hosted remote build cache server — a Develocity Build Cache Node
replacement that speaks the same Gradle remote build cache HTTP protocol, so migrating is
mostly a URL change. The same server implements the Apache Maven Build Cache Extension's
- remote HTTP mode (Maven acceptance coverage is in progress), so a mixed Gradle and Maven
+ remote HTTP mode, so a mixed Gradle and Maven
shop runs one deploy instead of two. Open source core, one-command deploy, and the thing
that matters: it stays patched, on a promise.
@@ -108,11 +109,11 @@
Where it stands: v0.1 — early. The cache core and
+
Where it stands: v0.2 — early. The cache core and
HTTP surface are tested, and the Gradle path is acceptance-tested against a real
multi-module build in CI; the release pipeline works and its evidence chain is being
rebuilt — the repository's SECURITY.md states exactly what is and is not proven today.
- Nobody is running it in a production build pipeline yet except us. Bugs and questions go to
+ Bugs and questions go to
GitHub issues, which is also
where the roadmap gets argued with.
@@ -135,6 +136,33 @@
What you get that a bare HTTP endpoint doesn't give you
30-minute migration
A step-by-step guide for existing Build Cache Node deployments. Same protocol, same CI config shape — migrating is mostly a URL change.
+
Security is the difference
+
Security is what sets this product apart. The release items below ship in the current release. The supply-chain items describe how we work today, in our own build and CI. You can check each one yourself.
+
Signed and verifiable
+
+
Signed releases. Images and the checksums file are signed with Sigstore, with no signing key stored anywhere. How to verify.
+
Build provenance. Every image and binary archive carries a signed statement naming the CI run that built it.
+
A bill of materials. Every binary archive ships with an SBOM.
+
Published vulnerability statements. A VEX file with each release says which known vulnerabilities do and do not affect it.
+
Reproducible. Each image is assembled a second time on a separate machine, and the digests must match or the release fails.
+
+
Supply chain
+
+
Pinned build steps. Every GitHub Action our releases use is pinned to an exact commit, and a check in CI enforces it on every change. The container base image is pinned by digest.
+
Hash-checked installs. Python tooling in our CI is installed with hash checking, so a swapped package is refused.
+
A 7-day wait. Our CI holds new dependency versions for 7 days before adopting them, and Dependabot has the same cooling-off period.
+
A daily supply-chain check. Once a day, everything our build and CI pull in (actions, tools, packages) is checked against public advisory databases and malicious-package reports. Any hit opens an issue.
+
Scanned before release. Two independent scanners with different databases check every release candidate, and any finding blocks the release unless a published VEX statement explains it. RELEASING.md.
+
Rescanned every day. Every published image is scanned again daily, so a new vulnerability in something we shipped raises an issue.
+
+
Small, quiet and patched
+
+
Tiny and quiet. A distroless image with one static program and no shell. It sends no telemetry and makes no outbound connections.
+
A FIPS build. A FIPS variant uses Go's validated FIPS 140-3 cryptographic module.
+
Patched for everyone. We aim to ship dependency fixes within 48 hours of disclosure. That is a target, not a promise. Security patches are never withheld from the free tier.
Gradle and Maven both run against the same server today; we maintain the cache
server, while the Maven client side is Apache's own Build Cache Extension. A dependency cache —
@@ -220,7 +275,7 @@
Roadmap honesty
of the documented remote build cache HTTP protocol. Apache Maven is a trademark of the
Apache Software Foundation; the Maven Build Cache Extension is Apache's project, not
ours.
-
Point your Jenkins agents, GitHub Actions runners or GitLab runners at one shared build cache, so a fresh agent stops rebuilding what another agent already built.
+
+
How a CI runner uses a remote build cache
+
The cache is a plain web server. Your build tool (Gradle, or Maven with Apache's Build Cache Extension) asks it for finished task results and uploads new ones. Any runner that can reach the server over HTTP can use it. There is no plugin to install on the CI system.
+
We have not tested it with any particular CI product. Our own tests run in GitHub Actions. The protocol is the same everywhere, so what matters is that the runner can reach the server and holds a login.
+
+
Let CI write, let developers read
+
The simplest safe setup is that only CI uploads to the cache and laptops only download. The server can enforce this: give laptops the optional read-only login (FSCACHE_RO_USERNAME and FSCACHE_RO_PASSWORD), and any write they attempt is refused. On the Gradle side you can also stop uploads from non-CI machines in settings.gradle.kts:
+
isPush = System.getenv("CI") != null
+
That line alone is the client choosing not to upload, not a rule on the server. Anyone holding the read-write password can write, so share it only with people and systems you already trust to change your code.
+
+
Runners inside your Kubernetes cluster
+
If your runners already run in the cluster that hosts the cache (including Jenkins agents), you need no load balancer and no ingress. Point the build at the cluster-internal name:
Replace fscache-ns with your namespace. Plain HTTP is acceptable here only because the traffic never leaves the cluster network. The manifests are in the Kubernetes guide.
+
+
Runners outside the cluster, or on plain servers
+
Run the server on a host the runners can reach and put HTTPS in front of it. Gradle sends the password with every request, so over plain HTTP it travels in the clear. Gradle refuses plain HTTP unless you opt in with isAllowInsecureProtocol, which is a useful warning, not a nuisance. (In our test with Gradle 9.8.0, the address 127.0.0.1 was allowed without it, but the name localhost was refused.) Setup is in the install guide.
+
+
Give the runner the password
+
Store it in your CI system's secret store and expose it as the FSCACHE_PASSWORD environment variable. On a long-lived agent, Gradle's background daemon reads its environment once, when it starts. If you change the secret and builds quietly stop using the cache, run ./gradlew --stop so the next build starts a fresh daemon.
+
+
Check that CI is really using the cache
+
Run the same build twice on two different agents. The second should show FROM-CACHE next to tasks. A runner's local cache can hide a broken remote, so run once with -i, which prints remote errors. Gradle treats a broken remote as non-fatal, so it fails silently otherwise. On the server, the hit and miss counters at /metrics should move.
+
Maven on CI follows the same pattern with the extension and a <server> entry for the password. See the Gradle and Maven setup page.