Shared runners throw away your cache after every job. Move to a dedicated runner where the Docker layer cache and /cache volume persist, and the same build job drops from 1m54s to 47s.

The single change that cut our build time by more than half wasn't a clever
.gitlab-ci.yml trick. It was moving off shared runners onto a dedicated one
that keeps its cache between jobs.
GitLab's shared runners are ephemeral by design. Every job gets a clean machine. Great for isolation, terrible for speed. Each job has to:
docker pull base images from scratchnpm install / pip install / bundle install, downloading every dependency againGitLab's cache: keyword helps, but only so much: shipping a 500MB archive to
and from S3 takes real time, cache misses happen silently, and Docker image
layers can't use cache: at all, which pushes you toward registry hacks and
BuildKit inline caching.
On a dedicated machine that survives between jobs, the wins come for free:
FROM node:20 isn't pulled every run; your RUN apt-get install layer is already built./cache volume persists. On shared runners that volume dies with the VM. On a dedicated machine it stays, so cache: writes to local disk instead of round-tripping through S3.These come from runner architecture, not magic.
The same build app job:
That's the warm-cache case. The first run is comparable, but every run after reuses layers and dependencies already on disk. Multiply by 50 pipeline runs a day and it adds up fast.
.gitlab-ci.ymlA typical Node.js build:
build:
stage: build
image: node:20
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
script:
- npm ci
- npm run buildOn shared runners, npm ci downloads everything every time and the cache
round-trip to S3 often costs more than the install. On a dedicated runner the
cache lives on a local volume. The first run fills it, and the rest read from disk.
A Docker build:
build-image:
stage: build
image: docker:24.0.5
services:
- docker:24.0.5-dind
script:
- docker build -t myapp:latest .Shared runners pull docker:24.0.5 and rebuild every layer each time. A
dedicated runner already has the daemon up and base images cached, so unchanged
layers get skipped.
Good fit:
npm install outlasts the testsProbably unnecessary:
RocketRunner spins up a dedicated runner in about two minutes: connect GitLab, pick a size and region, done. Pricing starts at 10.59/month cap. Most teams spend 10 a month. See how the runners work on the Dedicated GitLab Runners page, or start a free trial.