You’re building C++/CUDA code inside Docker. Compilation takes forever. You want ccache. But Docker builds are hermetic — there’s no persistent /root/.cache/ccache between builds.
Turns out there’s a neat two-part trick to solve this.
The Problem
Docker’s --mount=type=cache gives you persistent caches within BuildKit. So you can do:
RUN --mount=type=cache,target=/root/.cache/ccache \
--mount=type=cache,target=/root/.cache/uv \
uv sync --package rocket --frozen
This works great locally — BuildKit keeps the cache volume around between builds. But in CI, your BuildKit instance is ephemeral. Fresh runner, fresh BuildKit, empty cache. Every time.
The Fix: Cache Dance
The trick is bridging GitHub Actions cache into BuildKit’s cache mounts. You need two things:
1. A GitHub Actions cache step — stores ccache data between CI runs:
- name: Cache ccache data
uses: actions/cache@v4
with:
path: ccache-cache
key: ccache-v3-${{ hashFiles('mai_kernels/**/*.cu', 'mai_kernels/**/*.cpp', 'mai_kernels/**/*.h') }}
restore-keys: |
ccache-v3-
2. The buildkit-cache-dance action — injects that cached directory into BuildKit’s cache mounts:
- name: Inject caches into Docker
uses: reproducible-containers/buildkit-cache-dance@v3
with:
builder: ${{ steps.setup-buildx.outputs.name }}
cache-map: |
{
"ccache-cache": "/root/.cache/ccache"
}
That cache-map is the key part — it maps your Actions cache path (ccache-cache on the runner) to the BuildKit cache mount path (/root/.cache/ccache inside the build). The action handles the plumbing of getting data in and out.
Why It’s Two Layers
BuildKit cache mounts and GitHub Actions cache are completely separate systems. BuildKit doesn’t know about actions/cache, and actions/cache doesn’t know about BuildKit volumes.
The buildkit-cache-dance action bridges them — it pre-populates BuildKit’s cache mount from the Actions cache before the build, and extracts the updated cache after the build so Actions can persist it.
Without it, you’d need to COPY the cache directory into the image (bloating it) or give up on ccache in CI entirely.
The Gotcha
Don’t forget the id on your setup-buildx step:
- name: Set up Docker Buildx
id: setup-buildx # <-- need this
uses: docker/setup-buildx-action@v3
The cache-dance action needs ${{ steps.setup-buildx.outputs.name }} to know which builder to inject into. Miss the id and it silently does nothing.
Also — if you have multiple workflows building the same Dockerfile, you need the cache-dance in all of them. Adding --mount=type=cache,target=/root/.cache/ccache to your Dockerfile but only wiring up the cache in one workflow means the other workflows just see an empty cache mount. Ask me how I know.