A focused, high-performance remote build cache for Gradle and ccache. Implements the standard HTTP build cache protocol with pluggable storage, optional Basic authentication, built-in observability, and zero external dependencies beyond your chosen backend. Includes a hybrid local+S3 mode with configurable local-cache TTL eviction.
The server speaks plain HTTP. Gradle clients push and pull cache entries through a predictable REST surface. Storage is pluggable, metrics are automatic, and there is no coordination between replicas.
Gradle or ccache: HEAD to check, GET to fetch, PUT to store. Per-project cache isolation via path prefixes.
Go HTTP server with request instrumentation, path validation, and configurable upload limits.
Local filesystem, S3-compatible object store, or a hybrid tier that caches locally and backs everything with S3.
The server implements the standard HTTP build cache protocol used by Gradle and ccache. Each cache entry is addressed by a cacheID and an entryKey, forming the path /cache/{cacheID}/{entryKey}.
All implementations satisfy the same three-method interface. Switch from local disk to S3, or to a hybrid tier that combines both, with a single flag change.
Fastest path for single-node or NFS setups
io.ReadSeeker, so the server can use http.ServeContent for automatic range request support and correct Last-Modified headers.
AWS S3, MinIO, Wasabi, DigitalOcean Spaces
S3_PREFIX lets you share a single bucket across multiple cache servers or environments.
Best of both: speed of local, durability of S3
strictatime for accurate tracking.
The cleanup job uses each file's last access time (atime). Many Linux systems mount volumes with relatime or noatime for performance. For accurate TTL eviction, mount your cache directory with strictatime (or add mountOptions: [strictatime] in Kubernetes).
Every request is instrumented with latency histograms, per-cache-ID counters, and in-flight gauges. Scrape them with Prometheus or point Grafana directly at the metrics endpoint.
All metrics follow the OpenMetrics exposition format. No adapters, no exporters, no sidecars. Just point your Prometheus scrape config at :8080/metrics and start querying.
Request rate, errors, and duration are captured for every handler. A custom response writer records the exact status code before the connection closes, so your error budgets are always accurate.
On SIGINT or SIGTERM, the server stops accepting new connections and waits up to 10 seconds for in-flight requests to finish. No partial cache entries, no dropped uploads, no metric gaps.
Single static binary, a 15 MB Docker image, a production Helm chart, or raw Kubernetes manifests with HPA and health probes.
make build
# Local filesystem
./go-http-cache-server -storage=local -dir=./cache-data
# S3
export STORAGE_TYPE=s3
export S3_BUCKET=my-gradle-cache
export S3_REGION=us-east-1
export S3_CONCURRENCY=15
./go-http-cache-server
# Hybrid: local cache backed by S3, with 7-day local TTL
export STORAGE_TYPE=hybrid
export LOCAL_DIR=./cache-data
export S3_BUCKET=my-gradle-cache
export S3_REGION=us-east-1
export LOCAL_TTL=7d
./go-http-cache-server
# With upload limit (10 MiB)
./go-http-cache-server -storage=local -dir=./cache -max-upload=10485760
# With HTTP Basic authentication
./go-http-cache-server -storage=local -dir=./cache \
-auth-username=gradle \
-auth-password=change-me
# Multi-stage Alpine build (~15 MB final image)
docker build -t go-http-cache-server .
# Non-root user, CA certs included for S3 TLS
docker run -p 8080:8080 \
-e STORAGE_TYPE=s3 \
-e S3_BUCKET=my-cache \
-e S3_REGION=us-east-1 \
-e S3_CONCURRENCY=15 \
-e AUTH_USERNAME=gradle \
-e AUTH_PASSWORD=change-me \
-e AWS_ACCESS_KEY_ID=... \
-e AWS_SECRET_ACCESS_KEY=... \
go-http-cache-server
# Hybrid with local TTL: mount a host volume for the local cache
docker run -p 8080:8080 \
-v $(pwd)/cache-data:/app/cache-data \
-e STORAGE_TYPE=hybrid \
-e LOCAL_DIR=/app/cache-data \
-e LOCAL_TTL=7d \
-e S3_BUCKET=my-cache \
-e S3_REGION=us-east-1 \
-e AWS_ACCESS_KEY_ID=... \
-e AWS_SECRET_ACCESS_KEY=... \
go-http-cache-server
# Install directly from the OCI registry
helm install gradle-cache oci://ghcr.io/akrumov/go-http-cache-server --version 0.1.0
# Install with a custom values file
helm install gradle-cache oci://ghcr.io/akrumov/go-http-cache-server \
--version 0.1.0 -f my-values.yaml
# Upgrade after changing values
helm upgrade gradle-cache oci://ghcr.io/akrumov/go-http-cache-server \
--version 0.1.0 -f my-values.yaml
config:
storageType: s3
s3Bucket: my-gradle-cache
s3Region: us-east-1
s3Endpoint: "" # set for MinIO
s3Concurrency: 15 # parallel S3 part uploads
# localTTL and localCleanupInterval apply to local and hybrid storage
localTTL: "7d"
localCleanupInterval: "24h"
replicaCount: 2
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 70
persistence:
enabled: false # enable for local storage with PVC
size: 10Gi
resources:
requests:
memory: 2Gi
cpu: 1000m
limits:
memory: 4Gi
cpu: 2000m
secret:
data:
AWS_ACCESS_KEY_ID: ""
AWS_SECRET_ACCESS_KEY: ""
AUTH_USERNAME: "gradle"
AUTH_PASSWORD: "change-me"
// settings.gradle.kts
buildCache {
local { isEnabled = true }
remote<HttpBuildCache> {
url = uri("http://localhost:8080/cache/myapp")
isEnabled = true
isPush = providers.environmentVariable("CI").isPresent
credentials {
username = providers.environmentVariable("GRADLE_CACHE_USERNAME").orElse("").get()
password = providers.environmentVariable("GRADLE_CACHE_PASSWORD").orElse("").get()
}
}
}
// gradle.properties
org.gradle.caching=true
# ccache.conf
remote_storage = http|url=http://localhost:8080/cache/ccache
# With HTTP Basic authentication:
# remote_storage = http|url=http://localhost:8080/cache/ccache|credentials=gradle:change-me
# Or set via environment variable:
# export CCACHE_REMOTE_STORAGE="http|url=http://localhost:8080/cache/ccache"
Set a username and password to protect cache reads, writes, and metrics with HTTP Basic authentication. Health probes stay open.
Helm upgrades and raw-manifest rolling updates both ensure maxUnavailable stays zero. New pods pass readiness before old ones terminate.
Production defaults are 2 GiB memory request and 1 CPU, fully tunable through Helm values. Concurrency-aware for high-throughput S3 workloads.
Open source under the MIT License. Contributions welcome.