Why Does a Base Image Update Invalidate My Cache?

Every layer after FROM is chained to the base image, so a new base digest invalidates all of them. Pin the digest and schedule the rebuild on your terms.

A Docker build cache is a chain, and the bottom link is the base image that FROM resolves to. When the base image content changes, the FROM layer gets a new cache key, every instruction stacked above it inherits a different parent, and the rest of the Dockerfile rebuilds from scratch.

Answer

BuildKit keys each layer on its parent layer plus the instruction that produced it, and it documents the consequence directly: once one layer is invalidated, all downstream layers are invalidated too, and for FROM the trigger is a change in the base image itself, per the Docker cache invalidation reference, checked on 2026-08-13. A base image update is the worst case for that rule, because FROM sits at position one.

The part that surprises people is that nothing in the repository has to change for this to fire. FROM ubuntu:24.04 is a tag, and a tag is a mutable pointer that the registry can repoint at a new digest whenever the publisher pushes a rebuild. The Dockerfile line reads the same in git log while the content it resolves to is different, per the Dockerfile FROM reference, checked on 2026-08-13.

Here is the same Dockerfile across an upstream tag repoint:

Build on Monday          ubuntu:24.04 -> sha256:aaaa
  [1] FROM ubuntu:24.04                  key: sha256:aaaa        HIT
  [2] RUN apt-get install -y libvips     key: [1] + command      HIT
  [3] COPY package-lock.json .           key: [2] + file digest  HIT
  [4] RUN npm ci                         key: [3] + command      HIT
  [5] COPY . .                           key: [4] + context      HIT

Build on Tuesday         ubuntu:24.04 -> sha256:bbbb
  [1] FROM ubuntu:24.04                  key: sha256:bbbb        MISS  <- chain root moved
  [2] RUN apt-get install -y libvips     parent [1] changed      MISS
  [3] COPY package-lock.json .           parent [2] changed      MISS
  [4] RUN npm ci                         parent [3] changed      MISS
  [5] COPY . .                           parent [4] changed      MISS

Steps 2 through 5 are byte-identical between the two builds. They rebuild anyway, because a layer's identity includes the identity of everything underneath it.

Two things do survive the change:

ElementWhat its cache key includesAfter a base digest change
FROM node:22@sha256:...The resolved base image digestRebuilt
RUN apt-get install -y libvipsParent layer plus the command stringRebuilt
COPY package-lock.json .Parent layer plus the checksum of the copied filesRebuilt
RUN --mount=type=cache,target=/root/.npm npm ciParent layer plus the command; the mounted directory lives outside the layer chainLayer rebuilt, mounted directory reused
A second stage whose own FROM did not moveThat stage's base digest plus its own instructionsReused, until it copies from a rebuilt stage

Cache mounts are the useful exception. A directory mounted with RUN --mount=type=cache is persisted by the builder between builds and is never part of a layer, per the Dockerfile cache mount reference, checked on 2026-08-13, so a package manager download cache or a compiler cache comes back warm even when every layer above the base is being rebuilt. The catch is that the mount lives on the builder, so it survives only where the builder survives.

Multi-stage Dockerfiles are the other place where the blast radius is smaller than it looks. Each stage has its own FROM and its own chain, described in the multi-stage build reference, checked on 2026-08-13. Bumping a slim runtime base rebuilds the runtime stage and leaves an expensive compile stage untouched.

Detail

Pin the digest so the invalidation happens when you decide

FROM accepts a digest, and a digest addresses immutable content:

FROM ubuntu:24.04@sha256:3d1556a8a18cf5307b121e0a98e93f1ddf1f3f8e092f1fddfd941254785b95d7

The tag stays in the line as a readable label and the digest is what the builder resolves. Upstream can repoint ubuntu:24.04 as often as it wants; your build keeps hitting the same chain root until a commit changes the digest. Image digest covers what that string addresses and why it is stable.

Read the current digest for a tag with buildx, using the imagetools inspect reference, checked on 2026-08-13:

docker buildx imagetools inspect ubuntu:24.04 --format '{{.Manifest.Digest}}'

For a multi-architecture tag that returns the digest of the index manifest, which is the right thing to pin: it resolves to the correct per-platform manifest on both amd64 and arm64 builds.

A scheduled workflow that opens the bump pull request

Pinning without a refresh schedule freezes the base image, including its unpatched packages. The pairing that works is a digest pin plus a job on a cron that resolves the tag, rewrites the FROM line, and opens a pull request. Scheduled workflows use the schedule event, documented in the GitHub Actions events reference, checked on 2026-08-13.

name: base-image-update
on:
  schedule:
    - cron: "0 6 * * 1"
  workflow_dispatch:

permissions:
  contents: write
  pull-requests: write

