Docker Compose build cache: quick start and production
Run a shared Gradle build cache for your team in one Compose file, with a password and a size limit. For real use, put an HTTPS proxy in front: Gradle sends the password with every request.
Gradle build cache with Docker Compose
Our advice, not a test result. This is the shape we suggest once you go past a first trial.
Try it first, on one machine
docker run -d -p 127.0.0.1:8080:8080 -v fscache-data:/home/nonroot ghcr.io/fosterstack/cache:latest
The 127.0.0.1: keeps it reachable from this machine only. A bare -p 8080:8080 opens the port to every network interface, and on a cloud host that is a cache anyone can write to the moment the command returns. Pin a version tag instead of latest for anything real.
Quick start: the Compose file
This one is open to your CI runners, so it has a password on purpose. A cache that untrusted people can write to can feed files into your builds.
services:
fscache:
image: ghcr.io/fosterstack/cache:latest # pin a version tag
restart: unless-stopped
ports:
- "8080:8080" # all interfaces, so auth is on
volumes:
- fscache-data:/home/nonroot
environment:
FSCACHE_MAX_BYTES: "53687091200" # 50 GiB, size to your CI volume
FSCACHE_USERNAME: gradle
FSCACHE_PASSWORD: change-me # generate one: openssl rand -base64 24
volumes:
fscache-data:
- The data volume mounts at
/home/nonroot, not/data. The server runs as a non-root user. FSCACHE_MAX_BYTESis in bytes. 50 GiB is53687091200. See server sizing for more values.
Check it works, with no Gradle project
curl -s localhost:8080/healthz; echo
curl -s -u gradle:change-me -o /dev/null -w '%{http_code}\n' -X PUT --data-binary 'hello' localhost:8080/testkey123
curl -s -u gradle:change-me localhost:8080/testkey123; echo
You should see ok, then 201, then hello, each on its own line. The calls use the Compose file's password, change-me. A server with no password, such as the docker run above, accepts them too. If you leave out -u on a server that has a password, the PUT prints 401 and the read prints unauthorized.
Point Gradle at it
Use the settings from our Gradle and Maven setup page, and turn caching on with org.gradle.caching=true. If nothing comes from the cache, work through Gradle remote build cache not working.
Health checks
The container image has no shell, so there is no health check inside the Compose file. Probe /healthz from outside: your orchestrator's HTTP check, a monitor, or curl -f http://localhost:8080/healthz from the host.
Production: nginx and TLS in front
A Compose file for a shared build cache that only answers over HTTPS, with a password, behind nginx. It was run for real on FosterStack Cache 0.2.1 and Gradle 9.8.0. The file pins 0.2.2, a security-patch release whose release notes say it changes no behaviour.
Production: compose.yaml
services:
fscache:
image: ghcr.io/fosterstack/cache:0.2.2
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:
Generate a real password, for example with openssl rand -base64 24, and never keep change-me.
Production: 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 How we checked, below.
The Compose file expects certs/cert.pem and certs/key.pem. To try it on one machine, make a test certificate that is valid for two days:
mkdir -p certs && openssl req -x509 -newkey rsa:2048 -nodes -days 2 -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" -keyout certs/key.pem -out certs/cert.pem
Put compose.yaml, nginx.conf and the certs folder in one directory, then start the stack from there:
docker compose up -d
On a real server, use a certificate from an authority your clients trust.
Gradle over HTTPS
In our runs, 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 our self-signed test certificate we 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
In our runs, 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.
How we checked
- Tested: the two production files above, 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. We also ran every check in the table below, the two production files and the keytool step on a GitHub-hosted Linux machine with release 0.2.2, after verifying the image with cosign. The results were the same. We rewrote the calls under "Check it works" so that each result prints on its own line, then ran the new version on a GitHub-hosted Linux machine, with and without a password. Each result printed on its own line. The logs are public: the first run and the run of the check calls. - 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.
| Check | Result |
|---|---|
https://localhost:8443/healthz with the certificate trusted | ok |
| The same without trusting the certificate | curl refuses (exit 60) |
| The cache's own port 8080 from the host | not reachable |
Plain http:// to the proxy's TLS port | 400 |
| Upload with no password, through the proxy | 401 |
| Upload with the password, through the proxy | 201, and the value reads back |
| Uploads of about 1 MB and 2 MB with nginx's default settings | 413 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 authentication and status codes.
Related: can someone poison our shared build cache. Full detail: Docker deploy guide.