diff --git a/404.html b/404.html index 7389ce5..0946a10 100644 --- a/404.html +++ b/404.html @@ -63,15 +63,15 @@ overflow-x: auto; font-size: .9rem; line-height: 1.5; } .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; } .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600; - text-decoration: none; background: var(--accent); color: #fff; } - .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; } + text-decoration: none; background: var(--accent); color: var(--accent-fg); } + .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); } .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; } footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; } footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; } a { color: var(--accent); } - table { display: block; max-width: 100%; overflow-x: auto; } code { overflow-wrap: anywhere; } pre code { overflow-wrap: normal; } + :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; } diff --git a/README.md b/README.md index 0a87ee0..23a8af3 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ # fosterstack/www — marketing site Static landing page for fosterstack.com. No forms, no email capture, no cookies, no -analytics, and no third-party requests of any kind -service, no analytics (add privacy-respecting analytics later if wanted). +analytics, and no third-party requests of any kind. **One-time setup after cloning:** `git config core.hooksPath .githooks` — enables the public-repo hygiene pre-commit hook (see below). CI enforces the same check as a backstop diff --git a/air-gapped-build-cache-install/index.html b/air-gapped-build-cache-install/index.html index 1f0b521..eb2b993 100644 --- a/air-gapped-build-cache-install/index.html +++ b/air-gapped-build-cache-install/index.html @@ -69,15 +69,15 @@ overflow-x: auto; font-size: .9rem; line-height: 1.5; } .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; } .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600; - text-decoration: none; background: var(--accent); color: #fff; } - .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; } + text-decoration: none; background: var(--accent); color: var(--accent-fg); } + .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); } .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; } footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; } footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; } a { color: var(--accent); } - table { display: block; max-width: 100%; overflow-x: auto; } code { overflow-wrap: anywhere; } pre code { overflow-wrap: normal; } + :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; } @@ -100,7 +100,7 @@

What to bring across

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
+  
# 1. Prove the checksums file is ours
 cosign verify-blob \
   --bundle checksums.txt.bundle \
   --certificate-identity-regexp='^https://github.com/fosterstack/cache/' \
@@ -114,7 +114,7 @@ 

Verify before you cross the gap

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.

For containers:

-
docker load -i fscache-X.Y.Z.tar
+  
docker load -i fscache-X.Y.Z.tar
 docker run -d -p 127.0.0.1:8080:8080 -v fscache-data:/home/nonroot ghcr.io/fosterstack/cache:X.Y.Z

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.

diff --git a/back-up-and-restore-build-cache/index.html b/back-up-and-restore-build-cache/index.html index 76eccf6..39c6865 100644 --- a/back-up-and-restore-build-cache/index.html +++ b/back-up-and-restore-build-cache/index.html @@ -69,15 +69,15 @@ overflow-x: auto; font-size: .9rem; line-height: 1.5; } .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; } .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600; - text-decoration: none; background: var(--accent); color: #fff; } - .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; } + text-decoration: none; background: var(--accent); color: var(--accent-fg); } + .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); } .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; } footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; } footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; } a { color: var(--accent); } - table { display: block; max-width: 100%; overflow-x: auto; } code { overflow-wrap: anywhere; } pre code { overflow-wrap: normal; } + :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; } @@ -97,24 +97,24 @@

Where the data lives

1. Stop the server, then back up the volume

I stopped the container first. I did not test copying the files while the server is running, so I cannot say that is safe. The container image has no shell, so a second small container does the copying:

-
docker stop fscache
+  
docker stop fscache
 docker run --rm -v fscache-data:/data:ro -v "$PWD":/backup ubuntu:24.04 \
   tar czf /backup/fscache-data.tgz -C /data .

In my test, with three small entries, the archive held a data/blobs/ folder with one folder per entry prefix, and was 578 bytes.

2. Restore it into a new volume

To check it, I deleted the container and the volume, then restored into a new, empty one:

-
docker volume create fscache-data-restored
+  
docker volume create fscache-data-restored
 docker run --rm -v fscache-data-restored:/data -v "$PWD":/backup:ro ubuntu:24.04 \
   sh -c 'tar xzf /backup/fscache-data.tgz -C /data'
 ls -ln /data   # inside the helper: the files are owned by 65532

Listing the restored volume showed the data folder still owned by user and group 65532, which is what the server runs as. If you restore as a different user, the server will not be able to write.

3. Start the server on the restored volume

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

The status page and the entries were all back:

-
$ curl -s localhost:8080/statusz | grep -E '"store_entries"|"store_bytes"'
+  
$ curl -s localhost:8080/statusz | grep -E '"store_entries"|"store_bytes"'
   "store_bytes": 32,
   "store_entries": 3,
 $ curl -s localhost:8080/alpha
diff --git a/build-cache-docker-compose-production/index.html b/build-cache-docker-compose-production/index.html
new file mode 100644
index 0000000..f6202d6
--- /dev/null
+++ b/build-cache-docker-compose-production/index.html
@@ -0,0 +1,187 @@
+
+
+
+
+
+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

+
    +
  • Tested: the two files below, with Docker Compose on a Mac, nginx 1.29 as the proxy, a self-signed certificate for localhost, and a real Gradle build over HTTPS.
  • +
  • Not tested: a certificate from a public authority or automatic renewal, a public server on port 443, other proxies or load balancers, Kubernetes ingress, and long-running use.
  • +
+ +

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.

+ +
+ + + diff --git a/build-cache-eviction-size-limit/index.html b/build-cache-eviction-size-limit/index.html index ac72c6b..d8197a6 100644 --- a/build-cache-eviction-size-limit/index.html +++ b/build-cache-eviction-size-limit/index.html @@ -69,15 +69,16 @@ overflow-x: auto; font-size: .9rem; line-height: 1.5; } .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; } .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600; - text-decoration: none; background: var(--accent); color: #fff; } - .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; } + text-decoration: none; background: var(--accent); color: var(--accent-fg); } + .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); } .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; } footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; } footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; } a { color: var(--accent); } - table { display: block; max-width: 100%; overflow-x: auto; } code { overflow-wrap: anywhere; } pre code { overflow-wrap: normal; } + .table-scroll { max-width: 100%; overflow-x: auto; } + :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; } @@ -91,17 +92,17 @@

Build cache eviction: how the size limit works

Setting the cap

Set FSCACHE_MAX_BYTES, in bytes. The default, 0, means no limit. Real servers use gigabytes; see server sizing for values you can copy. For this test I used 3500 bytes, so that a few small entries fill it:

-
FSCACHE_MAX_BYTES=3500 ./fscache
+
FSCACHE_MAX_BYTES=3500 ./fscache

What happened

I stored three 1,000-byte entries, read one of them, then stored two more. After each step I looked at /statusz, the status page, and tried to read every key back.

- +
-
StepStored afterwardsEvicted so farWhich keys could be read
Store k1, k2, k33,000 bytes, 3 entries0all three
Read k1unchanged0k1 is now the most recently used
Store k4 (would go over 3,500)3,000 bytes, 3 entries1k1, k3, k4. k2 is gone.
Store k53,000 bytes, 3 entries2k3, k4, k5. k1 is gone.
+

The stored size never went above the cap. Each time something new did not fit, the entry that had gone longest without being used was removed.

Reading counts as using. In the last step k1 went too, even though I had read it earlier. That is because my own checks after storing k4 read k1, k3 and k4 in that order, which made k1 the oldest again. A cache hit keeps an entry alive.

@@ -112,7 +113,7 @@

Watching it on a real cache

The same numbers are in /statusz as store_bytes, max_bytes, store_entries and evicted_entries, and in the Prometheus metrics (fscache_evicted_entries_total and fscache_store_bytes). A rising eviction count together with a hit rate that never climbs means the cap is too small for your build. See monitoring.

Try it yourself

-
FSCACHE_ADDR=127.0.0.1:8080 FSCACHE_DATA_DIR=./data FSCACHE_MAX_BYTES=3500 ./fscache &
+  
FSCACHE_ADDR=127.0.0.1:8080 FSCACHE_DATA_DIR=./data FSCACHE_MAX_BYTES=3500 ./fscache &
 until curl -s localhost:8080/healthz >/dev/null; do sleep 0.2; done   # wait until it is up
 
 put() { head -c 1000 /dev/zero | curl -s -o /dev/null -w "PUT $1 %{http_code}\n" -X PUT --data-binary @- localhost:8080/$1; }
diff --git a/build-cache-node-end-of-life/index.html b/build-cache-node-end-of-life/index.html
index 12f4f27..4be4c36 100644
--- a/build-cache-node-end-of-life/index.html
+++ b/build-cache-node-end-of-life/index.html
@@ -69,15 +69,15 @@
         overflow-x: auto; font-size: .9rem; line-height: 1.5; }
   .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; }
   .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600;
-         text-decoration: none; background: var(--accent); color: #fff; }
-  .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; }
+         text-decoration: none; background: var(--accent); color: var(--accent-fg); }
+  .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); }
   .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; }
   footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; }
   footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; }
   a { color: var(--accent); }
-  table { display: block; max-width: 100%; overflow-x: auto; }
   code { overflow-wrap: anywhere; }
   pre code { overflow-wrap: normal; }
+  :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
 
 
 
@@ -118,32 +118,16 @@ 

What FosterStack Cache is today

It is early. The paid tiers are not built. Everything you can download today is the free core.

-

Security is the difference

-

Security is what we are building this product around. 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

+

More than kept patched.

    -
  • 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.
  • -
  • Report a problem privately. See the security policy.
  • +
  • Signed releases you can verify in minutes.
  • +
  • Build record, parts list (SBOM) and vulnerability statements published with every release.
  • +
  • Scanned before every release, rescanned every day.
  • +
  • Images assembled twice on separate machines; the results must match.
  • +
  • Minimal image, no telemetry. FIPS variant available.
  • +
  • Vulnerable dependencies fixed fast — our target is 48 hours from disclosure.
+

How we build · Security policy · Verify a release

Next step

Read the migration guide or the source on GitHub. There is no signup and no form.

diff --git a/build-cache-node-faq/index.html b/build-cache-node-faq/index.html index 194177f..408bf3f 100644 --- a/build-cache-node-faq/index.html +++ b/build-cache-node-faq/index.html @@ -14,7 +14,7 @@ - + @@ -100,7 +100,7 @@

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.

+

Every release is signed and carries build provenance, an SBOM and published vulnerability statements. Two different scanners, with different vulnerability databases, 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.

diff --git a/build-cache-node-migration-checklist/index.html b/build-cache-node-migration-checklist/index.html new file mode 100644 index 0000000..75fa9c1 --- /dev/null +++ b/build-cache-node-migration-checklist/index.html @@ -0,0 +1,154 @@ + + + + + +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.

+ +
+ + + 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..d67c443 --- /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

+
    +
  • A new, empty named volume. This is what our first-run steps create, and the volume then belongs to 65532. I also tried a new empty volume whose top folder I had set to root. The server still started, and afterwards the folder belonged to 65532, because Docker copies the image's ownership into an empty volume the first time it is used.
  • +
+ +

What I did not test

+
    +
  • A bind mount of a host folder on Linux. The same rule should apply (the folder has to be writable by 65532), but I ran on a Mac, where bind mounts behave differently.
  • +
  • Kubernetes without the fsGroup setting. The manifests in our Kubernetes page set fsGroup: 65532, and I ran them with it; I did not run them without it.
  • +
  • Running the container as root to avoid the problem. It is not what we document.
  • +
+ +

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

+ +
+ + + diff --git a/build-cache-server-sizing/index.html b/build-cache-server-sizing/index.html index b710fdf..c1ac9ff 100644 --- a/build-cache-server-sizing/index.html +++ b/build-cache-server-sizing/index.html @@ -69,15 +69,16 @@ overflow-x: auto; font-size: .9rem; line-height: 1.5; } .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; } .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600; - text-decoration: none; background: var(--accent); color: #fff; } - .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; } + text-decoration: none; background: var(--accent); color: var(--accent-fg); } + .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); } .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; } footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; } footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; } a { color: var(--accent); } - table { display: block; max-width: 100%; overflow-x: auto; } code { overflow-wrap: anywhere; } pre code { overflow-wrap: normal; } + .table-scroll { max-width: 100%; overflow-x: auto; } + :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; } @@ -90,24 +91,24 @@

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

- +
-
vCPURAMDisk
Absolute minimum11 GBcache cap + about 5 GB for the system
Recommended start, about 5 to 20 developers plus CI1 to 22 GB25 to 50 GB SSD
Larger CI farms2 to 44 to 8 GBcache 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:

- +
-
CapFSCACHE_MAX_BYTES
10 GiB10737418240
20 GiB21474836480
50 GiB53687091200
100 GiB107374182400
+

