Home » Reproducible Container Builds: What to Pin and What to Update

Reproducible Container Builds: What to Pin and What to Update

3 comments
Two matching stacks of stone layers stand beside a separate green slab on a studio bench.

Pin every input that can change the artifact, define exactly which output must match, and test the claim with independent builds. Then update those pins through a reviewed rebuild process. A digest-pinned base image improves control over one input; it does not, by itself, make the whole build reproducible or keep it secure.

Useful evidence names its boundary. “The application files match,” “the image manifest matches,” and “the complete published image index matches” are different claims. Build metadata and attestations can change even when the runnable application content does not.

Define the artifact before comparing it

A repeatable procedure is a good start: the same source revision, command, builder configuration, and target platform. Reproducibility asks a stronger question about the resulting bytes or content identities under the conditions you specify.

Choose the comparison target before running the build. For a binary, it may be the binary's hash. For a container, decide whether you compare filesystem contents, image configuration and layers, a platform manifest, or the larger index that also references other platforms and attestations. Do not change the definition silently after discovering a mismatch.

Record the source revision, lockfiles, Dockerfile, build arguments, target platform, builder version, base reference, and relevant external artifacts. Include generated input files and their metadata when they affect the build. A working tree with unrecorded local files is not the same input as a clean checkout at the nominal commit.

Pin more than the first FROM line

Docker's build guidance explains that tags can move and digest references select a specific image. For an application image, replace a floating base with a reviewed digest, while retaining a human-readable version in the review record. Verify the platform represented by that reference.

Then inspect the rest of the recipe:

Input What to control Why a base digest is insufficient
Application dependencies Lockfile, package hashes where supported, resolver behavior A package installer can select new packages inside the fixed base
OS packages Available package versions and repository snapshot strategy A live repository can change independently of the image
Downloaded tools Exact release artifact and verified digest A stable URL can serve different bytes
Build toolchain Compiler, frontend, builder and target platform Tools can emit different outputs from the same source
Generated files Inputs, ordering, timestamps and randomness A build-time clock or nondeterministic ordering changes content

Pinning also creates a retention dependency: the selected artifacts must remain retrievable. Preserve them through the organization's artifact policy when long-term rebuilds are required. A reference that was precise but is no longer available cannot reconstruct a release on its own.

What the minimal build experiment showed

We ran a local Docker 29.9.2 and Buildx 0.38.1 exercise on an ARM64 host. The image contained one text file and no operating-system base:

FROM scratch
COPY manifest.txt /manifest.txt

Using scratch removed a base-image download from the experiment. The builds used no cache, disabled build-step networking, and set the same SOURCE_DATE_EPOCH. That variable is documented by Docker as a way to control timestamps in reproducible builds. It does not make arbitrary tools inside a build deterministic. See Docker's reproducible-build guidance.

The first comparison, with default provenance settings, produced different identifiers in the files written by --iidfile. Repeating the comparison with provenance disabled produced matching identifiers for the same input; changing the copied file then produced a different identifier.

Comparison Observed result
Same input, default provenance Output identifiers differed
Same input, provenance disabled Output identifiers matched
Changed file, provenance disabled Output identifier changed

The controlled change isolates provenance configuration as relevant to this comparison. We did not compare every attestation byte or obtain a separate configuration digest from the command's metadata output, so we do not claim that the first pair's runnable contents were identical.

Docker documents default provenance attestations as metadata attached through the image index. Its provenance reference includes build timestamps and build-process details among the recorded facts. This is why the output boundary needs to be explicit before calling a difference a failure of application reproducibility.

Disabling provenance here was a diagnostic experiment. It is not a recommendation to remove supply-chain evidence from release images. In a release workflow, preserve the evidence you need and choose a comparison that accounts for how it is attached.

The fixture did not compile code, resolve dependencies, publish an image, test another architecture, or demonstrate that an application can run. It supports a narrow result about a minimal local recipe. It also shows why “two builds returned different digests” is the start of an investigation rather than its conclusion.

Investigate differences without accepting them blindly

When outputs differ, compare the selected artifact layer by layer. Look first for timestamps, generated version strings, ordering, file metadata, tool versions, external downloads, and platform differences. Keep the original outputs so another reviewer can inspect the same mismatch.

If cached outputs match but independent builds do not, investigate the fresh-build path. Cache reuse can hide a changing input by reusing yesterday's artifact. Conversely, clearing the cache does not freeze a moving package repository. These controls answer different questions.

Build secrets require another boundary. They should be available only to the operation that needs them and should not become image inputs or logged build arguments. Docker's build-variable reference explains the exposure risks of using arguments and persistent environment values for secrets. A reproducible image containing a leaked credential is still an unacceptable release.

Update deliberately, then preserve the tested result

Treat a base or dependency update as a new set of inputs. Review the changed artifact identity and release information, rebuild, run the application's compatibility and security checks, and retain the resulting image identity with the evidence. Rebuilding a pinned image without changing its outdated inputs does not automatically acquire upstream fixes.

Deploy the artifact that passed those checks instead of rebuilding it again during deployment. Keep the previous approved artifact and its configuration available for rollback, while checking whether database or external-state changes permit that rollback.

This process balances stability and maintenance: pins make changes visible, tests evaluate the new selection, and artifact promotion preserves what was evaluated. The reproducibility claim remains only as broad as the inputs recorded and the outputs actually compared.

You may also like