Get started / S3 caching
Reuse dependencies and Docker layers.
Keep caches in your own S3 buckets between disposable runners. Enable caching for a trusted fleet, restore before a build, and save after it succeeds. Available from controller v0.3.0 and module 0.3.0.
Existing actions/cache, setup-action caches, and BuildKit type=gha continue using GitHub’s cache service. Gondola’s S3 cache uses the action or CLI below and is disabled by default.
1. Enable a cache fleet
Start with an installed deployment. The current release pairs controller v0.3.0 with module 0.3.0. Merge these entries into your existing module, replacing example/project and the runner-group name with your own.
fleets = {
trusted-build = {
scale_set_name = "gondola-trusted-build"
runner_group = "project-trusted-builds"
}
}
cache_fleets = {
trusted-build = {
repository = "example/project"
trust_namespace = "trusted"
retention_days = 14
helper_image = "ghcr.io/gondola-build/gondola@sha256:080c4f6a9eabe1e5135f349f491a740af73f905d9ab5cc499477759cc61c57f4"
}
}Use the same fleet name in fleets and cache_fleets. In GitHub, restrict the group to the configured repository and trusted workflows. Repository-level scale sets can use their default group. Review and apply the Terraform or OpenTofu plan. New runners receive the cache configuration and helper automatically.
Each enabled fleet gets one private, encrypted bucket and scoped runner IAM permissions. The helper image above is the published multiarchitecture digest. Keep it publicly pullable even when you mirror the controller to private ECR: fresh runners do not inherit the controller’s registry login.
All jobs on a fleet share its cache authority. Use separate fleets and runner groups for untrusted pull requests and privileged builds. A repository name or branch in a cache key does not enforce isolation. Keep credentials, tokens, and build secrets out of cached directories.
Add read_only = true to omit the runner’s write permission; an administrator must populate that cache separately. Module-managed IAM roles are required. Existing buckets and external instance profiles are not supported by this integration.
2. Install the reusable action
The action comes in the public signed release bundle. Run the following in Bash from your repository root on a workstation with ORAS, Cosign, jq, and tar installed. It verifies the release and stages two action files for review. You can also use the CLI directly.
Download and verify the cache action
export GONDOLA_VERSION='v0.3.0'
export GONDOLA_RELEASE_DIRECTORY="$(mktemp -d)"
oras pull --output "${GONDOLA_RELEASE_DIRECTORY}" \
"ghcr.io/gondola-build/gondola:${GONDOLA_VERSION}-artifacts"
export GONDOLA_RELEASE_MANIFEST="${GONDOLA_RELEASE_DIRECTORY}/dist/release-manifest.json"
certificate_identity="https://github.com/gondola-build/gondola/.github/workflows/release.yml@refs/tags/${GONDOLA_VERSION}"
cosign verify-blob \
--bundle "${GONDOLA_RELEASE_MANIFEST}.bundle" \
--certificate-identity "${certificate_identity}" \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
"${GONDOLA_RELEASE_MANIFEST}"
jq -e --arg version "${GONDOLA_VERSION}" \
'.release.version == $version and .cache.helper_image == .controller.reference' \
"${GONDOLA_RELEASE_MANIFEST}"
cosign verify-blob \
--bundle "${GONDOLA_RELEASE_DIRECTORY}/dist/checksums.txt.bundle" \
--certificate-identity "${certificate_identity}" \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
"${GONDOLA_RELEASE_DIRECTORY}/dist/checksums.txt"
if command -v sha256sum >/dev/null 2>&1; then
checksum=(sha256sum)
else
checksum=(shasum -a 256)
fi
(cd "${GONDOLA_RELEASE_DIRECTORY}/dist" && "${checksum[@]}" -c checksums.txt)
cache_archive="$(jq -er '.cache.action.archive' "${GONDOLA_RELEASE_MANIFEST}")"
test "${cache_archive}" = "gondola_${GONDOLA_VERSION#v}_cache-action.tar.gz"
printf '%s %s\n' \
"$(jq -er '.cache.action.sha256' "${GONDOLA_RELEASE_MANIFEST}")" "${cache_archive}" | \
(cd "${GONDOLA_RELEASE_DIRECTORY}/dist" && "${checksum[@]}" -c -)
export GONDOLA_CACHE_HELPER_IMAGE="$(jq -er '.cache.helper_image' "${GONDOLA_RELEASE_MANIFEST}")"
cosign verify \
--certificate-identity "${certificate_identity}" \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
"${GONDOLA_CACHE_HELPER_IMAGE}"
mkdir -p "${GONDOLA_RELEASE_DIRECTORY}/cache-action"
tar -tzf "${GONDOLA_RELEASE_DIRECTORY}/dist/${cache_archive}"
tar -xzf "${GONDOLA_RELEASE_DIRECTORY}/dist/${cache_archive}" \
-C "${GONDOLA_RELEASE_DIRECTORY}/cache-action" action.yml run.sh
cat "${GONDOLA_RELEASE_DIRECTORY}/cache-action/action.yml" \
"${GONDOLA_RELEASE_DIRECTORY}/cache-action/run.sh"Review both displayed files, then copy and commit them:
mkdir -p .github/actions/gondola-cache
cp "${GONDOLA_RELEASE_DIRECTORY}/cache-action/action.yml" \
"${GONDOLA_RELEASE_DIRECTORY}/cache-action/run.sh" .github/actions/gondola-cache/
git add .github/actions/gondola-cache/action.yml .github/actions/gondola-cache/run.sh
git commit -m "Vendor Gondola cache action ${GONDOLA_VERSION}"Repeat verification when updating the action and helper image. Check out your repository before using a local action so its committed files are available. The action uses Bash and requires no npm install.
3. Cache dependencies
This workflow caches npm’s download directory using the Node version and lockfile hash. Restore before npm ci, then save after success when the exact key was not already present.
name: Dependency cache
on:
workflow_dispatch:
permissions:
contents: read
jobs:
build:
runs-on: gondola-trusted-build
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: '24'
package-manager-cache: false
- uses: ./.github/actions/gondola-cache
id: dependencies
with:
operation: restore
key: npm-24-${{ hashFiles('package-lock.json') }}
restore-keys: npm-24-
path: .gondola-cache/npm
- run: npm ci --cache .gondola-cache/npm
- run: npm test
- uses: ./.github/actions/gondola-cache
if: success() && steps.dependencies.outputs.cache-hit != 'true'
with:
operation: save
key: npm-24-${{ hashFiles('package-lock.json') }}
path: .gondola-cache/npmAlways run the package manager, even on a cache hit. Use distinct key families for different directories and toolchains. Save is an explicit step; the action has no automatic post-job save hook.
Cache Docker and BuildKit layers
Use Buildx’s docker-container driver and local cache backend. Gondola stores the exported directory in S3. Add .gondola-cache to your .dockerignore first.
Complete Docker workflow
name: Customer-owned BuildKit cache
on:
workflow_dispatch:
permissions:
contents: read
jobs:
build:
runs-on: gondola-trusted-build
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
- uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e
with:
driver: docker-container
- uses: ./.github/actions/gondola-cache
id: layers
with:
operation: restore
key: buildkit-${{ github.sha }}
restore-keys: buildkit-
path: .gondola-cache/buildkit
- name: Build using restored layers
shell: bash
run: |
# Add .gondola-cache to the project's .dockerignore before use.
cache_args=()
if [[ -f .gondola-cache/buildkit/index.json ]]; then
cache_args+=("--cache-from=type=local,src=.gondola-cache/buildkit")
fi
docker buildx build "${cache_args[@]}" \
--cache-to=type=local,dest=.gondola-cache/buildkit-next,mode=max \
--load --tag project:test .
# Insert the project's tests before saving. Save is skipped on failure.
- uses: ./.github/actions/gondola-cache
if: success() && steps.layers.outputs.cache-hit != 'true'
with:
operation: save
key: buildkit-${{ github.sha }}
path: .gondola-cache/buildkit-nextA new export directory avoids accumulating obsolete layers in the restored directory. Insert your project’s tests before saving. Use BuildKit secret mounts for build credentials. This integration does not require the type=s3 backend.
Use the CLI directly
Cache-enabled runners mount the helper at /opt/gondola/bin/gondola. Put restore before your build and save after success; no local action installation is needed.
/opt/gondola/bin/gondola cache status
/opt/gondola/bin/gondola cache restore --key npm-24-lockhash \
--restore-key npm-24- --path .gondola-cache/npm
/opt/gondola/bin/gondola cache save --key npm-24-lockhash --path .gondola-cache/npmThe helper uses the runner’s scoped instance role through the standard AWS SDK credential chain. No separate long-lived cache credential is needed. CLI errors exit nonzero. The timeout defaults to five minutes; --timeout accepts durations up to 30 minutes.
Keys and outputs
Keys contain 1–256 ASCII letters, numbers, periods, underscores, or hyphens and start with a letter or number. Gondola adds the format, operating system, and CPU architecture automatically. The directory path is not part of the key.
Exact keys are immutable until expiry or administrative deletion. Restore tries the exact key, then up to ten ordered prefixes. The first matching prefix selects its newest object. A prefix with more than 1,000 objects returns an error; narrow it or shorten retention.
| Output | Meaning |
|---|---|
cache-hit | True only for an exact restore; fallback and miss are false. |
cache-status | Restore: hit, fallback, or miss. Save: saved, exists, or read-only. Failures: error. |
restored-key | The key selected by restore. |
cache-bytes | Compressed archive bytes reported by the operation. |
The action warns and continues on errors by default. Set fail-on-error: 'true' to fail the job instead. A cold miss is normal. Concurrent saves select one winner; other saves report exists.
Limits, retention, and removal
- One directory of regular files per operation, including executable files. No globs, multiple paths, or symlinks.
- Maximum: 1 GiB compressed, 4 GiB expanded, and 100,000 entries. Leave disk space for the archive, extraction, and build output.
- Restore requires an absent or empty destination. It never merges with or removes existing files.
- Retention defaults to 14 days and accepts 1–365 days. Expiry follows object creation time, not last access.
S3 storage, requests, and transfer are billed to your AWS account. Lifecycle expiry is asynchronous and imposes no storage or spending cap. Compare total cold and warm job time, including transfers. See cache cost guidance.
To remove a cache, drain its jobs, explicitly empty its bucket, then remove its configuration. Buckets use force_destroy = false, so removing a populated cache does not silently delete its contents.
Troubleshooting
| Symptom | Check |
|---|---|
| Helper unavailable or cache disabled | Confirm the fleet is in cache_fleets, apply the plan, and use a fresh runner. |
| Runner does not start after enabling caching | Check /var/log/gondola-runner.log, registry egress, and the helper digest’s architecture. |
| Access denied | Check repository and trust scope, instance-role credentials, and additional AWS policies. Retain IMDSv2 access. |
| Restore rejected | Use an absent or empty directory. Check archive limits and whether the restore prefix is too broad. |
| Save reports exists or read-only | Use a new key for changed content. Read-only fleets intentionally cannot save. |
cache status reads local configuration; it does not test S3 connectivity. Use the job’s outputs and the runner troubleshooting guide to investigate a failed transfer.