Two mistakes that look like "the cache doesn't help"

    diff --git a/build-cache-survives-restart/index.html b/build-cache-survives-restart/index.html new file mode 100644 index 0000000..1953127 --- /dev/null +++ b/build-cache-survives-restart/index.html @@ -0,0 +1,141 @@ + + + + + +Does the Build Cache Survive a Restart? A Real Test + + + + + + + + + + + + + +
    + +
    Infrastructure worth keeping — and kept patched.
    +
    +
    +

    Does the build cache survive a restart? A real test

    +

    You restart the cache server, or it crashes, or the container is replaced. Do your stored build results come back? We tested a clean stop, a forced kill and a wrong data folder with FosterStack Cache 0.2.1 and a real Gradle build.

    + +

    The short answer

    +
      +
    • After a clean stop and start on the same data folder, every result was still there.
    • +
    • After the server was killed with kill -9 and started again on the same folder, every result was still there.
    • +
    • On a different, empty data folder, the cache was empty and the next build rebuilt everything.
    • +
    +

    The results live in the data folder, not in the running server. If that folder survives, your cache survives.

    + +

    What we ran

    +

    A four-module Gradle 9.8.0 project (the one from Gradle build cache for a monorepo) against a server set up with FSCACHE_DATA_DIR pointing at a folder. Every build used a fresh copy of the project and an empty Gradle home, so the only shared thing was the server. All four builds ended with BUILD SUCCESSFUL. "From cache" counts the four compileJava tasks that printed FROM-CACHE.

    +
    + + + + + + + +
    StepServer state before the buildEntries in the status pageFrom cache
    1. First buildNew, empty data folder A0, then 13 afterwards0 of 4
    2. Clean restartStopped normally, started on folder A13 (same as before)4 of 4
    3. CrashKilled with kill -9, started on folder A13 (same as before)4 of 4
    4. Wrong folderStarted on a different empty folder B00 of 4
    +

    The status page is the server's /statusz address. It showed the same 13 entries and the same size in bytes right after each restart, before any build ran.

    + +

    One limit of this test

    +

    We killed the server while it was idle. We did not kill it in the middle of an upload, so this page says nothing about a half-written result. What we can say is that a crash with nothing in flight lost nothing.

    + +

    If your cache comes back empty

    +

    An empty cache after a restart means the server is looking at a different folder than before. It is the same as step 4. Check that the data folder or volume is still the one you expect:

    + +

    To copy the data somewhere safe, or to move it to another server, see back up and restore.

    + +

    Run on FosterStack Cache 0.2.1 (macOS, arm64) with Gradle 9.8.0, Java 27.

    +
    + + + + diff --git a/changelog/index.html b/changelog/index.html index 0b5e375..2b55c5b 100644 --- a/changelog/index.html +++ b/changelog/index.html @@ -69,16 +69,16 @@ overflow-x: auto; font-size: .9rem; line-height: 1.5; } .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; } .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600; - text-decoration: none; background: var(--accent); color: #fff; } - .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; } + text-decoration: none; background: var(--accent); color: var(--accent-fg); } + .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); } .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; } footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; } footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; } a { color: var(--accent); } blockquote { border-left: 3px solid var(--line); padding: .2rem 0 .2rem 1rem; margin: .8rem 0; color: var(--muted); } - table { display: block; max-width: 100%; overflow-x: auto; } code { overflow-wrap: anywhere; } pre code { overflow-wrap: normal; } + :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; } diff --git a/fips-140-3-build-cache/index.html b/fips-140-3-build-cache/index.html index b54bc38..0572447 100644 --- a/fips-140-3-build-cache/index.html +++ b/fips-140-3-build-cache/index.html @@ -69,15 +69,15 @@ overflow-x: auto; font-size: .9rem; line-height: 1.5; } .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; } .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600; - text-decoration: none; background: var(--accent); color: #fff; } - .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; } + text-decoration: none; background: var(--accent); color: var(--accent-fg); } + .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); } .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; } footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; } footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; } a { color: var(--accent); } - table { display: block; max-width: 100%; overflow-x: auto; } code { overflow-wrap: anywhere; } pre code { overflow-wrap: normal; } + :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; } @@ -93,21 +93,21 @@

    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 \
    +  
    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:

    -
    $ docker logs <container>
    +  
    $ docker logs <container>
     {"time":"2026-10-09T01:00:54.861529344Z","level":"INFO","msg":"fscache: starting","version":"v0.2.1","addr":":8080","data_dir":"./data","max_bytes":0,"auth":"disabled","fips140":"active (Go validated module v1.0.0, CMVP cert #5247)"}

    The status page reports the same:

    -
    $ curl -s localhost:8080/statusz | grep -E 'version|fips140'
    +  
    $ curl -s localhost:8080/statusz | grep -E 'version|fips140'
       "version": "v0.2.1",
       "fips140": true,
       "fips140_note": "active (Go validated module v1.0.0, CMVP cert #5247)",

    The standard image, for comparison, reports it as off:

    -
    $ curl -s localhost:8080/statusz | grep -E 'version|fips140'
    +  
    $ curl -s localhost:8080/statusz | grep -E 'version|fips140'
       "version": "v0.2.1",
       "fips140": false,
       "fips140_note": "off",
    diff --git a/first-15-minutes-build-cache/index.html b/first-15-minutes-build-cache/index.html index ea7250f..2dc0709 100644 --- a/first-15-minutes-build-cache/index.html +++ b/first-15-minutes-build-cache/index.html @@ -69,15 +69,16 @@ overflow-x: auto; font-size: .9rem; line-height: 1.5; } .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; } .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600; - text-decoration: none; background: var(--accent); color: #fff; } - .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; } + text-decoration: none; background: var(--accent); color: var(--accent-fg); } + .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); } .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; } footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; } footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; } a { color: var(--accent); } - table { display: block; max-width: 100%; overflow-x: auto; } code { overflow-wrap: anywhere; } pre code { overflow-wrap: normal; } + .table-scroll { max-width: 100%; overflow-x: auto; } + :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; } @@ -90,13 +91,13 @@

    Your first 15 minutes with a self-hosted build cache

    A checklist from nothing to your first FROM-CACHE. I timed the commands on a real 0.2.1 release: ten seconds in all, on a laptop that already had Gradle and Java and a fast network. The 15 minutes is for you: reading, choosing values, and pointing your own project at the server.

    What I timed

    - +
    -
    StepTook
    Download the release and verify its signature and checksum1 second
    Unpack it and start the server, with a password and a size cap1 second
    Write the project filesunder 1 second
    Build, clean, build again, and see FROM-CACHE8 seconds
    +

    The checklist

    1. Decide where it will run

    @@ -106,7 +107,7 @@

    1. Decide where it will run

2. Download it and check it is ours

-
VER=0.2.1
+  
VER=0.2.1
 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"
@@ -117,7 +118,7 @@ 

2. Download it and check it is ours

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
+  
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.

@@ -130,7 +131,7 @@

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.
+  
// FosterStack Cache — remote Gradle build cache.
 // https://github.com/fosterstack/cache
 buildCache {
     remote<HttpBuildCache> {
@@ -145,7 +146,7 @@ 

5. Point Gradle at it

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>
+  
export FSCACHE_PASSWORD=<a long secret>
 gradle build --build-cache
 gradle clean
 gradle build --build-cache
diff --git a/gradle-build-cache-ci-writes-developers-read/index.html b/gradle-build-cache-ci-writes-developers-read/index.html
index 0ad874b..8b405fe 100644
--- a/gradle-build-cache-ci-writes-developers-read/index.html
+++ b/gradle-build-cache-ci-writes-developers-read/index.html
@@ -69,15 +69,16 @@
         overflow-x: auto; font-size: .9rem; line-height: 1.5; }
   .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; }
   .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600;
-         text-decoration: none; background: var(--accent); color: #fff; }
-  .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; }
+         text-decoration: none; background: var(--accent); color: var(--accent-fg); }
+  .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); }
   .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; }
   footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; }
   footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; }
   a { color: var(--accent); }
-  table { display: block; max-width: 100%; overflow-x: auto; }
   code { overflow-wrap: anywhere; }
   pre code { overflow-wrap: normal; }
+  .table-scroll { max-width: 100%; overflow-x: auto; }
+  :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
 
 
 
@@ -91,14 +92,14 @@ 

Gradle build cache: let CI write, developers only read

1. Give the server two logins

The server takes one read-write login and, optionally, a second read-only login. A client using the read-only one can read cache entries, and any write is refused with a 403.

-
FSCACHE_USERNAME=ci   FSCACHE_PASSWORD=<a long secret> \
+  
FSCACHE_USERNAME=ci   FSCACHE_PASSWORD=<a long secret> \
 FSCACHE_RO_USERNAME=dev FSCACHE_RO_PASSWORD=<another secret> \
 ./fscache

The two usernames must differ, and the read-only pair only works alongside the read-write pair. Both are in the configuration table.

2. One Gradle setting for everyone

Developers and CI use the same settings.gradle.kts. Only the environment differs:

-
rootProject.name = "demo"
+  
rootProject.name = "demo"
 
 // FosterStack Cache — remote Gradle build cache.
 // https://github.com/fosterstack/cache
@@ -117,17 +118,17 @@ 

2. One Gradle setting for everyone

3. What happened when I ran it

The server counts what it stores. Each line below is a build of the same small project. (In my test I turned Gradle's local cache off, with local { isEnabled = false } in the same block, so that a hit could only have come from the server. You do not need that line.)

- +
-
Who buildsResultServer after
A developer first, read-only login, CI unset:compileJava runs. Nothing to read yet, and nothing uploaded.0 entries
CI, read-write login, CI=true:compileJava runs, and the result is uploaded.1 entry
The developer again, read-only login> Task :compileJava FROM-CACHE, "1 from cache"1 entry, 1 hit
+

The laptop never wrote to the cache, and still got the benefit of CI's work.

If a laptop is set up to push anyway

I then made a code change, so there was something new to upload, and built with CI=true but only the read-only login. The build still succeeded. With -i Gradle logged:

-
Could not store entry d4f2de693b561fd2c5d60b17a9232d6f in remote build cache: Storing entry at 'http://127.0.0.1:18098/d4f2de693b561fd2c5d60b17a9232d6f' response status 403: Forbidden
+  
Could not store entry d4f2de693b561fd2c5d60b17a9232d6f in remote build cache: Storing entry at 'http://127.0.0.1:18098/d4f2de693b561fd2c5d60b17a9232d6f' response status 403: Forbidden
 The remote build cache was disabled during the build due to errors.

The server stored nothing: it stayed at 1 entry. The same change built by CI with the read-write login was stored (2 entries). So a misconfigured laptop cannot add to the cache, and its build is not broken by that. Note that Gradle switches the remote cache off for the rest of that build once a store fails, so a laptop in this state also stops reading from it for that build.

diff --git a/gradle-build-cache-corporate-proxy/index.html b/gradle-build-cache-corporate-proxy/index.html new file mode 100644 index 0000000..7232ebd --- /dev/null +++ b/gradle-build-cache-corporate-proxy/index.html @@ -0,0 +1,158 @@ + + + + + +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

+
    +
  • Tested: a small proxy I wrote for the test (it forwards plain HTTP, tunnels HTTPS with CONNECT, can refuse a host, and logs every request), the cache on a non-loopback address, once over plain HTTP and once over HTTPS through nginx with a self-signed certificate that Java was told to trust.
  • +
  • Not tested: a real corporate proxy product, a proxy that asks for a password (407), a proxy that inspects and re-signs TLS, automatic proxy configuration files, and the HTTP_PROXY variables. Maven is covered on its own page, where it behaves differently: for example it does not skip the proxy for loopback addresses.
  • +
+ +

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

+
    +
  • Ask for the cache's host and port to be allowed through the proxy (HTTPS).
  • +
  • Put the proxy lines in gradle.properties, with the systemProp. prefix, and list internal hosts in nonProxyHosts.
  • +
  • Use HTTPS to the cache. See a production setup with nginx and TLS.
  • +
  • If the proxy re-signs TLS, Java will need your company's certificate authority. I did not test that case.
  • +
+ +

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

+ +
+ + + diff --git a/gradle-build-cache-docker-compose/index.html b/gradle-build-cache-docker-compose/index.html index 36053a6..c4e4344 100644 --- a/gradle-build-cache-docker-compose/index.html +++ b/gradle-build-cache-docker-compose/index.html @@ -69,15 +69,15 @@ overflow-x: auto; font-size: .9rem; line-height: 1.5; } .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; } .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600; - text-decoration: none; background: var(--accent); color: #fff; } - .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; } + text-decoration: none; background: var(--accent); color: var(--accent-fg); } + .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); } .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; } footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; } footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; } a { color: var(--accent); } - table { display: block; max-width: 100%; overflow-x: auto; } code { overflow-wrap: anywhere; } pre code { overflow-wrap: normal; } + :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; } @@ -90,12 +90,12 @@

Gradle build cache with Docker Compose

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
+
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:
+  
services:
   fscache:
     image: ghcr.io/fosterstack/cache:latest   # pin a version tag
     restart: unless-stopped
@@ -117,7 +117,7 @@ 

The Compose file

Check it works, with no Gradle project

-
curl -s localhost:8080/healthz                                   # ok
+  
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.

diff --git a/gradle-build-cache-enable-disable/index.html b/gradle-build-cache-enable-disable/index.html new file mode 100644 index 0000000..537e939 --- /dev/null +++ b/gradle-build-cache-enable-disable/index.html @@ -0,0 +1,156 @@ + + + + + +Enable or Disable the Gradle Build Cache: Flag or Property + + + + + + + + + + + + + +
+ +
Infrastructure worth keeping — and kept patched.
+
+
+

Enable or disable the Gradle build cache: flag, property or settings

+

Gradle uses the build cache only when it is switched on and a cache is set up. Here is every way we tried to switch it on and off, with what each one did against a real cache server (release 0.2.1, Gradle 9.8.0).

+ +

The short answer

+
    +
  • To turn it on for one build, add --build-cache.
  • +
  • To turn it on for everyone, put org.gradle.caching=true in gradle.properties.
  • +
  • To turn it off for one build, add --no-build-cache. It wins over the property.
  • +
  • Switching it on is not enough by itself: the project also needs the cache address in settings.gradle.kts.
  • +
+ +

What we ran

+

A four-module project with the cache address set in settings.gradle.kts (shown below). Each run used a fresh copy of the project and an empty Gradle home, so the only shared thing was the cache server. We ran only :core:compileJava and looked at what Gradle printed next to it. Every build ended with BUILD SUCCESSFUL.

+
+ + + + + + + + + + +
How it was switchedCommandResult
Flag (first build)gradle :core:compileJava --build-cacheRan, and saved the result to the server
Nothing setgradle :core:compileJavaRan. The cache was not used
Flaggradle :core:compileJava --build-cacheFROM-CACHE
Property in gradle.propertiesorg.gradle.caching=trueFROM-CACHE
Property, and the flag offorg.gradle.caching=true and --no-build-cacheRan. The flag won
System propertygradle :core:compileJava -Dorg.gradle.caching=trueFROM-CACHE
Flag, and rerun everything--build-cache --rerun-tasksRan, even though a result was stored
+ +

Which one to use

+
    +
  • Everyone, always: the property in gradle.properties, checked in with the project. New machines then need no flags.
  • +
  • CI only: pass --build-cache in the CI command and leave the property out.
  • +
  • A suspect build: run it with --no-build-cache to rule the cache out. If it stays broken, the cache is not the cause.
  • +
  • Force fresh work: --rerun-tasks makes Gradle redo the tasks even when a stored result exists.
  • +
+ +

The address still has to be set

+

This is the settings.gradle.kts we ran. Replace the address with your server's. We used 127.0.0.1 over plain HTTP because the test ran on one machine; use HTTPS across a network.

+
buildCache {
+    local { isEnabled = false }
+    remote<HttpBuildCache> {
+        url = uri("http://127.0.0.1:18141/")
+        isPush = true
+        isAllowInsecureProtocol = true
+    }
+}
+ +

Read-only: turn off uploading, not the cache

+

Setting isPush = false lets a build download from the cache but never upload. We tried it against an empty server with the cache switched on. The build succeeded and ran the task, and the server still held 0 entries afterwards. A second build also ran the task, because nothing had ever been stored. Read-only only helps when someone else, such as CI, fills the cache. That setup is described in CI writes, developers read.

