---
title: "Docker CI builds rebuild from scratch every run | StarSling"
description: "Your Docker image build step redoes every layer on every run. Here is the ordered list of causes to check, starting with a missing GitHub Actions cache backend."
url: https://starsling.dev/github-actions/problems/slow-docker-builds
canonicalUrl: https://starsling.dev/github-actions/problems/slow-docker-builds
---

# Why your Docker build rebuilds from scratch on every CI run

[GitHub Actions](https://starsling.dev/github-actions) / [Problems](https://starsling.dev/github-actions/problems) / Why your Docker build rebuilds from scratch on every CI run

- [How StarSling works](https://starsling.dev/)

Diagnosis mode: static. Last updated: 2026-08-31

A Docker build step that rebuilds from scratch almost always means BuildKit has no cache to restore from: docker/build-push-action ran without cache-from and cache-to, so every layer, including the base image pull and dependency install, re-executes on an empty runner. Wire in the GitHub Actions cache backend before looking anywhere else.

## Table of contents

- [Symptoms](#symptoms)
- [How to diagnose it](#how-to-diagnose-it)
- [Likely causes](#likely-causes)
- [Hand it to an agent](#verify)
- [Related pages](#related-pages)
- [Sources](#sources)

<a id="symptoms"></a>

## Symptoms

GitHub-hosted runners are ephemeral, so the local Docker layer cache that makes a build fast on a laptop does not exist at the start of a run. If docker/build-push-action is not told where to read and write a cache, BuildKit builds every instruction in the Dockerfile from zero, every time, regardless of whether the app code, the base image, or nothing at all changed since the last run. That is the first thing to rule out because it is also the most common: the action defaults to no cache unless cache-from and cache-to are set explicitly. If caching is already wired in and the build is still slow, the next places to look are containers or compile steps that do not belong on the critical path at all, and a checkout step that spends time on git history the build never reads. The four causes below are ordered by how often each one turns out to be the actual reason, so work through them in this order rather than jumping to the last one.

- The Docker build step takes roughly the same amount of time whether or not the Dockerfile or application code changed since the last run.
- The Actions log shows every instruction in the Dockerfile executing, including FROM, apt-get or apk installs, and dependency installs that did not change.
- There is no line in the build log mentioning CACHED or importing a cache manifest before the layers run.
- The workflow uses docker/build-push-action or a plain docker build command with no reference to a cache location.
- Two workflows on the same push each build the same image from the same Dockerfile, and both take the full build time.

<a id="how-to-diagnose-it"></a>

## How to diagnose it

[ci-speedup](https://starsling.dev/ci-speedup) carries the detection logic behind the causes below and opens the fix as a reviewable pull request. Install it with `npx skills add starslingdev/skills`, then run `/ci-speedup` in your repository.

To check by hand:

1. Open the Actions log for the Docker build step and search for the word `CACHED` next to each instruction; its absence on every layer means nothing was restored.
2. Run `grep -rn 'docker/build-push-action' .github/workflows/` and check whether `cache-from: type=gha` and `cache-to: type=gha,mode=max` are both present on the same step.
3. Run `grep -rn 'docker build\|docker compose build\|docker compose up' .github/workflows/` to catch a plain build invocation or a compose command with no service argument.
4. Run `grep -rnE 'image:\s*[a-zA-Z0-9_./-]+(:latest)?\s*$' docker-compose*.yml .github/workflows/*.yml` to find unpinned image tags that would defeat caching even if it were configured.
5. Run `grep -rl 'on:' .github/workflows/*.yml` and diff the build commands across workflows that trigger on the same push, to rule out the same image being built twice.
6. Run `grep -rn 'fetch-depth' .github/workflows/` on the job that runs the Docker build to rule out a full-history checkout inflating the total job time.

<a id="likely-causes"></a>

## Likely causes

Ordered by how often each one turns out to be the answer. Confirm a cause with its check before you change anything.

### 1. The build has no cache backend configured

docker/build-push-action builds with BuildKit, but BuildKit only reuses layers from a cache it is explicitly pointed at. Without cache-from and cache-to set to type=gha, there is nothing to import at the start of the build and nothing gets exported at the end, so a runner that never saw this repository before builds every layer and the next run starts from the same empty state. A plain docker build or docker compose build invocation has the same gap: neither flag exists to opt in to persistent caching on its own.

Confirm it: Run `grep -rn 'docker/build-push-action' .github/workflows/` and check each match for `cache-from` and `cache-to`. Also run `grep -rn 'docker build\|docker compose build' .github/workflows/` to catch a plain invocation with no caching flags at all.

Fix: [Docker layer caching](https://starsling.dev/github-actions/optimizations/use-docker-layer-caching) (`ci.cache.docker-layers`, detection mode static)

### 2. Compose starts every service and pulls floating image tags

A docker-compose.yml written for local development usually lists every service the app can run against, so a CI job that runs docker compose up with no service argument starts all of them even when the job only connects to one. Floating tags compound this: an image reference with no tag or :latest resolves to whatever the registry currently serves, so the exact image pulled can differ between runs, and a cache keyed on the image reference cannot tell an old :latest from a new one.

Confirm it: Run `grep -rn 'docker compose up' .github/workflows/` and check whether a service name follows it. Run `grep -rnE 'image:\s*[a-zA-Z0-9_./-]+(:latest)?\s*$' docker-compose*.yml .github/workflows/*.yml` to find images with no tag or an explicit `:latest`.

Fix: [Docker build hygiene](https://starsling.dev/github-actions/optimizations/optimize-docker-builds) (`ci.build.docker-builds`, detection mode static)

### 3. The same compile runs again inside or alongside the Docker build

If the Dockerfile's build stage recompiles source that a separate CI job already built, or if the same commit triggers more than one workflow that each build the image from scratch, the Docker step is paying for a compile that already happened elsewhere on the same push. That extra work looks identical to a caching gap in the log, but no cache configuration fixes it: the fix is to stop running the build twice, not to cache the duplicate.

Confirm it: Run `grep -rl 'on:' .github/workflows/*.yml` and check each file's `on.push` or `on.pull_request` block for the same branches, then compare each workflow's build or Dockerfile build-stage commands for the same commit.

Fix: [Duplicate compilation](https://starsling.dev/github-actions/optimizations/avoid-duplicate-compilation) (`ci.build.duplicate-compilation`, detection mode static)

### 4. The checkout step fetches full history the build never reads

A Dockerfile build stage that only needs the working tree at HEAD does not benefit from fetch-depth: 0. When actions/checkout pulls the full git history before the Docker build even starts, that time shows up in the job's total wall clock even though it is not part of the image build itself, and it is easy to mistake for the Docker step being slow.

Confirm it: Run `grep -rn 'fetch-depth' .github/workflows/` and check whether any job feeding into the Docker build sets `fetch-depth: 0` without a reason (changelog generation, git blame, tag history) that actually needs it.

Fix: [Shallow checkout](https://starsling.dev/best-practices/github-actions/shallow-checkout) (`ci.checkout.shallow-clone`, detection mode static)

<a id="verify"></a>

## Hand it to an agent

Hand this prompt to your coding agent (Claude Code, Cursor, and the like) to run this diagnosis against your repository and report which cause it found:

Diagnose why the Docker image build step in this repository's CI rebuilds from scratch on every run. Work through these checks in order and stop at the first one that matches, then report which cause you found before changing anything.

1. Run `grep -rn 'docker/build-push-action' .github/workflows/` and check each match for `cache-from` and `cache-to` set to `type=gha`. Also check for a plain `docker build` or `docker compose build` with no cache flags.
2. Run `grep -rn 'docker compose up' .github/workflows/` for missing service arguments, and `grep -rnE 'image:\s*[a-zA-Z0-9_./-]+(:latest)?\s*$' docker-compose*.yml .github/workflows/*.yml` for unpinned tags.
3. Compare build commands across workflows triggered on the same push to check for the same image being built twice.
4. Run `grep -rn 'fetch-depth' .github/workflows/` on the job that runs the Docker build.

Report the cause you found, the exact file and line, and wait for confirmation before editing any workflow file.

<a id="related-pages"></a>

## Related pages

- [Use Docker layer caching in GitHub Actions](https://starsling.dev/github-actions/optimizations/use-docker-layer-caching)
- [Speed up Docker builds in GitHub Actions](https://starsling.dev/github-actions/optimizations/optimize-docker-builds)
- [Build once and reuse it across GitHub Actions jobs](https://starsling.dev/github-actions/optimizations/avoid-duplicate-compilation)
- [Set actions/checkout fetch-depth: 0 vs 1 vs 2 in GitHub Actions](https://starsling.dev/best-practices/github-actions/shallow-checkout)
- [Why GitHub Actions feels slow, and how to find the real bottleneck](https://starsling.dev/github-actions-too-slow)

<a id="sources"></a>

## Sources

- [Docker docs: GitHub Actions cache backend for BuildKit](https://docs.docker.com/build/ci/github-actions/cache/)
- [Docker docs: cache storage backends overview](https://docs.docker.com/build/cache/backends/gha/)
- [docker/build-push-action on GitHub](https://github.com/docker/build-push-action)
- [actions/checkout on GitHub](https://github.com/actions/checkout)
