Slow Docker builds in GitHub Actions: causes and fixes
Why are Docker builds slow in GitHub Actions?
Docker builds are slow in GitHub Actions because every job starts on a fresh runner with an empty layer cache, so each build reruns every RUN step from scratch, on a 2 vCPU machine on private repositories. The fixes, in order: cache layers with docker/build-push-action and type=gha (with mode=max for multi-stage builds), order the Dockerfile so dependencies install before the source is copied, keep the build context small with a .dockerignore, and build on faster hardware. On Sengi’s AMD Ryzen 9 7950X3D bare metal boosting past 5 GHz with local NVMe, a production Docker image built 1.9× faster than on GitHub’s runner at 74% less per build, and the layer cache is included.
Last checked October 23, 2026. Build times measured on Sengi September 30 and October 9, 2026, against RunsOn’s published results for GitHub’s runner; GitHub’s cache limits from its Actions limits and billing documentation, checked October 23, 2026.
What makes a Docker build slow in CI?
| Cause | How it shows in the log | Fix |
|---|---|---|
| No layer cache | Every step runs; no CACHED lines. | cache-from: type=gha and cache-to: type=gha,mode=max. |
| Cache invalidated early | COPY . . comes before the install, so a code change reruns it. | Copy the lockfile, install, then copy the source. |
| Builder stage not cached | The final stage is CACHED but the compile stage runs again. | mode=max. |
| Large build context | transferring context takes seconds and invalidates COPY. | A .dockerignore that leaves out .git, build output and logs. |
| Cache evicted | Builds go cold at random. | A scope per image, and a smaller cache; 10 GB per repository on GitHub. |
| Slow CPU | Compile and install steps dominate even when cached up to them. | More cores and a faster CPU. |
| Slow disk | Layer export, unpacking and package installs crawl. | Local NVMe instead of network storage. |
How do I cache Docker layers in GitHub Actions?
Create a Buildx builder, then build with the cache pointed at the GitHub Actions cache. The default docker driver cannot export a cache, which is why setup-buildx-action comes first:
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
with:
context: .
push: false
tags: my-app:latest
cache-from: type=gha
cache-to: type=gha,mode=max
The first run fills the cache; every run after it restores each layer whose inputs have not changed. When one workflow builds several images, give each its own cache with scope=api, scope=worker and so on, so they stop evicting each other. The longer walk-through, with branch scopes and how to verify a warm build, is Docker layer caching on Sengi, and the short answer is how do I cache Docker layers in GitHub Actions?
How should the Dockerfile be ordered?
A layer is reused only if it and every layer before it are unchanged, so put what changes least first:
FROM node:22-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-slim
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]
A code change now reuses the base image and the npm ci layer and reruns only the copy and the build. With mode=max, the build stage is cached as well.
How much faster is a Docker build on faster hardware?
Caching skips unchanged layers; the hardware decides how fast the rest run. Measured on Sengi against GitHub’s runner of the same size:
| Docker build | GitHub-hosted | Sengi | Faster |
|---|---|---|---|
| Production image (PostHog), 4 vCPU | 14m36s, $0.175 | 7m34s, $0.045 | 1.9× |
| Production image (PostHog), 8 vCPU | 9m46s | 6m26s | 1.5× |
| Mastodon image, 2 vCPU | 252 s, $0.030 | 172 s, $0.009 | 1.47× |
| Node and TypeScript image, warm layer cache, 4 vCPU | Not measured | 68 s cold, 36 s warm | 1.9× |
The 4 and 8 vCPU rows are RunsOn’s open benchmark harness, run on Sengi October 9, 2026, against RunsOn’s published GitHub results; the Mastodon build was measured October 2026 on the same 2 vCPU as ubuntu-latest; the warm-cache row was measured on Sengi September 30, 2026. Sengi’s local NVMe reads at 15× the random-read speed of GitHub’s 4 vCPU runner, which is where layer export and package installs spend their time. See the runner comparison for prices.
What about “no space left on device”?
A standard GitHub-hosted runner lists a 14 GB SSD, and a Docker build with large base images and a full layer cache can fill what is left. Remove images and build cache you no longer need with docker system prune -af or docker builder prune -af between steps, build fewer stages per job, or move the job to a runner with a bigger disk: GitHub’s larger runners start at 150 GB for 4 cores, and Sengi’s 4 and 8 vCPU runners have 150 and 200 GiB volumes on local NVMe.
How do I run Docker builds on Sengi?
Change runs-on and merge. Docker, Compose and Buildx are installed, and your type=gha lines use Sengi’s cache with no change:
jobs:
build:
- runs-on: ubuntu-latest
+ runs-on: sengi-standard-4-ubuntu-2404
Or give your coding agent one prompt and it opens the pull request. See how switching works and Sengi runner pricing.
Frequently asked questions
Why is my Docker build not using the cache in GitHub Actions?
Usually one of four things: the workflow never exports a cache (no cache-to), it uses the default docker driver instead of a Buildx builder, the build uses mode=min so a multi-stage build's builder stage is not saved, or an early COPY of the whole source invalidates every layer after it. Check the build log for CACHED lines and for importing cache manifest from gha.
What is the difference between mode=min and mode=max?
mode=min, the default, saves only the layers that end up in the final image. mode=max saves every layer of every stage, including builder stages. Use mode=max for multi-stage builds, where the expensive compile or install happens in a stage that is not in the final image.
Should I use type=gha or type=registry for the Docker cache?
type=gha stores layers in the GitHub Actions cache and needs no registry credentials, so it is the simplest choice for one repository. type=registry stores the cache as an image in your registry, which suits a cache shared across repositories or larger than the Actions cache allows.
How big can the GitHub Actions cache get?
Each repository includes 10 GB of Actions cache. When it fills, the oldest entries are evicted, which can leave a Docker build running cold. GitHub bills cache storage above 10 GB at $0.07 per GB-month if the repository's limit is raised.
Does a faster runner speed up Docker builds?
Yes. Every RUN step that is not cached runs on the runner's CPU and disk. On RunsOn's open benchmark harness at 4 vCPU, Sengi built a production Docker image in 7m34s against 14m36s on GitHub's runner, 1.9× faster, at $0.045 a build against $0.175.
Does Docker layer caching cost extra on Sengi?
No. Docker layer caching through type=gha, actions/cache and the setup actions' caches are included with every Sengi runner, served from Sengi's own cache with no workflow changes.
The fastest hardware for your GitHub Actions.
AMD Ryzen 9 7950X3D desktop CPUs boosting past 5 GHz, on bare metal with local NVMe, a fresh VM for every job, and 2 vCPU jobs ready in 2 seconds. Builds finish up to 1.9× faster than on GitHub’s runners, and at up to half GitHub’s price per minute they cost up to 74% less per build. Change one runs-on line, or give your coding agent one prompt and it opens the pull request for you.