+ +

If you do not see FROM-CACHE

+

Work through why the hit rate is low and remote cache not working. To clear stored results, see reset the build cache.

+ +

Run on Gradle 9.8.0 with FosterStack Cache 0.2.1, macOS (arm64), Java 27.

+
+ + + + diff --git a/gradle-build-cache-hit-rate-low/index.html b/gradle-build-cache-hit-rate-low/index.html index 254d648..00689e7 100644 --- a/gradle-build-cache-hit-rate-low/index.html +++ b/gradle-build-cache-hit-rate-low/index.html @@ -69,15 +69,15 @@ overflow-x: auto; font-size: .9rem; line-height: 1.5; } .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; } .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600; - text-decoration: none; background: var(--accent); color: #fff; } - .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; } + text-decoration: none; background: var(--accent); color: var(--accent-fg); } + .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); } .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; } footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; } footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; } a { color: var(--accent); } - table { display: block; max-width: 100%; overflow-x: auto; } code { overflow-wrap: anywhere; } pre code { overflow-wrap: normal; } + :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; } @@ -91,12 +91,12 @@

Gradle build cache hit rate low: the common causes

How to find the cause

Run the build with --info. For every cacheable task Gradle prints its key:

-
Build cache key for task ':greetStable' is 932183de90ed643c9e448f23bd169d87
+
Build cache key for task ':greetStable' is 932183de90ed643c9e448f23bd169d87

If the same task has a different key on two machines or two runs, something in its inputs differs. Compare the keys first, then look for what changed. If the key is the same and the task still ran, look at the server (the last section).

The test project

Java sources plus three small cacheable tasks in build.gradle.kts. They differ only in the value they take as input:

-
@CacheableTask
+  
@CacheableTask
 abstract class Greet : DefaultTask() {
     @get:Input abstract val label: Property<String>
     @get:OutputFile abstract val out: RegularFileProperty
@@ -119,12 +119,12 @@ 

The test project

Cause 1: a path that is only true on one machine

I copied the project to a different directory and built the same tasks from a clean checkout:

-
> Task :compileJava FROM-CACHE
+  
> Task :compileJava FROM-CACHE
 > Task :greetStable FROM-CACHE
 > Task :greetAbsPath
 > Task :greetTime

Moving the checkout did not matter for compileJava or for the task with a stable input. The task whose input holds the project's absolute path ran again. The keys show why:

-
$ gradle greetAbsPath --info        (in the first directory)
+  
$ gradle greetAbsPath --info        (in the first directory)
 Build cache key for task ':greetAbsPath' is 24c324403b9dacb75e78e128fbf76848
 $ gradle greetAbsPath --info        (in the copy)
 Build cache key for task ':greetAbsPath' is b7b4f69d180d1dc577a36f2869d0806e
@@ -135,7 +135,7 @@

Cause 2: a value that changes on every build

Cause 3: a different Java

The cache had been filled by Gradle running on JDK 27. I ran the same project, in the same directory, with Gradle on JDK 21:

-
> Task :compileJava
+  
> Task :compileJava
 > Task :greetStable

Both ran again. A second run on JDK 21 hit, and going back to JDK 27 hit its own entries. So two Java versions on your CI agents and laptops means two separate sets of entries and a lower hit rate. Fix: use the same Java version everywhere builds share a cache.

diff --git a/gradle-build-cache-http-status-codes/index.html b/gradle-build-cache-http-status-codes/index.html index 1c627bd..3c513bf 100644 --- a/gradle-build-cache-http-status-codes/index.html +++ b/gradle-build-cache-http-status-codes/index.html @@ -69,15 +69,16 @@ overflow-x: auto; font-size: .9rem; line-height: 1.5; } .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; } .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600; - text-decoration: none; background: var(--accent); color: #fff; } - .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; } + text-decoration: none; background: var(--accent); color: var(--accent-fg); } + .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); } .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; } footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; } footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; } a { color: var(--accent); } - table { display: block; max-width: 100%; overflow-x: auto; } code { overflow-wrap: anywhere; } pre code { overflow-wrap: normal; } + .table-scroll { max-width: 100%; overflow-x: auto; } + :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; } @@ -90,7 +91,7 @@

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

- +
@@ -98,11 +99,11 @@

The short table

-
StatusWhen you get itWhat to do
200A stored entry was found and sent.Nothing. This is a cache hit.
201An upload was stored.Nothing.
401No password, or the wrong one.Fix the credentials block. See below.
403A write sent with the read-only login.Use the read-write login for machines that upload, or set the client not to upload.
413An 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 /healthz (no credentials)                                  200
 GET /metrics (no credentials)                                  200
 GET key, no credentials                                        401
 GET key, wrong password                                        401
@@ -120,9 +121,9 @@ 

Tested on a real server

401 does not break the build

Gradle treats a remote cache failure as non-fatal, so a 401 does not fail the build. The build just runs without the cache. In our test with Gradle 9.8.0 on macOS it printed this at the default log level:

-
Could not load entry d4f2de693b561fd2c5d60b17a9232d6f from remote build cache: Loading entry from 'http://127.0.0.1:18099/d4f2de693b561fd2c5d60b17a9232d6f' response status 401: Unauthorized
+
Could not load entry d4f2de693b561fd2c5d60b17a9232d6f from remote build cache: Loading entry from 'http://127.0.0.1:18099/d4f2de693b561fd2c5d60b17a9232d6f' response status 401: Unauthorized

The usual causes are no credentials block, a wrong password, or a typo. Run with -i for more detail. To test the server directly:

-
curl -s -o /dev/null -w '%{http_code}\n' -u gradle:wrong-password https://cache.example.com/some-key
+
curl -s -o /dev/null -w '%{http_code}\n' -u gradle:wrong-password https://cache.example.com/some-key

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

diff --git a/gradle-build-cache-kubernetes/index.html b/gradle-build-cache-kubernetes/index.html index 007df36..79a14ea 100644 --- a/gradle-build-cache-kubernetes/index.html +++ b/gradle-build-cache-kubernetes/index.html @@ -69,15 +69,15 @@ overflow-x: auto; font-size: .9rem; line-height: 1.5; } .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; } .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600; - text-decoration: none; background: var(--accent); color: #fff; } - .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; } + text-decoration: none; background: var(--accent); color: var(--accent-fg); } + .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); } .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; } footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; } footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; } a { color: var(--accent); } - table { display: block; max-width: 100%; overflow-x: auto; } code { overflow-wrap: anywhere; } pre code { overflow-wrap: normal; } + :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; } diff --git a/gradle-build-cache-monorepo/index.html b/gradle-build-cache-monorepo/index.html new file mode 100644 index 0000000..9ade795 --- /dev/null +++ b/gradle-build-cache-monorepo/index.html @@ -0,0 +1,163 @@ + + + + + +Gradle Build Cache for a Monorepo: Real Multi-Module Run + + + + + + + + + + + + + +
+ +
Infrastructure worth keeping — and kept patched.
+
+
+

Gradle build cache for a monorepo: what a multi-module run shows

+

A monorepo is many Gradle modules in one build. A shared cache server lets CI and every developer reuse each module's work. Below is a real run with four modules against FosterStack Cache 0.2.1: which modules came from the cache, and which did not.

+ +

The short answer

+
    +
  • Change one module's code and the other modules still come from the cache.
  • +
  • Change a module's public methods and every module that uses it rebuilds.
  • +
  • A teammate who makes the same change gets everything from the cache.
  • +
+ +

What we ran

+

A build with four modules. core has no dependencies. util and api use core. app uses util and api. Each module has 12 small Java classes. The cache server was release 0.2.1 (macOS, arm64), with Gradle 9.8.0.

+

Every run below used a brand-new copy of the project and an empty Gradle home folder, and the local cache was switched off. The only thing the runs shared was the cache server. Each run was gradle build --console=plain --no-daemon, and every build finished with BUILD SUCCESSFUL.

+ +

Results: what each module's compile step did

+
+ + + + + + + + + +
Runcoreutilapiapp
1. First build, as CI would do itranranranran
2. Another machine, same codefrom cachefrom cachefrom cachefrom cache
3. Edit inside a method in coreranfrom cachefrom cachefrom cache
4. A teammate makes the same editfrom cachefrom cachefrom cachefrom cache
5. Add a public method to coreranranranran
6. Edit only appfrom cachefrom cachefrom cacheran
+

"Ran" means Gradle did the work and uploaded the result. "From cache" means Gradle downloaded it instead, and printed FROM-CACHE next to the task. This is what run 3 printed:

+
> Task :core:compileJava
+> Task :util:compileJava FROM-CACHE
+> Task :api:compileJava FROM-CACHE
+> Task :app:compileJava FROM-CACHE
+ +

What this means for your monorepo

+
    +
  • Edits inside a method stay local (run 3). Gradle compares the public shape of a module, not every byte, when it decides whether a module that depends on it must rebuild. A change inside a method body left the other three modules' inputs the same, so they came from the cache.
  • +
  • Changes to public methods spread (run 5). All four modules ran. In a big monorepo, a small, stable shared module with few public changes keeps more of the build cached.
  • +
  • The first person pays, everyone else downloads (runs 4 and 6). The edit in run 3 was uploaded once. In run 4 a fresh checkout made the same edit and ran nothing.
  • +
+

Only the compile step is shown here, because it is the step that showed the pattern clearly in a small project. The jar steps in this build are not cacheable by default in Gradle, so they ran every time. The project is small, so we are not quoting time saved: the cache pays off as your modules get bigger.

+ +

Set it up

+

This is the exact settings.gradle.kts we ran. Replace the address with your server's. We used 127.0.0.1 over plain HTTP because the test ran on one machine; use HTTPS across a network.

+
buildCache {
+    local { isEnabled = false }
+    remote<HttpBuildCache> {
+        url = uri("http://127.0.0.1:18140/")
+        isPush = true
+        isAllowInsecureProtocol = true
+    }
+}
+

And this is the whole gradle.properties:

+
org.gradle.caching=true
+org.gradle.parallel=true
+

In a real team, let only CI upload (isPush) and have developers read. That setup, with a read-only login, is on the CI writes, developers read page.

+ +

If you do not see FROM-CACHE

+

Work through why the hit rate is low. To check the server itself, see your first 15 minutes, and for logins see authentication.

+ +

Run on Gradle 9.8.0 with FosterStack Cache 0.2.1 on macOS (arm64), Java 27.

+
+ + + + diff --git a/gradle-build-cache-tutorial/index.html b/gradle-build-cache-tutorial/index.html index 6b98b56..7f4eb62 100644 --- a/gradle-build-cache-tutorial/index.html +++ b/gradle-build-cache-tutorial/index.html @@ -69,15 +69,15 @@ overflow-x: auto; font-size: .9rem; line-height: 1.5; } .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; } .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600; - text-decoration: none; background: var(--accent); color: #fff; } - .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; } + text-decoration: none; background: var(--accent); color: var(--accent-fg); } + .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); } .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; } footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; } footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; } a { color: var(--accent); } - table { display: block; max-width: 100%; overflow-x: auto; } code { overflow-wrap: anywhere; } pre code { overflow-wrap: normal; } + :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; } @@ -91,14 +91,14 @@

Gradle build cache tutorial: see FROM-CACHE with a self-hosted cache

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:

-
FSCACHE_ADDR=127.0.0.1:8080 FSCACHE_DATA_DIR=./data \
+  
FSCACHE_ADDR=127.0.0.1:8080 FSCACHE_DATA_DIR=./data \
   FSCACHE_USERNAME=gradle FSCACHE_PASSWORD=demo-pass \
   FSCACHE_MAX_BYTES=1073741824 ./fscache

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"
+  
rootProject.name = "demo"
 
 // FosterStack Cache — remote Gradle build cache.
 // https://github.com/fosterstack/cache
@@ -114,11 +114,11 @@ 

2. A small project

} }

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
+
org.gradle.caching=true

And src/main/java/demo/App.java is any small class.

3. Build once

-
export FSCACHE_PASSWORD=demo-pass
+  
export FSCACHE_PASSWORD=demo-pass
 gradle build --build-cache
 
 > Task :compileJava
@@ -135,13 +135,13 @@ 

3. Build once

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"'
+  
$ 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,

4. Clean, and build again

-
gradle clean
+  
gradle clean
 gradle build --build-cache
 
 > Task :compileJava FROM-CACHE
@@ -158,14 +158,14 @@ 

4. Clean, and build again

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"'
+  
$ 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 me up

My first attempt failed before any build ran, because I used http://localhost:...:

-
FAILURE: Build failed with an exception.
+  
FAILURE: Build failed with an exception.
 
 * What went wrong:
 Using insecure protocols with remote build cache, without explicit opt-in, is unsupported.
diff --git a/gradle-configuration-cache-vs-build-cache/index.html b/gradle-configuration-cache-vs-build-cache/index.html index aba0018..65fa9f0 100644 --- a/gradle-configuration-cache-vs-build-cache/index.html +++ b/gradle-configuration-cache-vs-build-cache/index.html @@ -69,15 +69,16 @@ overflow-x: auto; font-size: .9rem; line-height: 1.5; } .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; } .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600; - text-decoration: none; background: var(--accent); color: #fff; } - .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; } + text-decoration: none; background: var(--accent); color: var(--accent-fg); } + .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); } .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; } footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; } footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; } a { color: var(--accent); } - table { display: block; max-width: 100%; overflow-x: auto; } code { overflow-wrap: anywhere; } pre code { overflow-wrap: normal; } + .table-scroll { max-width: 100%; overflow-x: auto; } + :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; } @@ -90,37 +91,37 @@

Gradle configuration cache vs build cache

They sound alike and they are not the same thing. One saves Gradle from working out your build again. The other saves your tasks from running again. You can use both, and only the second one uses a cache server. This is a real run of both together, on Gradle 9.8.0 and a 0.2.1 server.

The short answer

- +
-
Configuration cacheBuild cache
What it savesThe work of reading your build files and planning the tasksThe work of running a task, by reusing its saved output
Where it livesA folder in your project, on your machineOn your machine, and optionally on a server shared with a team
Turned on with--configuration-cache--build-cache (and a server URL for a shared one)
Thrown away whenA build file it depends on changesA task's inputs change, so it gets a different key
Shows up as"Reusing configuration cache."FROM-CACHE next to a task
+

