A dedicated runner already keeps your Docker layer cache warm. Switching from plain docker build to a persistent BuildKit builder with registry cache took our warm builds from 46 to 14 seconds — and made every other runner warm too.
ROCKETRUNNER
gitlab
A dedicated runner with a persistent Docker daemon already beats shared
runners: your layer cache survives between jobs, so most builds skip most
work. That was our starting point — a warm docker build took 46 seconds.
The same build now takes 14 seconds. One CI change, no Dockerfile changes.
docker build leaves on the tableEven with a warm cache, the classic workflow has three drags:
docker build walks your Dockerfile top to bottom.
Multi-stage builds where stages could run in parallel — or be skipped
entirely — still get evaluated one after another.docker push re-checks and uploads layers as a separate step.BuildKit fixes all three, and on a dedicated runner you can make its cache as persistent as the daemon's. The whole change fits in the build job:
build:
image: docker:latest
script:
- echo "$CI_REGISTRY_PASSWORD" | docker login -u $CI_REGISTRY_USER $CI_REGISTRY --password-stdin
# Registers the builder client-side; on first build, buildx finds an
# existing builder container by name on the host and re-attaches.
- docker buildx create --name ci --use
- docker buildx build
--push
--cache-from type=registry,ref=$CI_REGISTRY_IMAGE:cache
--cache-to type=registry,ref=$CI_REGISTRY_IMAGE:cache,mode=max
--tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
.Note what's not there: no services: docker:dind. On a runner that
mounts the host Docker socket — which is how RocketRunner provisions
runners — jobs talk to the host daemon directly. The builder is a small
BuildKit container that lives on the runner host, keeps running between
jobs, and holds the build cache. Every job re-attaches to it by name.
--push streams only missing layers straight
out of the builder. No local image assembly, no separate push step.mode=max cache. Intermediate stages (your frontend compile, your
dependency install) are cached individually, not just the final image
lineage. An unchanged service rebuilds as pure cache hits.--cache-to type=registry publishes the
cache to your GitLab registry. A build on a different runner — or on
this one after a cache wipe — pulls warm cache instead of starting cold.
With several runners, they effectively share one cache.The builder handles concurrent jobs, too: one BuildKit daemon serves parallel build sessions and deduplicates shared work between them.
RUN --mount=type=cache directories don't travel. Package-manager
caches inside cache mounts persist with the builder container but are
never exported to the registry. A step that re-runs on a cold builder
pays its full download cost once.RocketRunner runners come provisioned exactly this way — persistent Docker
daemon, socket mounted into jobs, disk cleanup that leaves running
builders untouched. Drop the snippet above into your .gitlab-ci.yml and
compare your next two pipelines.
// Keep reading