Emmanuel Corels
← Blog
Containers & Kubernetes

Rebuild a Docker image without confusing cache with freshness

Separate stale build layers, base-image updates and package-manager resolution; then verify the resulting image by digest and behavior.

By Emmanuel Corels

A developer changes a dependency or expects a patched base image, rebuilds, and the container still behaves like yesterday. “Docker cache” may be involved, but three different freshness decisions are often collapsed into one: whether BuildKit reuses a layer, whether Docker pulls a newer base image, and whether a package manager resolves new dependencies inside a rerun layer.

Read the build record first

docker build --progress=plain   -t registry.example.com/payments:debug .

BuildKit labels reused steps as CACHED. Identify the first unexpected cached layer. Every later layer may also be reused or rebuilt depending on the changed inputs and Dockerfile instruction.

Force every Dockerfile step to execute again

docker build --no-cache   -t registry.example.com/payments:debug .

Docker’s official guidance defines --no-cache as disabling build-cache reuse. It does not guarantee that the base image tag was refreshed from its registry. A locally present base can still be used.

Refresh the base image as a separate decision

docker build --pull --no-cache   -t registry.example.com/payments:debug .

--pull asks Docker to check for a newer version of the base image. Combining it with --no-cache gives a fully re-executed build based on the currently resolved tag. It does not make the result reproducible: mutable tags and unpinned dependencies can change underneath the same source revision.

Prove which base and output you received

docker image inspect   --format '{{.Id}} {{json .RepoDigests}} {{.Created}}'   registry.example.com/payments:debug

docker history --no-trunc   registry.example.com/payments:debug

For repeatable releases, pin the base by digest while retaining a human-readable tag:

FROM python:3.13.5-slim@sha256:<approved-digest>

A deliberate dependency-update process can then change the digest, run tests and record provenance. “Always newest during every build” and “same input produces the same artifact” are different policies.

Package-manager freshness has its own cache rules

FROM debian:bookworm-slim
RUN apt-get update  && apt-get install -y --no-install-recommends curl ca-certificates  && rm -rf /var/lib/apt/lists/*

Docker does not automatically invalidate a RUN apt-get update layer because a repository changed. Pair update and install in one instruction and pin versions when repeatability demands it. A no-cache build reruns the command, but the external repository may now serve different content.

Invalidate only the stage that needs it

docker build   --no-cache-filter install   -t registry.example.com/payments:debug .

Current BuildKit supports stage-specific invalidation. In a multi-stage Dockerfile, this preserves expensive stable layers while rerunning the dependency stage and everything downstream from its changed result.

FROM python:3.13-slim AS install
WORKDIR /app
COPY requirements.lock .
RUN pip install --no-cache-dir -r requirements.lock

FROM python:3.13-slim AS runtime
COPY --from=install /usr/local /usr/local
COPY src/ /app/src/

Fix Dockerfile ordering instead of permanently disabling cache

# Poor: any source edit invalidates dependency installation
COPY . /app
RUN pip install -r /app/requirements.lock

# Better: dependency metadata changes independently
COPY requirements.lock /app/
RUN pip install --no-cache-dir -r /app/requirements.lock
COPY src/ /app/src/

Cache reuse is a performance feature when inputs are modeled correctly. A broad early COPY ., missing lockfile, or oversized context makes it unpredictable. Add a .dockerignore for VCS data, local dependencies, secrets, test output and other irrelevant files.

Secrets do not invalidate cache by content

Docker documents that changing a build secret value does not itself invalidate a cached layer. If the output legitimately depends on a rotated secret, couple the rotation to a non-secret build argument:

docker build   --secret id=TOKEN,env=TOKEN   --build-arg CACHE_EPOCH=2026-07-rotation-2   -t registry.example.com/payments:debug .

Never pass the secret itself as a build argument: arguments can leak through metadata and history. Prefer build steps where credentials authorize downloads but secret bytes do not become image content.

Verify behavior, not only a successful build

docker run --rm   registry.example.com/payments:debug   python -c 'import ssl,sys; print(sys.version); print(ssl.OPENSSL_VERSION)'

docker run --rm -d --name payments-check   -p 127.0.0.1:18080:8080   registry.example.com/payments:debug
curl -fsS http://127.0.0.1:18080/health
docker rm -f payments-check

Record source revision, Dockerfile hash, base digest, output digest, build arguments excluding secrets, SBOM/provenance when available, and test result. If production still runs stale code, verify the deployment pulled and started the new digest rather than assuming the build is at fault.

NeedControl
Rerun every build instruction--no-cache
Check for newer base tag--pull
Rerun one multi-stage boundary--no-cache-filter stage
Reproducible basePin an approved digest
Prove deployed artifactDeploy and record image digest

A clean build answers whether layers were reused. It does not by itself answer which upstream bytes were selected or which image production actually started.

Sources: the solved Stack Overflow question (accepted answer by Assaf Lavie, CC BY-SA), Docker’s official cache invalidation and build best practices.

Primary source: Review the official reference ↗