A remote cache server such as FosterStack Cache is for the build cache only. If Gradle suggests enabling the configuration cache, that is a separate local feature and has nothing to do with the server.

Both together, in a real run

The same small project, both flags on, the build cache pointed at a server:

-
gradle build --configuration-cache --build-cache
+
gradle build --configuration-cache --build-cache

1. The first build. Nothing is cached anywhere yet:

-
Calculating task graph as no cached configuration is available for tasks: build
+  
Calculating task graph as no cached configuration is available for tasks: build
 > Task :compileJava
 BUILD SUCCESSFUL in 5s
 Configuration cache entry stored.

2. The same build again. Both caches work, in different ways. The plan is reused, and the task is simply up to date on disk:

-
Reusing configuration cache.
+  
Reusing configuration cache.
 > Task :compileJava UP-TO-DATE
 BUILD SUCCESSFUL in 1s
 Configuration cache entry reused.

3. After gradle clean. The build outputs are gone, so the task would have to run again. The plan is still reused, and the output now comes from the server:

-
Reusing configuration cache.
+  
Reusing configuration cache.
 > Task :compileJava FROM-CACHE
 BUILD SUCCESSFUL in 1s
 Configuration cache entry reused.

The server's status page counted one hit for that, and it holds the compiled output only.

4. After editing build.gradle.kts, then gradle clean. Now the configuration cache is thrown away, because a build file changed, but the build cache still works, because the task's inputs did not change:

-
Calculating task graph as configuration cache cannot be reused because file 'build.gradle.kts' has changed.
+  
Calculating task graph as configuration cache cannot be reused because file 'build.gradle.kts' has changed.
 > Task :compileJava FROM-CACHE
 BUILD SUCCESSFUL in 2s
 Configuration cache entry stored.
diff --git a/gradle-remote-build-cache-authentication/index.html b/gradle-remote-build-cache-authentication/index.html index f80906c..a6ab879 100644 --- a/gradle-remote-build-cache-authentication/index.html +++ b/gradle-remote-build-cache-authentication/index.html @@ -69,15 +69,15 @@ overflow-x: auto; font-size: .9rem; line-height: 1.5; } .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; } .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600; - text-decoration: none; background: var(--accent); color: #fff; } - .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; } + text-decoration: none; background: var(--accent); color: var(--accent-fg); } + .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); } .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; } footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; } footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; } a { color: var(--accent); } - table { display: block; max-width: 100%; overflow-x: auto; } code { overflow-wrap: anywhere; } pre code { overflow-wrap: normal; } + :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; } @@ -90,12 +90,12 @@

Gradle remote build cache authentication and 401 errors

How to put a password on a self-hosted build cache and give Gradle the right one, three ways to supply it, and what you see when it is wrong. Every output below is from a real run: a 0.2.1 server and Gradle 9.8.0 on macOS.

Turn on a password on the server

-
FSCACHE_USERNAME=gradle FSCACHE_PASSWORD=<a long secret> ./fscache
+
FSCACHE_USERNAME=gradle FSCACHE_PASSWORD=<a long secret> ./fscache

This is HTTP Basic Auth. The health and metrics paths stay open without a password, so monitoring still works. The status page does not. For a second, read-only login see let CI write, developers only read.

Tell Gradle the password

In settings.gradle.kts:

-
// FosterStack Cache — remote Gradle build cache.
+  
// FosterStack Cache — remote Gradle build cache.
 // https://github.com/fosterstack/cache
 buildCache {
     remote<HttpBuildCache> {
@@ -119,7 +119,7 @@ 

Three ways to supply the password

What a wrong or missing password looks like

Gradle treats a broken remote cache as non-fatal, so the build still succeeds, just without the cache. In my runs it printed this at the default log level, with a wrong password and with none:

-
Could not load entry d4f2de693b561fd2c5d60b17a9232d6f from remote build cache: Loading entry from 'http://127.0.0.1:18099/d4f2de693b561fd2c5d60b17a9232d6f' response status 401: Unauthorized
+
Could not load entry d4f2de693b561fd2c5d60b17a9232d6f from remote build cache: Loading entry from 'http://127.0.0.1:18099/d4f2de693b561fd2c5d60b17a9232d6f' response status 401: Unauthorized

If you see that line, the password Gradle sent was wrong or missing. To test the server directly, a 401 means wrong credentials and a 404 means the credentials are right and the key is not stored yet. See the status codes.

Do I need to restart the Gradle daemon?

diff --git a/gradle-remote-cache-not-working/index.html b/gradle-remote-cache-not-working/index.html index 2a559f8..8709b8b 100644 --- a/gradle-remote-cache-not-working/index.html +++ b/gradle-remote-cache-not-working/index.html @@ -69,15 +69,15 @@ overflow-x: auto; font-size: .9rem; line-height: 1.5; } .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; } .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600; - text-decoration: none; background: var(--accent); color: #fff; } - .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; } + text-decoration: none; background: var(--accent); color: var(--accent-fg); } + .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); } .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; } footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; } footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; } a { color: var(--accent); } - table { display: block; max-width: 100%; overflow-x: auto; } code { overflow-wrap: anywhere; } pre code { overflow-wrap: normal; } + :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; } @@ -91,13 +91,13 @@

Gradle remote build cache not working: no FROM-CACHE

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
+  
# 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:

-
rm -rf ~/.gradle/caches/build-cache-1
+  
rm -rf ~/.gradle/caches/build-cache-1
 ./gradlew --stop
 ./gradlew build --build-cache -i

Run it twice. The second run is the one that tells you something.

@@ -109,7 +109,7 @@