jobs:
  bump-base-image:
    runs-on: warp-ubuntu-latest-x64-2x
    steps:
      - uses: actions/checkout@v4

      - name: Resolve the published digest for ubuntu:24.04
        id: resolve
        run: |
          digest=$(docker buildx imagetools inspect ubuntu:24.04 \
            --format '{{.Manifest.Digest}}')
          echo "digest=$digest" >> "$GITHUB_OUTPUT"

      - name: Rewrite the FROM line
        run: |
          sed -i -E \
            "s|^FROM ubuntu:24\.04@sha256:[0-9a-f]{64}|FROM ubuntu:24.04@${{ steps.resolve.outputs.digest }}|" \
            Dockerfile
          git diff --stat -- Dockerfile

      - name: Open a pull request
        uses: peter-evans/create-pull-request@v7
        with:
          branch: chore/base-image-ubuntu-2404
          commit-message: "chore: bump ubuntu:24.04 to ${{ steps.resolve.outputs.digest }}"
          title: "Bump ubuntu:24.04 base image"
          body: |
            Scheduled base image refresh.

            Merging this rebuilds every layer above FROM. That is the intended
            trade: the rebuild is paid here, on one reviewed pull request, rather
            than on whichever feature branch happened to build first after
            upstream repointed the tag.

The action opens no pull request when the digest is unchanged, per the create-pull-request repository, so a quiet week produces a no-op run at the 2 vCPU rate. Run the same job for every base image the repository pins, including the ones inside multi-stage builds, and give each one its own branch name so the pull requests stay reviewable on their own.

What the two approaches cost

The rebuild itself is fixed work. The variable is how many separate cache scopes pay for it. Assumptions here are stated so you can substitute your own from a job log: 600 Docker builds a month, a warm build at 4 minutes and a full cold rebuild at 22 minutes, upstream repointing the tag 4 times in the month, and roughly 7 active branch cache scopes on a busy repository. The runner is warp-ubuntu-latest-x64-16x at $0.032 per minute from the pricing page, checked on 2026-08-13.

LineUnpinned tagDigest pinned, weekly bump
Full cold rebuilds per month284
Extra runner minutes50472
Extra runner cost per month$16.13$2.30
Where the rebuild landsOn whichever pull requests build first after each repointOn the bump pull request
Who waits 22 minutesWhoever opened those pull requestsThe reviewer of one scheduled pull request

The dollars are small and the scheduling is the point. Twenty-eight surprise 22-minute builds spread across a month land on people who changed one line of application code and have no idea why their build went cold.

The branch-scope multiplier disappears when the layer cache stops being per branch. A WarpBuild remote Docker builder profile maps to a dedicated builder virtual machine that keeps its layer cache on local disk and shares it across every job pointed at that profile, per the Docker builders documentation, checked on 2026-08-13. One build repopulates the chain after a base change and the jobs behind it read the warm layers, so the cold rebuild count tracks the number of digest changes rather than the number of branches. That builder cache has a 10 day TTL, so a profile left unused for more than 10 days is reset and the next build is cold for the same reason a base bump is. Builder sizes start at 16 vCPU, 32 GB, and 100 GB of disk at $0.06 per minute on the pricing page, checked on 2026-08-13, and the builder is billed separately from the runner. For dependency caching that has nothing to do with Docker layers, the WarpBuild caching documentation covers the drop-in replacement for actions/cache.

Linux runners are the usual host for a Docker build job. The Docker builds on GitHub Actions hub covers the rest of the pipeline.

Does pinning by digest stop base image security updates from reaching my image?

Pinning freezes the base image until a commit changes the digest, so the update arrives on the scheduled bump rather than on the next build. That is why the pin and the cron ship as one change: the pin decides that invalidation happens on a reviewable commit, and the schedule decides that it happens every week. The base image update workflow guide covers the full job, including repositories that pin several bases, and image digest defines what the pinned string addresses.

Why did only some of my build stages rebuild?

Each stage in a multi-stage Dockerfile carries its own FROM and its own cache chain, per the multi-stage build reference. Bumping the runtime stage base rebuilds the runtime stage and leaves a cached builder stage alone. Bumping the builder stage base rebuilds that stage plus every later stage that copies from it with COPY --from. The Docker cache invalidation guide works through the ordering rules that decide which stages are affected.

Can I avoid the rebuild after a base image change entirely?

No. New base content produces new layer content for everything above it, so the work happens once by definition, per the Docker cache invalidation reference. What you control is the timing and the repeat count. A digest pin plus a scheduled bump moves the rebuild onto one pull request, and a builder that holds its layer cache on local disk means one job repopulates the chain for every job behind it, per the Docker builders documentation. The Docker builds on GitHub Actions hub puts both moves in the context of a full pipeline.

Start with $10 in free credits

Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.