3. Is the password reaching Gradle?

  • Check that the variable is set in the shell or CI job that runs Gradle. In our test with Gradle 9.8.0 on macOS, a password exported after the background daemon was already running was used by the very next build (same daemon, no restart), and a wrong one gave the 401 straight away. If a password still seems ignored, run ./gradlew --stop to rule the daemon out, or put it 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:

    -
    curl -s -o /dev/null -w '%{http_code}\n' -u gradle:wrong-password \
    +  
    curl -s -o /dev/null -w '%{http_code}\n' -u gradle:wrong-password \
       https://cache.example.com/some-key

    4. Is the URL right, and is it HTTPS?

    @@ -117,7 +117,7 @@

    4. Is the URL right, and is it HTTPS?

    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.

    5. Is the server seeing requests?

    -
    curl -s https://cache.example.com/metrics | grep -E 'fscache_cache_(hits|misses)_total'
    +
    curl -s https://cache.example.com/metrics | grep -E 'fscache_cache_(hits|misses)_total'

    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?

    diff --git a/how-our-pipeline-maps-to-nist-and-slsa/index.html b/how-our-pipeline-maps-to-nist-and-slsa/index.html new file mode 100644 index 0000000..69adac7 --- /dev/null +++ b/how-our-pipeline-maps-to-nist-and-slsa/index.html @@ -0,0 +1,231 @@ + + + + + +NIST SP 800-204C, 800-204D and SLSA: Our Release Pipeline + + + + + + + + + + + + + +
    + +
    Infrastructure worth keeping — and kept patched.
    +
    +
    +

    How our release pipeline maps to NIST SP 800-204C, 800-204D and SLSA

    +

    This page takes the release pipeline that built FosterStack Cache v0.2.1 and puts each stage next to the NIST practice it maps to. It says where a stage only partly implements a practice, and it says which SLSA Build level we assess v0.2.1 at.

    +

    Words used here: SBOM is a parts list of what is inside a build. VEX is a note saying a known flaw does not affect us. Provenance is a signed record of how and where something was built. Digest is the fingerprint of a file or image. SLSA is a public scale of how well a build can be trusted, from Level 1 to Level 3 in its Build track.

    +

    Guidance we designed against, not a certification we hold.

    + +

    The two NIST documents

    +
      +
    • NIST SP 800-204C, Implementation of DevSecOps for a Microservices-based Application with Service Mesh. We use its pipeline sections, mainly 4.6 "Securing the CI/CD Pipeline" and 4.8 "Security Testing".
    • +
    • NIST SP 800-204D, Strategies for the Integration of Software Supply Chain Security in DevSecOps CI/CD Pipelines. We use sections 4 and 5, mainly 5.1.1 "Secure Build" and 5.2 "Securing Workflows in CD Pipelines".
    • +
    +

    Parts of both documents are about running services in your own cluster: service mesh, runtime monitoring, deployment by GitOps. That part happens in your environment, not in our release pipeline, so this page does not map it.

    + +

    Stage by stage: what runs, and which practice it maps to

    +

    This describes the workflow files as they were at the v0.2.1 tag. "Signs" means the stage writes a statement about its work and signs it with keyless signing tied to that stage's own workflow identity, logged in a public transparency log.

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    StageWhat it does in v0.2.1NIST practiceHow much
    Source admissionChecks the tag and commit signatures against the allowed signers on main (or GitHub's own key for merges made on github.com), that the commit is on main, that every required check passed, and that an approved requirements list exists for the version. Signs.800-204D 5.1.4 "Secure Code Commits" (commit-signing enforcement, COMMIT-REQ-2); 5.1.2 "Secure Pull-Push Operations on Repositories" (automated checks before code is trusted)Partly. There is one maintainer, so no second person reviews or approves a change.
    BuildBuilds the binaries once on a fresh GitHub-hosted machine with the toolchain pinned, and checks that the dependency files did not change. Writes checksums and an SBOM per archive. Signs a build statement and SLSA provenance.800-204D 5.1.1 "Secure Build"; 800-204C 3.3.3 (SBOM generation and code signing as pipeline tasks)Partly. The signing runs in the same job as the build steps, not in a separate, more trusted process as 5.1.1 asks.
    Image assemblyChecks every archive against the signed build statement before opening it. Adds one binary to a base image pinned by digest, with no commands run inside the image. Pushes by digest only, with no version tags, to a separate candidates package. Signs, recording the base digest and binary hashes, plus SLSA provenance.800-204D 4.2: "The inputs and outputs of each build step should be verified"Implements the practice.
    Reproducibility checkAssembles the images again on a separate machine. The digests must match, or the release fails. Signs.No direct match in either document.Not mapped. It repeats the image assembly; it does not rebuild the binaries from source.
    ScansTwo different scanners with different vulnerability databases, each on its own machine, check the image-assembly and reproducibility statements first, then scan every platform image of every variant. One also scans the archives, the other the Go module files. A finding at any severity blocks the release. For one of the two scanners, a published VEX statement can clear a finding. Each scanner signs its verdict for the exact digests.800-204C 4.8 and 4.8.1 (container image scans, software composition analysis); 800-204D 5.2, DEPLOY_REQ-4 (evidence the image was scanned)Partly. The release pipeline runs no static or dynamic application security testing itself. The signed verdict records a pass, not the full scan report.
    Acceptance testsRun against the exact candidate images: the Linux archives and binaries (macOS archives are checked, not run), the images on both CPU architectures, the image variants, the Gradle and Maven test suites, the Kubernetes setup from our docs, and a test that the server makes no outbound connections. A final job checks that every release-blocking acceptance test in the approved list passed, then signs one statement.800-204C 3.3.1 (tests run in the pipeline; an organisation may choose to stop the pipeline when a test fails); 4.6 (stop further pipeline tasks if a test fails)Implements the practice. The individual test jobs do not sign their own results.
    Release authorizationVerifies every earlier statement: signed by the expected stage, naming this commit and these digests, scan verdicts pass, acceptance results complete. Anything missing stops the release. Records each statement's transparency-log entry. Signs the approval.800-204C 4.6: "verify that all required signatures … are present to ensure that no one bypasses the pipeline"; 800-204D 5.1.1 (policy checks that the required attestations are found)Partly. The policy is code in the workflow file, not a separate signed policy document as 5.1.1 describes.
    Promotion and publishWill not start without the signed approval. Copies the approved digests to GitHub's registry and the Docker Hub mirror with no rebuild, and checks the digests did not change. Signs the public images and the checksums file. Uploads the release files, re-checks the image signatures, the signed checksums and every tag the way a customer would, without credentials, then makes the release page public.800-204D 5.2, DEPLOY-REQ-3 (only an artifact made by the approved build process is cleared)Partly. We refuse to publish anything the approved build did not make. Enforcing that at deploy time is up to your own environment.
    Signing, across all stagesKeyless signing: a short-lived signing identity per stage, no stored signing key, every signature logged publicly.800-204C 4.6: "signing the release artifact during each required CI/CD stage (preferably multi-party signing)"; 800-204D 5.1.1 (attestations signed, stored tamper-proof)Partly. Not multi-party: every signature is made by automation in one repository, run by one maintainer.
    Evidence bundlerelease-manifest.json lists every digest, the hash of the approved requirements list, per-test results, and the transparency-log entry of every statement the authorization verified. It is attached to each image in GitHub's registry as a signed statement and published with the SBOMs, VEX file and signed checksums.800-204D 4 (record provenance, with a way to verify and authenticate it)Implements the practice. Neither document has a section on VEX.
    + +

    Which SLSA Build level does v0.2.1 meet?

    +

    We assess v0.2.1 at SLSA Build Level 2, against the SLSA v1.0 requirements. We do not claim Build Level 3.

    +
      +
    • Consistent build process (L1). Met. One workflow builds every release. Only a tag push starts it, and the first stage stops any tag not signed by an allowed signer.
    • +
    • Provenance exists (L1). Met. SLSA v1 provenance covers the binary archives, their SBOMs, checksums.txt, and the three container images, naming each digest, the source commit and the workflow run.
    • +
    • Provenance is distributed (L1). Met. It is in GitHub's attestation store, and gh attestation verify returns it.
    • +
    • Hosted build platform (L2). Met. The build ran on GitHub-hosted machines, never on a workstation.
    • +
    • Authentic provenance (L2). Met. The provenance is signed with an identity issued to the building workflow, and you can check the signature and the signing workflow yourself.
    • +
    +

    Level 3 is not claimed. It requires that the build steps cannot reach the material used to sign the provenance ("Unforgeable" and "Isolated" in the spec). In v0.2.1, the build steps and the provenance signing run in the same job, so that separation is not there. Build Level 3 is planned. It is not part of v0.2.1.

    + +

    What is not met today

    +
      +
    • SLSA Build Level 3.
    • +
    • Evidence made by a separate, more trusted process than the build itself (800-204D 5.1.1).
    • +
    • A second person: one maintainer writes, approves and releases every change. No review by another person, and no multi-party signing (800-204D 5.1.2, 800-204C 4.6).
    • +
    • All signing is automated. 800-204D 5.1.3, written for software update systems, points the same way: multiple keys with threshold trust, and automated keys kept out of roles customers rely on.
    • +
    • The acceptance test jobs pass their results to the job that signs them; they do not sign or verify statements themselves.
    • +
    • No dynamic application security testing, and no static testing inside the release pipeline.
    • +
    • The release policy is workflow code, not a separate signed policy document.
    • +
    • The reproducibility check covers image assembly, not a second build of the binaries from source.
    • +
    • Promoted images are in the public registries a few steps before the final customer-style checks finish. Only the release page waits for those checks.
    • +
    • Scan verdicts are signed, but the full scan reports are not published as release files.
    • +
    • Checking provenance with gh attestation verify needs a GitHub token. The cosign checks need no account.
    • +
    + +

    Check it yourself

    +

    This checks the SLSA provenance of the v0.2.1 image and the workflow that signed it:

    +
    $ gh attestation verify oci://ghcr.io/fosterstack/cache:0.2.1 \
    +  --repo fosterstack/cache \
    +  --signer-workflow fosterstack/cache/.github/workflows/stage-image.yml
    + + +

    Section numbers refer to the final published versions of NIST SP 800-204C and NIST SP 800-204D. Guidance we designed against, not a certification we hold.

    + + +
    + + + + diff --git a/how-we-build/index.html b/how-we-build/index.html index da10ac1..acacb38 100644 --- a/how-we-build/index.html +++ b/how-we-build/index.html @@ -69,8 +69,8 @@ overflow-x: auto; font-size: .9rem; line-height: 1.5; } .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; } .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600; - text-decoration: none; background: var(--accent); color: #fff; } - .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; } + text-decoration: none; background: var(--accent); color: var(--accent-fg); } + .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); } .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; } footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; } footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; } @@ -78,9 +78,9 @@ ol.steps { margin: .6rem 0 0 1.3rem; } ol.steps li { padding: .3rem 0; } - table { display: block; max-width: 100%; overflow-x: auto; } code { overflow-wrap: anywhere; } pre code { overflow-wrap: normal; } + :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; } @@ -122,7 +122,7 @@

    Kept patched

    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.
    • +
    • Vulnerable dependencies fixed fast — our target is 48 hours from disclosure.
    • The paid tiers are not built. Everything here is the free MIT core.
    diff --git a/index.html b/index.html index 4e4f460..391dbe1 100644 --- a/index.html +++ b/index.html @@ -69,15 +69,16 @@ overflow-x: auto; font-size: .9rem; line-height: 1.5; } .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; } .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600; - text-decoration: none; background: var(--accent); color: #fff; } - .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; } + text-decoration: none; background: var(--accent); color: var(--accent-fg); } + .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); } .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; } footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; } footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; } a { color: var(--accent); } - table { display: block; max-width: 100%; overflow-x: auto; } code { overflow-wrap: anywhere; } pre code { overflow-wrap: normal; } + .table-scroll { max-width: 100%; overflow-x: auto; } + :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; } @@ -93,7 +94,7 @@ What that means and your options. -

    Your build cache shouldn't die with its vendor.

    +

    Fast builds from cache are worth keeping.

    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 @@ -106,7 +107,7 @@

    Your build cache shouldn't die with its vendor.

    Try it now

    There is no signup, no waitlist, and no license key for the free tier. Pull the image and point your build at it:

    -
    docker run -d -p 8080:8080 ghcr.io/fosterstack/cache:latest
    +  
    docker run -d -p 8080:8080 ghcr.io/fosterstack/cache:latest
     curl localhost:8080/healthz   # -> ok

    Quickstart @@ -135,37 +136,21 @@

    What you get that a bare HTTP endpoint doesn't give you

    Cache management

    Size-capped LRU eviction that keeps a busy CI cache healthy without hand-tending — set the cap, and the oldest-unused entries make room.

    Authentication

    HTTP Basic Auth over TLS, wired the way Gradle and Maven already expect credentials. Constant-time comparison, no credential ever logged.

    Metrics & status

    Prometheus metrics, a read-only status page with hit rates and size-vs-cap, and a Grafana dashboard in the repo — so you know the cache is earning its keep.

    -

    Maintenance target

    Dependency CVEs remediated fast — target within 48 hours of disclosure. A stated intention until the paid tiers exist, and the release history is the track record either way.

    -

    Acceptance-tested

    Every change runs against a real multi-module Gradle build in CI: a from-scratch second build must produce real remote-cache hits, or the change does not merge.

    +

    Maintenance target

    Vulnerable dependencies fixed fast — our target is 48 hours from disclosure.

    +

    Designed against NIST guidance

    Our release pipeline is designed against NIST SP 800-204C and 800-204D: each stage's result is recorded in a signed statement, and nothing is promoted to the public registries until a signed release authorization has verified every earlier statement. Guidance we designed against, not a certification we hold. See how each stage maps.

    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

    +

    More than kept patched.

      -
    • 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.
    • -
    • Report a problem privately. See the security policy.
    • +
    • Signed releases you can verify in minutes.
    • +
    • Build record, parts list (SBOM) and vulnerability statements published with every release.
    • +
    • Scanned before every release, rescanned every day.
    • +
    • Images assembled twice on separate machines; the results must match.
    • +
    • Minimal image, no telemetry. FIPS variant available.
    • +
    • Vulnerable dependencies fixed fast — our target is 48 hours from disclosure.
    +

    How we build · Security policy · Verify a release

    Built to be verified, not trusted

    We're a new vendor asking to sit in your build pipeline, so the burden of proof is on us. @@ -199,7 +184,7 @@

    What we do not collect

    Pricing

    Per product. Free is the full product.

    - +
    -
    FreeTeamBusinessCompliance
    The full cache server, MIT-licensed, self-hosted. Every security patch, the @@ -213,7 +198,7 @@

    Pricing

    security-questionnaire support up to 8 hours per year (async), and named-version LTS. Who this is for.
    +

    No sales call. No per-seat tax. No enterprise pricing mystery.

    The Compliance tier, plainly: the security evidence underneath — SBOMs, SLSA diff --git a/jenkins-ci-build-cache/index.html b/jenkins-ci-build-cache/index.html index c531f32..9046406 100644 --- a/jenkins-ci-build-cache/index.html +++ b/jenkins-ci-build-cache/index.html @@ -69,15 +69,15 @@ overflow-x: auto; font-size: .9rem; line-height: 1.5; } .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; } .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600; - text-decoration: none; background: var(--accent); color: #fff; } - .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; } + text-decoration: none; background: var(--accent); color: var(--accent-fg); } + .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); } .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; } footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; } footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; } a { color: var(--accent); } - table { display: block; max-width: 100%; overflow-x: auto; } code { overflow-wrap: anywhere; } pre code { overflow-wrap: normal; } + :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; } @@ -95,12 +95,12 @@

    How a CI runner uses a remote build cache

    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
    +
    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:

    -
    // FosterStack Cache — remote Gradle build cache.
    +  
    // FosterStack Cache — remote Gradle build cache.
     // https://github.com/fosterstack/cache
     buildCache {
         remote<HttpBuildCache> {
    diff --git a/llms.txt b/llms.txt
    index 032c825..48bf21a 100644
    --- a/llms.txt
    +++ b/llms.txt
    @@ -101,9 +101,7 @@ Not yet shipped:
       not yet purchasable; the license key that unlocks them is not built.
       Everything in the repository is the free MIT core, including the -fips image
       and all release evidence.
    -- A CVE patch commitment that can be relied on. The project aims to ship fixes for
    -  dependency CVEs within 48 hours of public disclosure; that is a stated intention,
    -  not a contractual promise.
    +- A CVE patch commitment that can be relied on. Vulnerable dependencies fixed fast — our target is 48 hours from disclosure.
     
     Please do not describe unshipped items as available, and do not attach dates to them.
     
    diff --git a/long-term-support-build-cache/index.html b/long-term-support-build-cache/index.html
    index 99acb4d..0afba7e 100644
    --- a/long-term-support-build-cache/index.html
    +++ b/long-term-support-build-cache/index.html
    @@ -69,15 +69,15 @@
             overflow-x: auto; font-size: .9rem; line-height: 1.5; }
       .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; }
       .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600;
    -         text-decoration: none; background: var(--accent); color: #fff; }
    -  .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; }
    +         text-decoration: none; background: var(--accent); color: var(--accent-fg); }
    +  .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); }
       .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; }
       footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; }
       footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; }
       a { color: var(--accent); }
    -  table { display: block; max-width: 100%; overflow-x: auto; }
       code { overflow-wrap: anywhere; }
       pre code { overflow-wrap: normal; }
    +  :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
     
     
     
    @@ -96,7 +96,7 @@ 

    What that means for you

    • To get a fix, move to the latest release. Older releases are not on the supported list.
    • Security patches are never withheld from the free tier.
    • -
    • We aim to ship fixes for dependency vulnerabilities within 48 hours of public disclosure. That is a target, not a promise.
    • +
    • Vulnerable dependencies fixed fast — our target is 48 hours from disclosure.
    • Every published image is rescanned daily, so a vulnerability disclosed against something we already shipped raises an issue.
    diff --git a/maven-build-cache-corporate-proxy/index.html b/maven-build-cache-corporate-proxy/index.html new file mode 100644 index 0000000..7992cff --- /dev/null +++ b/maven-build-cache-corporate-proxy/index.html @@ -0,0 +1,172 @@ + + + + + +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

    +
      +
    • Tested: a small proxy I wrote for the test, which forwards plain HTTP, can refuse a host, and logs every request (I made it close each connection after one request so that none go unlogged). The cache was on a non-loopback address, over plain HTTP, with a password.
    • +
    • Not tested: HTTPS through a proxy for Maven, a proxy that asks for a password, other Maven or extension versions, and any real proxy product. Gradle is on its own page.
    • +
    + +

    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

    +
      +
    • A local test can fail because of the proxy. If you try the cache on 127.0.0.1 with a proxy configured in settings.xml, Maven sends it to the proxy, which may not be able to reach your machine's loopback. List it in <nonProxyHosts>.
    • +
    • A refusing proxy looks like a cache error. The 403 comes from the proxy, not from the cache. To check, run the same request from the build machine with and without the proxy; see the curl check on the Gradle page.
    • +
    • Failure does not break the build. A build behind a blocking proxy just stops using the cache, so look for Error downloading cache item and for missing Saved to remote cache lines.
    • +
    + +

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

    + +
    + + + diff --git a/maven-build-cache-extension-enable-disable/index.html b/maven-build-cache-extension-enable-disable/index.html new file mode 100644 index 0000000..d7f5741 --- /dev/null +++ b/maven-build-cache-extension-enable-disable/index.html @@ -0,0 +1,154 @@ + + + + + +Enable or Disable the Maven Build Cache Extension + + + + + + + + + + + + + +
    + +
    Infrastructure worth keeping — and kept patched.
    +
    +
    +

    Enable or disable the Maven build cache extension: the switches that work

    +

    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. Here is what each one did in a real run against a self-hosted cache (FosterStack Cache 0.2.1, Maven 3.10.0, extension 1.2.3).

    + +

    The short answer

    +
      +
    • -Dmaven.build.cache.enabled=false turns the cache off for that build.
    • +
    • -Dmaven.build.cache.skipCache=true ignores stored results and builds fully.
    • +
    • -Dmaven.build.cache.remote.enabled=false stops all contact with the server.
    • +
    • -Dmaven.build.cache.remote.save.enabled=false lets the build download but never upload.
    • +
    + +

    What we ran

    +

    A small Maven project with the extension set up as described in Maven remote build cache setup. 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.

    +
    + + + + + + + + + + +
    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 each switch

    +
    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
    +

    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.

    + +

    "Error downloading cache item" on a normal miss

    +

    The first build against an empty server printed this line, and it still succeeded:

    +
    [ERROR] Error downloading cache item: http://127.0.0.1:18160//v1.1/demo/demo/ebb9bceba2f7a6bc/buildinfo.xml
    +[INFO] Remote cache is incomplete or missing, trying local build for demo:demo
    +

    In our run this was an ordinary miss: nothing was stored under that key yet, so Maven built the project itself. The next line says so. If you see this line on a cache that you know holds the result, check the address, the login and the proxy: see Maven build cache not restoring.

    + +

    Which one to use

    +
      +
    • A suspect build: enabled=false rules the cache out completely.
    • +
    • Make sure you build fresh: skipCache=true, for example for a release build.
    • +
    • Working offline or off the VPN: remote.enabled=false, so Maven does not wait on a server it cannot reach.
    • +
    • Developers read, CI writes: remote.save.enabled=false on developer machines. The same idea for Gradle is on CI writes, developers read. A read-only login on the server enforces it.
    • +
    +

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

    + +

    Run on Maven 3.10.0 with maven-build-cache-extension 1.2.3 and FosterStack Cache 0.2.1, macOS (arm64).

    +
    + + + + diff --git a/maven-build-cache-extension-multi-module/index.html b/maven-build-cache-extension-multi-module/index.html new file mode 100644 index 0000000..9eb1a7a --- /dev/null +++ b/maven-build-cache-extension-multi-module/index.html @@ -0,0 +1,165 @@ + + + + + +Maven Build Cache Extension in a Multi-Module Project + + + + + + + + + + + + + +
    + +
    Infrastructure worth keeping — and kept patched.
    +
    +
    +

    Maven build cache extension in a multi-module project: a real run

    +

    With many modules, the Apache Maven Build Cache Extension keeps one stored result per module. We ran a three-module project against a self-hosted cache (FosterStack Cache 0.2.1, Maven 3.10.0, extension 1.2.3) and recorded which modules were restored and which were built.

    + +

    The short answer

    +
      +
    • Every module is stored and restored on its own.
    • +
    • Change one module and the modules it depends on are still restored.
    • +
    • Change one module and the modules that depend on it are built again.
    • +
    • You set the cache up once, in the parent project. Modules need no changes.
    • +
    + +

    What we ran

    +

    Three modules: core has no dependencies, util uses core, and app uses util. Each has six small Java classes. Every build used a fresh copy of the project and an empty local build cache, so the only shared thing was the cache server. All six builds ended with BUILD SUCCESS.

    +
    + + + + + + + + + +
    Buildcoreutilapp
    1. First build, as CI would do itbuiltbuiltbuilt
    2. Another machine, same coderestoredrestoredrestored
    3. Edit inside a method in corebuiltbuiltbuilt
    4. A teammate makes the same editrestoredrestoredrestored
    5. Edit only apprestoredrestoredbuilt
    6. Edit only utilrestoredbuiltbuilt
    +

    "Restored" is the log line Found cached build, restoring demo:core from cache for that module. "Built" means Maven compiled it, then saved the result.

    + +

    What this means for your project

    +
      +
    • Work flows downstream, not upstream (builds 5 and 6). An edit rebuilt that module and the modules that use it, and nothing before it.
    • +
    • A small edit in a base module rebuilds everything above it (build 3). Even a change inside a method rebuilt all three modules in this run. Put code that changes often in modules near the top of the dependency chain.
    • +
    • The first person pays (builds 3 and 4). The teammate with the same edit built nothing.
    • +
    • This differs from Gradle in our other run. There, a change inside a method in the base module left the modules above it restored; see Gradle build cache for a monorepo. Plan around the tool you use.
    • +
    + +

    Set it up once, in the parent

    +

    Two files in the .mvn folder at the root of the project, the same as for a single module:

    +
    <!-- .mvn/extensions.xml -->
    +<extensions>
    +  <extension>
    +    <groupId>org.apache.maven.extensions</groupId>
    +    <artifactId>maven-build-cache-extension</artifactId>
    +    <version>1.2.3</version>
    +  </extension>
    +</extensions>
    +
    <!-- .mvn/maven-build-cache-config.xml -->
    +<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>
    +    <remote enabled="true" saveToRemote="true" id="fosterstack-cache">
    +      <url>http://127.0.0.1:18180/</url>
    +    </remote>
    +  </configuration>
    +</cache>
    +

    Replace the address with your server's. We used 127.0.0.1 over plain HTTP because the test ran on one machine; use HTTPS across a network. For logins, see Maven remote build cache setup.

    + +

    If modules are not restored

    +

    See Maven build cache not restoring. To turn the cache off or skip it for one build, see enable or disable the extension.

    + +

    Run on Maven 3.10.0 with maven-build-cache-extension 1.2.3 and FosterStack Cache 0.2.1, macOS (arm64).

    +
    + + + + diff --git a/maven-build-cache-extension-not-restoring/index.html b/maven-build-cache-extension-not-restoring/index.html index c1f8de3..7e2d1c4 100644 --- a/maven-build-cache-extension-not-restoring/index.html +++ b/maven-build-cache-extension-not-restoring/index.html @@ -69,15 +69,15 @@ overflow-x: auto; font-size: .9rem; line-height: 1.5; } .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; } .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600; - text-decoration: none; background: var(--accent); color: #fff; } - .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; } + text-decoration: none; background: var(--accent); color: var(--accent-fg); } + .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); } .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; } footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; } footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; } a { color: var(--accent); } - table { display: block; max-width: 100%; overflow-x: auto; } code { overflow-wrap: anywhere; } pre code { overflow-wrap: normal; } + :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; } @@ -91,19 +91,19 @@

    Maven build cache extension not restoring: the causes

    First, what a healthy build looks like

    The first build stores its result, and it looks alarming because a miss is logged as an error:

    -
    [INFO] Attempting to restore project demo:demo from build cache
    +  
    [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] 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

    That [ERROR] line is only the server saying it does not have the entry yet. What matters is the Saved to remote cache lines after it. After deleting target and the extension's local cache folder, the second build:

    -
    [INFO] Attempting to restore project demo:demo from build cache
    +  
    [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

    You must delete the local folder to prove the remote works, because the extension keeps its own local cache and will use it first.

    Symptom 1: Maven stops with "Unrecognised tag: 'remote'"

    -
    Cannot initialize cache because xml config is not valid or not available
    +  
    Cannot initialize cache because xml config is not valid or not available
     Unable to parse cache xml element: Unrecognised tag: 'remote' ...

    Cause: the remote element is outside configuration. Fix: move it inside, as in the working file below. Nothing built, and the server saw nothing.

    @@ -112,7 +112,7 @@

    Symptom 2: the build succeeds, but the server stays empty

    Cause: without saveToRemote="true" the extension reads from the server and never uploads. Fix: add it on the machines that should upload, usually CI.

    Symptom 3: "HTTP Status: 401" in the log, and still no uploads

    -
    org.eclipse.aether.spi.connector.transport.http.HttpTransporterException: HTTP Status: 401
    +
    org.eclipse.aether.spi.connector.transport.http.HttpTransporterException: HTTP Status: 401

    The build still ends in BUILD SUCCESS, the server stays empty, and the lines Saved to remote cache are missing. I got exactly this from two different mistakes:

    • the id of the <server> in settings.xml did not match the id on the remote element, and
    • @@ -121,7 +121,7 @@

      Symptom 3: "HTTP Status: 401" in the log, and still no uploads

      Fix: make the two ids identical and check the password. Maven 3.10 may also print a warning about the credentials being used for an address without a matching repository entry. In my working setup the build behaved the same either way.

      The working configuration

      -
      <?xml version="1.0" encoding="UTF-8"?>
      +  
      <?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">
      diff --git a/maven-remote-build-cache-setup/index.html b/maven-remote-build-cache-setup/index.html
      index 637ae83..4a59896 100644
      --- a/maven-remote-build-cache-setup/index.html
      +++ b/maven-remote-build-cache-setup/index.html
      @@ -69,15 +69,15 @@
               overflow-x: auto; font-size: .9rem; line-height: 1.5; }
         .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; }
         .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600;
      -         text-decoration: none; background: var(--accent); color: #fff; }
      -  .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; }
      +         text-decoration: none; background: var(--accent); color: var(--accent-fg); }
      +  .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); }
         .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; }
         footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; }
         footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; }
         a { color: var(--accent); }
      -  table { display: block; max-width: 100%; overflow-x: auto; }
         code { overflow-wrap: anywhere; }
         pre code { overflow-wrap: normal; }
      +  :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
       
       
       
      @@ -94,7 +94,7 @@ 

      Who owns what

      1. Add the extension

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

      -
      <extensions>
      +  
      <extensions>
         <extension>
           <groupId>org.apache.maven.extensions</groupId>
           <artifactId>maven-build-cache-extension</artifactId>
      @@ -109,7 +109,7 @@ 

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

    • The remote element goes inside configuration. Beside it, the extension fails to start with "Unrecognised tag: 'remote'".
    • Add saveToRemote="true" on the machines that should upload. Without it the extension reads from the server but never writes to it, and the server stays empty.
    -
    <?xml version="1.0" encoding="UTF-8"?>
    +  
    <?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">
    @@ -126,7 +126,7 @@ 

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

    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>
    +  
    <settings>
       <servers>
         <server>
           <id>fosterstack-cache</id>
    @@ -139,7 +139,7 @@ 

    3. Add the password, if the server has one

    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
    +  
    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
    @@ -149,12 +149,12 @@ 

    4. Build, then build again

    [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 yet. 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"'
    +  
    $ 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 build-cache   # 'build-cache' is the extension's local folder, next to your local repository
    +  
    rm -rf target build-cache   # '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
    diff --git a/migrate-build-cache-node/index.html b/migrate-build-cache-node/index.html
    index 401039e..e849948 100644
    --- a/migrate-build-cache-node/index.html
    +++ b/migrate-build-cache-node/index.html
    @@ -69,15 +69,15 @@
             overflow-x: auto; font-size: .9rem; line-height: 1.5; }
       .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; }
       .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600;
    -         text-decoration: none; background: var(--accent); color: #fff; }
    -  .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; }
    +         text-decoration: none; background: var(--accent); color: var(--accent-fg); }
    +  .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); }
       .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; }
       footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; }
       footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; }
       a { color: var(--accent); }
    -  table { display: block; max-width: 100%; overflow-x: auto; }
       code { overflow-wrap: anywhere; }
       pre code { overflow-wrap: normal; }
    +  :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
     
     
     
    @@ -93,15 +93,15 @@ 

    Before you start

    A build cache holds copies of things your builds can always rebuild, so there is nothing to export from the old node. Your first build on the new server just fills it. If you have not read why the node is going away, see Build Cache Node end of life.

    Step 1: Run the new cache server

    -
    docker run -d -p 127.0.0.1:8080:8080 -v fscache-data:/home/nonroot ghcr.io/fosterstack/cache:latest
    +
    docker run -d -p 127.0.0.1:8080:8080 -v fscache-data:/home/nonroot ghcr.io/fosterstack/cache:latest

    For anything beyond a first look, pin a specific version instead of latest. This command listens on this machine only. For other machines to use it, put an HTTPS proxy in front. Other ways to run it: bare binary with systemd, Kubernetes, or a disconnected network.

    If the server can be reached by anyone you do not trust, turn on Basic Auth. Anyone who can write to the cache can change what your later builds use.

    Step 2: Gradle build cache setup

    Turn the cache on, in gradle.properties:

    -
    org.gradle.caching=true
    +
    org.gradle.caching=true

    Then point Gradle at the new server in settings.gradle.kts. If you used the old node, you only change the URL:

    -
    // FosterStack Cache — remote Gradle build cache.
    +  
    // FosterStack Cache — remote Gradle build cache.
     // https://github.com/fosterstack/cache
     buildCache {
         remote<HttpBuildCache> {
    @@ -114,7 +114,7 @@ 

    Step 2: Gradle build cache setup

    } }

    Or in settings.gradle (Groovy):

    -
    // FosterStack Cache — remote Gradle build cache.
    +  
    // FosterStack Cache — remote Gradle build cache.
     // https://github.com/fosterstack/cache
     buildCache {
         remote(HttpBuildCache) {
    @@ -131,7 +131,7 @@ 

    Step 2: Gradle build cache setup

    Step 3: Maven build cache setup

    Maven uses Apache's Build Cache Extension. FosterStack Cache implements the server side of it.

    Add the extension in .mvn/extensions.xml (check Maven Central for the current version):

    -
    <extensions>
    +  
    <extensions>
       <extension>
         <groupId>org.apache.maven.extensions</groupId>
         <artifactId>maven-build-cache-extension</artifactId>
    @@ -139,7 +139,7 @@ 

    Step 3: Maven build cache setup

    </extension> </extensions>

    Point it at the server in .mvn/maven-build-cache-config.xml:

    -
    <?xml version="1.0" encoding="UTF-8"?>
    +  
    <?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">
    @@ -157,44 +157,28 @@ 

    Step 3: Maven build cache setup

    Step 4: Check that it works

    For Gradle, run the build twice:

    -
    ./gradlew build --build-cache
    +
    ./gradlew build --build-cache

    On the second run, look for FROM-CACHE next to tasks. Gradle also has a local cache that gives hits even when the remote is broken, so to test the remote itself, clear the local one and run with -i:

    -
    rm -rf ~/.gradle/caches/build-cache-1
    +  
    rm -rf ~/.gradle/caches/build-cache-1
     ./gradlew --stop
     ./gradlew build --build-cache -i

    On the server, the hit and miss counters should move:

    -
    curl -s https://cache.example.com/metrics | grep -E 'fscache_cache_(hits|misses)_total'
    +
    curl -s https://cache.example.com/metrics | grep -E 'fscache_cache_(hits|misses)_total'

    For Maven, build twice from a clean checkout and look for cache hits in the log for unchanged modules.

    Step 5: Retire the old node

    Once builds hit the new server, stop the old Build Cache Node container.

    -

    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

    +

    More than kept patched.

      -
    • 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.
    • -
    • Report a problem privately. See the security policy.
    • +
    • Signed releases you can verify in minutes.
    • +
    • Build record, parts list (SBOM) and vulnerability statements published with every release.
    • +
    • Scanned before every release, rescanned every day.
    • +
    • Images assembled twice on separate machines; the results must match.
    • +
    • Minimal image, no telemetry. FIPS variant available.
    • +
    • Vulnerable dependencies fixed fast — our target is 48 hours from disclosure.
    +

    How we build · Security policy · Verify a release

    What you give up

    The old node had a small web page with a purge button. FosterStack Cache has no settings page. You set it with flags and environment variables, and a read-only status page shows size and hit counts. To empty the cache, remove its volume and start again.

    diff --git a/migrate-maven-build-cache-to-a-new-server/index.html b/migrate-maven-build-cache-to-a-new-server/index.html new file mode 100644 index 0000000..9a0972c --- /dev/null +++ b/migrate-maven-build-cache-to-a-new-server/index.html @@ -0,0 +1,154 @@ + + + + + +Move a Maven Build Cache to a New Server: Real Run + + + + + + + + + + + + + +
    + +
    Infrastructure worth keeping — and kept patched.
    +
    +
    +

    Move a Maven build cache to a new server

    +

    You are replacing the machine that runs your build cache and your builds use the Apache Maven Build Cache Extension. Here is what to change, what to expect from the first build, and how to keep the cache warm. Every step was run for real with FosterStack Cache 0.2.1.

    + +

    The short answer

    +
      +
    • Change the address in .mvn/maven-build-cache-config.xml. Nothing else in the project changes.
    • +
    • If you only change the address, the first build is a normal full build and fills the new server. Nothing fails.
    • +
    • If you want the new server to be ready at once, stop the old one, copy its data folder, and start the new server on the copy.
    • +
    + +

    What we ran

    +

    A small Maven project, Maven 3.10.0, extension 1.2.3, macOS (arm64). Every build used a fresh copy of the project and an empty local build cache, so the only thing shared was the cache server. All five builds ended with BUILD SUCCESS.

    +
    + + + + + + + + +
    BuildServer it usedResult
    1Old server, emptyBuilt the project and saved the result to the server
    2Old server, from another machineRestored from the cache, no compile
    3New server, empty, only the address changedFull build, saved to the new server
    4New server, from another machineRestored from the cache, no compile
    5New server started on a copy of the old data folderRestored from the cache, no compile
    + +

    Option 1: change the address only

    +

    Start the new server, then edit the url in the cache config and commit it. We changed this one line and nothing else:

    +
    <remote enabled="true" saveToRemote="true" id="fosterstack-cache">
    +  <url>http://127.0.0.1:18151/</url>
    +</remote>
    +

    Build 3 had no cached result to find, so Maven compiled the project and saved it:

    +
    [INFO] Attempting to restore project demo:demo from build cache
    +[INFO] Compiling 1 source file with javac [debug release 17] to target/classes
    +[INFO] Saved to remote cache http://127.0.0.1:18151//v1.1/demo/demo/ebb9bceba2f7a6bc/demo.jar
    +

    From then on, every other machine pointing at the new address gets the result from the new server, as build 4 did. Anyone who still has the old address in their config keeps using the old server until they update.

    + +

    Option 2: copy the data folder first

    +

    This keeps the new server warm from the first build. Stop the old server, copy its data folder, and start the new server with its data folder set to the copy. See back up and restore for why the old server should be stopped first.

    +
    cp -R data-old data-new-copied
    +FSCACHE_ADDR=127.0.0.1:18152 FSCACHE_DATA_DIR=$PWD/data-new-copied fscache
    +

    Build 5 found the result straight away, with no compile:

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

    The new server's status page agreed: 2 hits and 0 misses at that point.

    + +

    How to tell it worked

    +

    Look for Found cached build, restoring in the Maven output. You may also see Saved to remote cache lines on a build that restored. In our run these were a small report file that the extension saves each time, not a rebuild. If a build prints Compiling instead of Found cached build, it did not find a stored result.

    + +

    Credentials and other settings

    +

    If the new server has logins, Maven reads them from the <server> entry in settings.xml whose id matches the id on the <remote> line. See Maven remote build cache setup for that, and behind a corporate proxy if your builds go through one. To move Gradle builds too, see one server for Gradle and Maven.

    + +

    Run on Maven 3.10.0 with maven-build-cache-extension 1.2.3 and FosterStack Cache 0.2.1, macOS (arm64).

    +
    + + + + diff --git a/monitor-build-cache-prometheus-grafana/index.html b/monitor-build-cache-prometheus-grafana/index.html index 4ff1a55..d8092ce 100644 --- a/monitor-build-cache-prometheus-grafana/index.html +++ b/monitor-build-cache-prometheus-grafana/index.html @@ -69,15 +69,16 @@ overflow-x: auto; font-size: .9rem; line-height: 1.5; } .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; } .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600; - text-decoration: none; background: var(--accent); color: #fff; } - .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; } + text-decoration: none; background: var(--accent); color: var(--accent-fg); } + .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); } .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; } footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; } footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; } a { color: var(--accent); } - table { display: block; max-width: 100%; overflow-x: auto; } code { overflow-wrap: anywhere; } pre code { overflow-wrap: normal; } + .table-scroll { max-width: 100%; overflow-x: auto; } + :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; } @@ -93,7 +94,7 @@

    Metrics need no password

    The /metrics and /healthz paths stay open even when Basic Auth is on, so a Prometheus scraper needs no credentials. The status page, /statusz, sits behind Basic Auth when it is enabled.

    A scrape setting

    -
    scrape_configs:
    +  
    scrape_configs:
       - job_name: fscache
         static_configs:
           - targets: ['cache.example.com:8080']
    @@ -101,7 +102,7 @@

    A scrape setting

    The metrics

    These are the names a running 0.2.1 server reports:

    - +
    @@ -112,10 +113,10 @@

    The metrics

    -
    MetricTypeWhat it tells you
    fscache_cache_hits_totalcounterRequests answered from the cache
    fscache_cache_misses_totalcounterRequests for something not stored
    fscache_http_request_duration_secondshistogramHow long requests take
    fscache_bytes_read_totalcounterBytes sent to clients
    fscache_bytes_written_totalcounterBytes received from clients
    +

    Queries worth having

    -
    # hit rate over the last 5 minutes
    +  
    # hit rate over the last 5 minutes
     sum(rate(fscache_cache_hits_total[5m]))
       / clamp_min(sum(rate(fscache_cache_hits_total[5m])) + sum(rate(fscache_cache_misses_total[5m])), 0.0001)
     
    @@ -131,7 +132,7 @@ 

    A ready-made Grafana dashboard

    The status page

    For a quick look without Prometheus, /statusz returns the same numbers in one place, in a browser or as JSON. This is what a freshly started 0.2.1 server returned after one stored entry and two requests:

    -
    $ curl -s localhost:8080/statusz
    +  
    $ curl -s localhost:8080/statusz
     {
       "version": "v0.2.1",
       "revision": "0bfc498ecfca3e38cb4a456acda44359a1620c5a",
    diff --git a/one-build-cache-server-gradle-and-maven/index.html b/one-build-cache-server-gradle-and-maven/index.html
    index b36239e..7eb2fbc 100644
    --- a/one-build-cache-server-gradle-and-maven/index.html
    +++ b/one-build-cache-server-gradle-and-maven/index.html
    @@ -69,15 +69,16 @@
             overflow-x: auto; font-size: .9rem; line-height: 1.5; }
       .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; }
       .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600;
    -         text-decoration: none; background: var(--accent); color: #fff; }
    -  .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; }
    +         text-decoration: none; background: var(--accent); color: var(--accent-fg); }
    +  .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); }
       .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; }
       footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; }
       footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; }
       a { color: var(--accent); }
    -  table { display: block; max-width: 100%; overflow-x: auto; }
       code { overflow-wrap: anywhere; }
       pre code { overflow-wrap: normal; }
    +  .table-scroll { max-width: 100%; overflow-x: auto; }
    +  :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
     
     
     
    @@ -90,18 +91,18 @@ 

    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. This is a real run: a repository with a Gradle app and a Maven app, both pointed at one 0.2.1 server. Maven works through the Apache Maven Build Cache Extension.

    The repository

    -
    mixed-repo/
    +  
    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
    +
    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"
    +  
    rootProject.name = "gradle-app"
     
     // FosterStack Cache — remote Gradle build cache.
     // https://github.com/fosterstack/cache
    @@ -118,7 +119,7 @@ 

    The Gradle side

    The Maven side

    maven-app/.mvn/maven-build-cache-config.xml. The remote element goes inside configuration, and saveToRemote="true" is what makes it upload:

    -
    <?xml version="1.0" encoding="UTF-8"?>
    +  
    <?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">
    @@ -132,7 +133,7 @@ 

    The Maven side

    </configuration> </cache>

    And settings.xml, whose server id must match the remote's. Maven filled in the password from the same environment variable:

    -
    <settings>
    +  
    <settings>
       <servers>
         <server>
           <id>fosterstack-cache</id>
    @@ -145,15 +146,15 @@ 

    The Maven side

    What happened

    Each app was built twice. For Gradle, I ran gradle clean in between. For Maven, I deleted target and the extension's local cache folder, so the only copy left was on the server. (I 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/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)
    diff --git a/regulated/index.html b/regulated/index.html
    index a651e37..5aab999 100644
    --- a/regulated/index.html
    +++ b/regulated/index.html
    @@ -67,15 +67,15 @@
             overflow-x: auto; font-size: .9rem; line-height: 1.5; }
       .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; }
       .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600;
    -         text-decoration: none; background: var(--accent); color: #fff; }
    -  .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; }
    +         text-decoration: none; background: var(--accent); color: var(--accent-fg); }
    +  .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); }
       .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; }
       footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; }
       footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; }
       a { color: var(--accent); }
    -  table { display: block; max-width: 100%; overflow-x: auto; }
       code { overflow-wrap: anywhere; }
       pre code { overflow-wrap: normal; }
    +  :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
     
     
     
    @@ -91,8 +91,8 @@ 

    For teams entering regulated markets

    federal or compliance requirements attached — FIPS-validated cryptography, SBOMs, provenance, a security questionnaire with two hundred rows — and suddenly every tool in your build pipeline needs paperwork it has never - had. Big vendors sell their way through that pile with compliance teams - and six-figure contracts. Small teams mostly just lose the deal. + had. Big vendors have compliance teams to get through that pile. Small teams + mostly just lose the deal.

    The cost of compliance shouldn't determine who gets to @@ -116,32 +116,16 @@

    What our piece does — and does not do

    signed, and checkable instead of a shrug.

    -

    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

    +

    More than kept patched.

      -
    • 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.
    • -
    • Report a problem privately. See the security policy.
    • +
    • Signed releases you can verify in minutes.
    • +
    • Build record, parts list (SBOM) and vulnerability statements published with every release.
    • +
    • Scanned before every release, rescanned every day.
    • +
    • Images assembled twice on separate machines; the results must match.
    • +
    • Minimal image, no telemetry. FIPS variant available.
    • +
    • Vulnerable dependencies fixed fast — our target is 48 hours from disclosure.
    +

    How we build · Security policy · Verify a release

    What is free, and what is not available yet

    diff --git a/remote-build-cache-security/index.html b/remote-build-cache-security/index.html index 1f7ac23..548c6e0 100644 --- a/remote-build-cache-security/index.html +++ b/remote-build-cache-security/index.html @@ -69,15 +69,15 @@ overflow-x: auto; font-size: .9rem; line-height: 1.5; } .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; } .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600; - text-decoration: none; background: var(--accent); color: #fff; } - .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; } + text-decoration: none; background: var(--accent); color: var(--accent-fg); } + .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); } .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; } footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; } footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; } a { color: var(--accent); } - table { display: block; max-width: 100%; overflow-x: auto; } code { overflow-wrap: anywhere; } pre code { overflow-wrap: normal; } + :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; } diff --git a/reset-gradle-build-cache/index.html b/reset-gradle-build-cache/index.html index 76a20b6..c497c27 100644 --- a/reset-gradle-build-cache/index.html +++ b/reset-gradle-build-cache/index.html @@ -69,15 +69,15 @@ overflow-x: auto; font-size: .9rem; line-height: 1.5; } .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; } .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600; - text-decoration: none; background: var(--accent); color: #fff; } - .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; } + text-decoration: none; background: var(--accent); color: var(--accent-fg); } + .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); } .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; } footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; } footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; } a { color: var(--accent); } - table { display: block; max-width: 100%; overflow-x: auto; } code { overflow-wrap: anywhere; } pre code { overflow-wrap: normal; } + :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; } @@ -94,15 +94,15 @@

    Why there is no purge button

    It is also safe to do. A build cache holds results your builds can always rebuild. The first builds after a reset are slower while it refills, and that is all.

    Docker Compose

    -
    docker compose down -v && docker compose up -d
    +
    docker compose down -v && docker compose up -d

    The -v removes the named data volume. The fresh start sets up its ownership correctly.

    Plain Docker

    -
    docker rm -f fscache
    +  
    docker rm -f fscache
     docker volume rm fscache-data
     # then run your original docker run command again

    Tested on the 0.2.1 image. Before the reset, /statusz showed one stored entry. After removing the container and the volume and starting again:

    -
    $ curl -s localhost:8080/statusz | grep -E '"store_entries"|"store_bytes"'
    +  
    $ curl -s localhost:8080/statusz | grep -E '"store_entries"|"store_bytes"'
       "store_bytes": 0,
       "store_entries": 0,
     $ curl -s localhost:8080/testkey -o /dev/null -w 'GET testkey %{http_code}\n'
    @@ -110,7 +110,7 @@ 

    Plain Docker

    Kubernetes

    Delete the volume claim, then create it again. The Deployment cannot start without its claim, so the new claim is a required step. Tested on a throwaway cluster:

    -
    kubectl -n fscache-ns scale deploy/fscache --replicas=0
    +  
    kubectl -n fscache-ns scale deploy/fscache --replicas=0
     kubectl -n fscache-ns delete pvc fscache-data
     kubectl -n fscache-ns apply -f fscache-pvc.yaml   # your claim manifest, the same one you first applied
     kubectl -n fscache-ns scale deploy/fscache --replicas=1
    diff --git a/security/index.html b/security/index.html index cc3f4a0..1e91260 100644 --- a/security/index.html +++ b/security/index.html @@ -69,15 +69,15 @@ overflow-x: auto; font-size: .9rem; line-height: 1.5; } .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; } .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600; - text-decoration: none; background: var(--accent); color: #fff; } - .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; } + text-decoration: none; background: var(--accent); color: var(--accent-fg); } + .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); } .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; } footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; } footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; } a { color: var(--accent); } - table { display: block; max-width: 100%; overflow-x: auto; } code { overflow-wrap: anywhere; } pre code { overflow-wrap: normal; } + :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; } @@ -98,7 +98,7 @@

    Report a vulnerability

    Send the report, not a patch. Please do not post details publicly before a fix ships.

    What we aim for

    -

    We aim to ship fixes for dependency vulnerabilities within 48 hours of public disclosure. That is a target, not a promise you can hold us to. Security patches are never withheld from the free tier: the same fix ships to everyone on the same day.

    +

    Vulnerable dependencies fixed fast — our target is 48 hours from disclosure. Security patches are never withheld from the free tier: the same fix ships to everyone on the same day.

    The supported version is the latest minor release.

    What we sign and publish

    diff --git a/sitemap.xml b/sitemap.xml index 5e501cc..cf762b0 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -15,6 +15,10 @@ https://fosterstack.com/back-up-and-restore-build-cache/ 2026-10-08 + + https://fosterstack.com/build-cache-docker-compose-production/ + 2026-10-08 + https://fosterstack.com/build-cache-eviction-size-limit/ 2026-10-08 @@ -27,6 +31,14 @@ https://fosterstack.com/build-cache-node-faq/ 2026-10-08 + + https://fosterstack.com/build-cache-node-migration-checklist/ + 2026-10-08 + + + https://fosterstack.com/build-cache-permission-denied-data-directory/ + 2026-10-08 + https://fosterstack.com/build-cache-server-sizing/ 2026-10-08 @@ -47,6 +59,10 @@ https://fosterstack.com/gradle-build-cache-ci-writes-developers-read/ 2026-10-08 + + https://fosterstack.com/gradle-build-cache-corporate-proxy/ + 2026-10-08 + https://fosterstack.com/gradle-build-cache-docker-compose/ 2026-10-08 @@ -79,6 +95,10 @@ https://fosterstack.com/gradle-remote-cache-not-working/ 2026-10-08 + + https://fosterstack.com/how-our-pipeline-maps-to-nist-and-slsa/ + 2026-10-08 + https://fosterstack.com/how-we-build/ 2026-10-08 @@ -91,6 +111,10 @@ https://fosterstack.com/long-term-support-build-cache/ 2026-10-08 + + https://fosterstack.com/maven-build-cache-corporate-proxy/ + 2026-10-08 + https://fosterstack.com/maven-build-cache-extension-not-restoring/ 2026-10-08 @@ -127,6 +151,10 @@ https://fosterstack.com/security/ 2026-10-08 + + https://fosterstack.com/upgrade-build-cache-server/ + 2026-10-08 + https://fosterstack.com/verify-a-release/ 2026-10-08 @@ -139,4 +167,28 @@ https://fosterstack.com/verify-replacement-build-cache/ 2026-10-08 + + https://fosterstack.com/gradle-build-cache-monorepo/ + 2026-10-08 + + + https://fosterstack.com/migrate-maven-build-cache-to-a-new-server/ + 2026-10-08 + + + https://fosterstack.com/gradle-build-cache-enable-disable/ + 2026-10-08 + + + https://fosterstack.com/maven-build-cache-extension-enable-disable/ + 2026-10-08 + + + https://fosterstack.com/build-cache-survives-restart/ + 2026-10-08 + + + https://fosterstack.com/maven-build-cache-extension-multi-module/ + 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..2a3ab19 --- /dev/null +++ b/upgrade-build-cache-server/index.html @@ -0,0 +1,144 @@ + + + + + +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

    +
      +
    • Tested: Docker, one named volume at /home/nonroot, release 0.1.0 and then 0.2.1 (and back), no password, stopped and started cleanly between versions.
    • +
    • Not tested: any other pair of releases (there is no 0.2.0 release; see the changelog), upgrading while the server is running, Kubernetes or Compose rollouts, the FIPS image, or a cache that was close to its size cap.
    • +
    + +

    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

    +
      +
    • 0.1.0 has no status page. /statusz returned 404 there. 0.2.1 has it. The metrics page is on both.
    • +
    • 0.1.0's entry count resets after a restart. After the rollback its metric fscache_store_entries read 0 although every value was readable. I checked whether that was caused by the upgrade: it is not. A volume only 0.1.0 ever wrote showed 3 entries before a plain restart and 0 after it, with the data still readable. 0.2.1, restarted the same way, reported 3 entries and 9 bytes correctly.
    • +
    + +

    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.

    + +
    + + + diff --git a/verify-a-release/index.html b/verify-a-release/index.html index 577665e..c313844 100644 --- a/verify-a-release/index.html +++ b/verify-a-release/index.html @@ -69,15 +69,15 @@ overflow-x: auto; font-size: .9rem; line-height: 1.5; } .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; } .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600; - text-decoration: none; background: var(--accent); color: #fff; } - .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; } + text-decoration: none; background: var(--accent); color: var(--accent-fg); } + .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); } .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; } footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; } footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; } a { color: var(--accent); } - table { display: block; max-width: 100%; overflow-x: auto; } code { overflow-wrap: anywhere; } pre code { overflow-wrap: normal; } + :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; } @@ -91,11 +91,11 @@

    Verify a FosterStack Cache release yourself

    What you need

    cosign, the gh tool (for the build-provenance steps), and jq. Set the version and your platform once:

    -
    $ VER=0.2.1
    +  
    $ VER=0.2.1
     PLATFORM=linux_arm64   # or linux_amd64, darwin_arm64, darwin_amd64

    1. Download the release files

    -
    $ gh release download v${VER} --repo fosterstack/cache \
    +  
    $ gh release download v${VER} --repo fosterstack/cache \
       -p checksums.txt -p checksums.txt.bundle \
       -p "fscache_${VER}_${PLATFORM}.tar.gz" \
       -p "fscache_${VER}_${PLATFORM}.tar.gz.sbom.json" \
    @@ -104,7 +104,7 @@ 

    1. Download the release files

    2. Is the checksums file ours?

    This checks the signature on checksums.txt. It needs no GitHub account.

    -
    $ cosign verify-blob \
    +  
    $ cosign verify-blob \
       --bundle checksums.txt.bundle \
       --certificate-identity-regexp='^https://github.com/fosterstack/cache/' \
       --certificate-oidc-issuer='https://token.actions.githubusercontent.com' \
    @@ -112,13 +112,13 @@ 

    2. Is the checksums file ours?

    Verified OK

    3. Does your download match it?

    -
    $ sha256sum -c <(grep "fscache_${VER}_${PLATFORM}.tar.gz" checksums.txt | grep -v sbom)
    +  
    $ sha256sum -c <(grep "fscache_${VER}_${PLATFORM}.tar.gz" checksums.txt | grep -v sbom)
     fscache_0.2.1_linux_arm64.tar.gz: OK

    Steps 2 and 3 together are the important pair. The checksum alone only shows the archive matches a file you downloaded beside it. The signature is what ties that file to us.

    4. Is the container image signed by our release workflow?

    Pin the workflow and the release tag, not "any workflow in the repository". No account is needed.

    -
    $ cosign verify ghcr.io/fosterstack/cache:${VER} \
    +  
    $ cosign verify ghcr.io/fosterstack/cache:${VER} \
       --certificate-identity-regexp="^https://github.com/fosterstack/cache/.github/workflows/stage-promote.yml@refs/tags/v${VER}$" \
       --certificate-oidc-issuer='https://token.actions.githubusercontent.com'
     Verification for ghcr.io/fosterstack/cache:0.2.1 --
    @@ -132,7 +132,7 @@ 

    4. Is the container image signed by our release workflow?

    5. Where was the image built?

    A signature says who signed. Build provenance says what produced the bytes. This step reads GitHub's attestation store, so it needs a token (GH_TOKEN or gh auth login), even though the repository is public.

    -
    $ gh attestation verify oci://ghcr.io/fosterstack/cache:${VER} \
    +  
    $ gh attestation verify oci://ghcr.io/fosterstack/cache:${VER} \
       --repo fosterstack/cache \
       --signer-workflow fosterstack/cache/.github/workflows/stage-image.yml \
       --source-ref "refs/tags/v${VER}" \
    @@ -148,7 +148,7 @@ 

    5. Where was the image built?

    The command exits with status 0 on success. The image digest is the one from step 4, and the run link is the public build that produced it.

    6. Where was your download built?

    -
    $ gh attestation verify "fscache_${VER}_${PLATFORM}.tar.gz" --repo fosterstack/cache \
    +  
    $ gh attestation verify "fscache_${VER}_${PLATFORM}.tar.gz" --repo fosterstack/cache \
       --format json | jq -r '.[0].verificationResult.statement.subject[].name, .[0].verificationResult.signature.certificate.subjectAlternativeName'
     fscache-fips_0.2.1_linux_amd64.tar.gz
     fscache-fips_0.2.1_linux_amd64.tar.gz.sbom.json
    @@ -168,7 +168,7 @@ 

    6. Where was your download built?

    7. Did the whole release chain pass?

    Each stage of the release signs its own statement about the exact image. One of them exists only if the final authorization stage checked all the others for this image: the source check, the build, the image assembly, the reproducibility check, each scanner's verdict, and the acceptance tests.

    -
    $ gh attestation verify oci://ghcr.io/fosterstack/cache:${VER} \
    +  
    $ gh attestation verify oci://ghcr.io/fosterstack/cache:${VER} \
       --repo fosterstack/cache \
       --signer-workflow fosterstack/cache/.github/workflows/stage-authorize.yml \
       --predicate-type https://fosterstack.com/attestations/release-authorization/v1 \
    @@ -180,13 +180,13 @@ 

    7. Did the whole release chain pass?

    8. What is inside? The SBOM

    Each binary archive has a software bill of materials in SPDX format.

    -
    $ jq '{spdxVersion, name, packages: (.packages | length)}' "fscache_${VER}_${PLATFORM}.tar.gz.sbom.json"
    +  
    $ jq '{spdxVersion, name, packages: (.packages | length)}' "fscache_${VER}_${PLATFORM}.tar.gz.sbom.json"
     {
       "spdxVersion": "SPDX-2.3",
       "name": "fscache_0.2.1_linux_arm64.tar.gz",
       "packages": 13
     }
    -
    $ jq -r '.packages[0:4][] | "\(.name) \(.versionInfo)"' "fscache_${VER}_${PLATFORM}.tar.gz.sbom.json"
    +  
    $ jq -r '.packages[0:4][] | "\(.name) \(.versionInfo)"' "fscache_${VER}_${PLATFORM}.tar.gz.sbom.json"
     github.com/beorn7/perks v1.0.1
     github.com/cespare/xxhash/v2 v2.3.0
     github.com/fosterstack/cache v0.2.1
    @@ -194,7 +194,7 @@ 

    8. What is inside? The SBOM

    9. Which known vulnerabilities do not apply? The VEX file

    The VEX (OpenVEX) file lists vulnerabilities that scanners may report and says why each does not affect this release.

    -
    $ jq -r '.statements[] | "\(.vulnerability.name) \(.status) \(.justification)"' fosterstack-cache.openvex.json
    +  
    $ jq -r '.statements[] | "\(.vulnerability.name) \(.status) \(.justification)"' fosterstack-cache.openvex.json
     CVE-2024-51744 not_affected component_not_present
     CVE-2025-60876 not_affected vulnerable_code_not_in_execute_path
     CVE-2025-46394 not_affected vulnerable_code_not_in_execute_path
    @@ -203,13 +203,13 @@ 

    9. Which known vulnerabilities do not apply? The VEX file

    10. The release manifest

    release-manifest.json lists the evidence for the whole release. It names three image variants (the standard one, debug and fips), each with its digest. The standard one is the digest you verified in steps 4 and 5, at both registries:

    -
    $ jq -r '.images[] | "\(.variant) \(.digest)"' release-manifest.json
    +  
    $ jq -r '.images[] | "\(.variant) \(.digest)"' release-manifest.json
     production sha256:8df991b5febdf5b4a177325b8af95b6bb6c321059e1715c06cb659c659f5a4f0
     debug sha256:e2e502f44d8bd9e6967cb160aabeb012e8520ebba0faa88e130143fe534f282e
     fips sha256:e125fe3f6ed2c18c478453300ba2b787a42999edf66a39403877efb39865ab1b
    -
    $ jq -c '.images[0].refs' release-manifest.json
    +  
    $ jq -c '.images[0].refs' release-manifest.json
     ["ghcr.io/fosterstack/cache@sha256:8df991b5febdf5b4a177325b8af95b6bb6c321059e1715c06cb659c659f5a4f0","docker.io/fosterstack/cache@sha256:8df991b5febdf5b4a177325b8af95b6bb6c321059e1715c06cb659c659f5a4f0"]
    -
    $ jq '.verified_statements | length' release-manifest.json
    +  
    $ jq '.verified_statements | length' release-manifest.json
     43

    That last number is the statements the authorization stage checked, each with its entry in the public transparency log.

    diff --git a/verify-container-image-signature-cosign/index.html b/verify-container-image-signature-cosign/index.html index b728a22..13c6618 100644 --- a/verify-container-image-signature-cosign/index.html +++ b/verify-container-image-signature-cosign/index.html @@ -69,15 +69,15 @@ overflow-x: auto; font-size: .9rem; line-height: 1.5; } .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; } .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600; - text-decoration: none; background: var(--accent); color: #fff; } - .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; } + text-decoration: none; background: var(--accent); color: var(--accent-fg); } + .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); } .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; } footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; } footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; } a { color: var(--accent); } - table { display: block; max-width: 100%; overflow-x: auto; } code { overflow-wrap: anywhere; } pre code { overflow-wrap: normal; } + :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; } @@ -90,19 +90,19 @@

    Verify a container image signature with cosign

    How to check that a FosterStack Cache image was signed by our release workflow and built by GitHub's CI, using cosign and the gh tool. It takes under a minute.

    Find the current version

    -
    VER=$(curl -fsSL https://api.github.com/repos/fosterstack/cache/releases/latest \
    +  
    VER=$(curl -fsSL https://api.github.com/repos/fosterstack/cache/releases/latest \
             | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')

    1. Check the signature

    This answers one question: did our release workflow sign these exact bytes? Pin the workflow and the release tag, not "any workflow in the repository".

    -
    cosign verify ghcr.io/fosterstack/cache:${VER} \
    +  
    cosign verify ghcr.io/fosterstack/cache:${VER} \
       --certificate-identity-regexp="^https://github.com/fosterstack/cache/.github/workflows/stage-promote.yml@refs/tags/v${VER}$" \
       --certificate-oidc-issuer='https://token.actions.githubusercontent.com'

    There is no signing key to steal or rotate. The certificate is short-lived and issued at the moment the workflow ran, and the signature is recorded in a public transparency log. No account is needed for this step.

    2. Check where it was built

    This answers a different question: what produced these bytes? It reads a signed statement about the build, naming the repository, workflow and run. It needs a GitHub token (GH_TOKEN or gh auth login), even though the repository is public.

    -
    gh attestation verify oci://ghcr.io/fosterstack/cache:${VER} \
    +  
    gh attestation verify oci://ghcr.io/fosterstack/cache:${VER} \
       --repo fosterstack/cache \
       --signer-workflow fosterstack/cache/.github/workflows/stage-image.yml \
       --source-ref "refs/tags/v${VER}"
    diff --git a/verify-replacement-build-cache/index.html b/verify-replacement-build-cache/index.html index c2e1bee..e54cb65 100644 --- a/verify-replacement-build-cache/index.html +++ b/verify-replacement-build-cache/index.html @@ -69,15 +69,15 @@ overflow-x: auto; font-size: .9rem; line-height: 1.5; } .cta-row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: 1.1rem; } .btn { display: inline-block; padding: .55rem 1rem; border-radius: 8px; font-weight: 600; - text-decoration: none; background: var(--accent); color: #fff; } - .btn-secondary { background: transparent; color: inherit; border: 1px solid #cbd5e1; } + text-decoration: none; background: var(--accent); color: var(--accent-fg); } + .btn-secondary { background: transparent; color: inherit; border: 1px solid var(--muted); } .muted-note { color: var(--muted); font-size: .93rem; margin-top: 1.1rem; } footer { border-top: 1px solid var(--line); margin-top: 4rem; padding: 1.5rem 1.25rem 3rem; } footer div { max-width: 44rem; margin: 0 auto; color: var(--muted); font-size: .82rem; } a { color: var(--accent); } - table { display: block; max-width: 100%; overflow-x: auto; } code { overflow-wrap: anywhere; } pre code { overflow-wrap: normal; } + :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; } @@ -99,7 +99,7 @@

    3. Can you verify what you download?

    Look for signatures, a software bill of materials, build provenance and vulnerability statements, and check whether you need an account to see them. FosterStack Cache: signed images and checksums, an SBOM per binary archive, build provenance and a VEX file, all public. See how we build.

    4. Is it kept patched?

    -

    Ask how often published files are rescanned and how fast fixes ship. FosterStack Cache: a release is blocked on scanner findings unless a VEX statement explains them, and published images are rescanned daily. We aim to ship dependency fixes within 48 hours of disclosure. That is a target, not a promise.

    +

    Ask how often published files are rescanned and how fast fixes ship. FosterStack Cache: a release is blocked on scanner findings unless a VEX statement explains them, and published images are rescanned daily. Vulnerable dependencies fixed fast — our target is 48 hours from disclosure.

    5. Does it call home?

    A server in your build network should not need to reach out. FosterStack Cache: no telemetry, no update check, no licence check. It can run on a network with no route to the internet.