Heads builds inside a versioned Docker image that provides a reproducible, hermetic build environment. Docker images are built with Nix since PR #1661.
See also: General reproducible-build notes, QEMU testing.
The short path to build Heads is to do what CircleCI does:
- Install Docker CE for your OS
- Run
./docker_repro.sh make BOARD=XYZ
# Canonical, reproducible build (recommended for all users)
./docker_repro.sh make BOARD=x230-hotp-maximized
# Build and run a QEMU board
./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run./docker_repro.sh is the canonical, reproducible way to build and test Heads.
docker_local_dev.sh is intended for developers who need to modify the local image built
from flake.nix/flake.lock and is not recommended for general testing.
The supported and tested workflow uses the provided Docker wrappers
(./docker_repro.sh, ./docker_local_dev.sh, or ./docker_latest.sh). Host-side
installation of QEMU, swtpm, or other QEMU-related tooling is unnecessary for the
standard workflow and is not part of the tested configuration. Only advanced or edge-case
workflows may require installing those tools on the host (see qemu.md).
The Docker images produced by our Nix build include QEMU (qemu-system-x86_64),
swtpm / libtpms, canokey-qemu (a virtual OpenPGP smartcard), and other userspace
tooling required to build and test QEMU boards. You only need Docker on the host. For KVM
acceleration expose /dev/kvm (load kvm_intel / kvm_amd); the wrapper scripts mount
it automatically when present.
If you plan to manage disk images or use qemu-img snapshots on the host (outside
containers), install the qemu-utils package locally (which provides qemu-img).
Three wrappers cover different use cases:
| Script | Use case | Reproducibility | When to use |
|---|---|---|---|
./docker_repro.sh |
Canonical reproducible builds | Pinned to immutable digest | All users & maintainers: Standard way to build Heads; matches CircleCI exactly; use for releases and critical builds |
./docker_local_dev.sh |
Developer customization | Local build may differ if flake changes | Developers only: Rebuilds from local flake.nix/flake.lock when dirty; use HEADS_CHECK_REPRODUCIBILITY=1 to verify against published version |
./docker_latest.sh |
Convenience | Defaults to reproducible digest; may be unpinned if no digest is available | Testing/convenience: Uses latest published image; by default falls back to the reproducible digest (DOCKER_REPRO_DIGEST) when available (no confirmation needed). Runs unpinned only when no digest is configured, in which case it requires confirmation unless HEADS_ALLOW_UNPINNED_LATEST=1 or DOCKER_LATEST_DIGEST is set. |
Recommendation by role:
- End users & QA: Use
./docker_repro.shfor all builds (ensures reproducibility and security) - Developers: Use
./docker_local_dev.shwhen iterating on the build system or Nix flake, but verify reproducibility withHEADS_CHECK_REPRODUCIBILITY=1before committing - Maintainers: Use
./docker_repro.shfor official releases; see Maintenance workflow
Examples:
# Canonical builds
./docker_repro.sh make BOARD=x230-hotp-maximized
./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run
# Developer workflow (verify before committing)
./docker_local_dev.sh make BOARD=nitropad-nv41
HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.sh make BOARD=nitropad-nv41If you are already inside the container interactively, run make BOARD=board_name as usual.
# Build ROM, then export public key to emulated USB storage at QEMU runtime
./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2
# Inject a GPG public key into the ROM image
./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 PUBKEY_ASC=~/pubkey.asc inject_gpg
# Full install run with hardware token, disk image, and install ISO
./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 \
USB_TOKEN=Nitrokey3NFC \
PUBKEY_ASC=~/pubkey.asc \
ROOT_DISK_IMG=~/qemu-disks/debian-9.cow2 \
INSTALL_IMG=~/Downloads/debian-9.13.0-amd64-xfce-CD-1.iso \
runIf you do not specify USB_TOKEN, the container uses the included canokey-qemu virtual
token by default. Set USB_TOKEN (or use hostbus/hostport/vendorid,productid) to
forward a hardware token instead. See qemu.md for details.
Each wrapper shows its own focused help (only variables it actually uses). For the complete
environment reference run docker/common.sh directly:
# Wrapper-specific help
./docker_repro.sh --help
./docker_latest.sh --help
./docker_local_dev.sh --help
# Full environment variable reference (shared helper)
./docker/common.shThe shared helper documents all supported environment variables (opt-ins and opt-outs) and defaults. Wrapper help is intentionally narrower so it only lists variables relevant to that wrapper.
HEADS_MAINTAINER_DOCKER_IMAGE — override the canonical maintainer's Docker image
repository (default: tlaurion/heads-dev-env). Use this for local testing or if you
maintain a fork. Example: export HEADS_MAINTAINER_DOCKER_IMAGE="myuser/heads-dev-env".
This affects reproducibility checks and default image references across all Docker wrapper
scripts.
HEADS_CHECK_REPRODUCIBILITY_REMOTE — specify which remote image to compare against
when verifying reproducibility (default: ${HEADS_MAINTAINER_DOCKER_IMAGE}:latest). Use
this to test against a specific tagged version instead of :latest.
# Compare against a specific version
export HEADS_CHECK_REPRODUCIBILITY_REMOTE="tlaurion/heads-dev-env:v0.2.7"
HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.shHEADS_DISABLE_USB=1 — disable automatic USB passthrough and the automatic USB
cleanup (default: 0).
HEADS_X11_XAUTH=1 — force mounting your ${HOME}/.Xauthority into the container
for X11 authentication. When set the helper will bypass programmatic Xauthority generation
and mount your ${HOME}/.Xauthority (if present); if the file is missing the helper will
warn and will not attempt automatic cookie creation (GUI may fail).
HEADS_SKIP_DOCKER_REBUILD=1 — skip automatically rebuilding the local image when
flake.nix/flake.lock are dirty.
HEADS_CHECK_REPRODUCIBILITY=1 — recommended for verifying reproducible builds.
After building/loading the local image, automatically compares its digest with the
published maintainer image to verify reproducibility. Requires network access. By default
compares against ${HEADS_MAINTAINER_DOCKER_IMAGE}:latest. Use
HEADS_CHECK_REPRODUCIBILITY_REMOTE to specify a different tag (e.g., v0.2.7). See
Verifying reproducibility below for detailed examples.
HEADS_AUTO_INSTALL_NIX=1 — automatically attempt to download the Nix single-user
installer when nix is missing (interactive prompt suppressed).
For supply-chain safety the helper will download the installer to a temporary file and
print its SHA256; it will NOT execute the installer automatically unless the downloaded
installer matches a pinned hash. The helper will also attempt to detect the installer
version heuristically (when possible) and suggest the canonical releases URL (for example
https://releases.nixos.org/nix/nix-2.33.2/install.sha256) so you can fetch the
published sha and compare. To verify:
-
Preferred — pin a release version: set
HEADS_NIX_INSTALLER_VERSIONto a release (for examplenix-2.33.2). The helper will fetchhttps://releases.nixos.org/nix/${HEADS_NIX_INSTALLER_VERSION}/installandinstall.sha256and show both checksums for you to compare. To auto-run in trusted automation, setHEADS_NIX_INSTALLER_SHA256to the expected sha256 as well. -
Or compute-and-pin locally: run
./docker/fetch_nix_installer.sh --version nix-2.33.2(or--url) to download the installer and print its sha256, then setHEADS_NIX_INSTALLER_SHA256to that value for automation.Otherwise verify the downloaded installer manually and run it yourself:
sh /path/to/installer --no-daemon.
HEADS_AUTO_ENABLE_FLAKES=1 — automatically enable flakes by writing
experimental-features = nix-command flakes to $HOME/.config/nix/nix.conf
(interactive prompt suppressed).
HEADS_MIN_DISK_GB — minimum free disk space in GB required on /nix (or / if
/nix missing) for building (default: 50).
HEADS_SKIP_DISK_CHECK=1 — skip the preflight disk-space check.
HEADS_ALLOW_UNPINNED_LATEST=1 — when set, bypass the interactive warning that using
:latest in ./docker_latest.sh is a supply-chain risk (otherwise :latest requires
confirmation unless DOCKER_LATEST_DIGEST is set or the wrapper can fall back to
DOCKER_REPRO_DIGEST for the maintainer image).
DOCKER_LATEST_DIGEST — pin the convenience wrapper to a specific immutable digest.
DOCKER_REPRO_DIGEST — pin the image used by ./docker_repro.sh to an immutable
digest: tlaurion/heads-dev-env@<digest> (recommended for reproducible and secure
builds). Note: DOCKER_REPRO_DIGEST is consumed by ./docker_repro.sh via
resolve_docker_image in docker/common.sh and is the canonical way to pin the repro
image for reproducible builds. The repository file docker/DOCKER_REPRO_DIGEST contains
the pinned digest used by default.
When USB passthrough is active the wrappers will detect processes that may be holding a
USB token (for example scdaemon or pcscd). The wrapper will warn and, on interactive
shells, give a 3-second abort window before attempting to kill those processes to free
the token. Set HEADS_DISABLE_USB=1 to opt out of this automatic cleanup.
HEADS_DISABLE_USB=1 ./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 runFor details about selecting or forwarding a physical USB token to QEMU (handled by the
USB_TOKEN make variable), see qemu.md.
Note: you may need to prefix commands with sudo depending on your Docker setup.
# List local images
docker images
# Inspect a specific image (IDs, digests, repo tags)
docker image inspect <image>
# Remove a specific image
docker rmi <image>
# Remove all local images (destructive)
docker rmi -f $(docker images -aq)
# Remove unused images/containers/networks/build cache (destructive)
docker system prune -a --volumesIf you manage qcow2 disk images on the host, qemu-img can create, list, restore, and
delete snapshots. These examples assume a qcow2 disk image:
# Create a snapshot
qemu-img snapshot -c clean root.qcow2
# List snapshots
qemu-img snapshot -l root.qcow2
# Restore (apply) a snapshot
qemu-img snapshot -a clean root.qcow2
# Delete a snapshot
qemu-img snapshot -d clean root.qcow2
# Optional: create an overlay backed by a base image
qemu-img create -f qcow2 -b base.qcow2 overlay.qcow2If you prefer to run these inside the container, prefix with ./docker_repro.sh:
./docker_repro.sh qemu-img snapshot -l root.qcow2The canonical, reproducible way to build Heads is to use ./docker_repro.sh, which
automatically pulls the pinned Docker image digest from docker/DOCKER_REPRO_DIGEST and
ensures your builds match the CI environment exactly.
./docker_repro.sh make BOARD=x230-hotp-maximized
./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 runThis will:
- Resolve the canonical image digest from
docker/DOCKER_REPRO_DIGEST(immutable, pinned to a specific version) - Pull the image if not present locally
- Execute your build inside that exact Docker environment
- Guarantee reproducibility: your ROM output will match official CircleCI builds for that commit
About the published image:
- Repository:
tlaurion/heads-dev-envon Docker Hub is the maintainer's canonical image (configurable viaHEADS_MAINTAINER_DOCKER_IMAGE) - Versioning: Tagged with version numbers (e.g.,
v0.2.7) for stability;:latestis mutable and not recommended - Pinning: The repository file
docker/DOCKER_REPRO_DIGESTpins an immutable digest (tlaurion/heads-dev-env@sha256:...) to ensure reproducibility - Trust: As long as
flake.nixandflake.lockare not modified locally, your build will produce identical digests, confirming integrity - Fork/Override: To use a different image repository, set
HEADS_MAINTAINER_DOCKER_IMAGE="youruser/your-image"before running any Docker wrapper script
DOCKER_REPRO_DIGEST (the environment variable or the repository file docker/DOCKER_REPRO_DIGEST)
is consumed by ./docker_repro.sh via resolve_docker_image(); pinning ensures
reproducible builds and mitigates supply-chain risk from mutable :latest tags.
./docker_local_dev.sh is a developer helper that ensures a local Nix-based Docker image
(linuxboot/heads:dev-env) is available for interactive development. It performs preflight
checks and interactive prompts to make the process easier:
- Ensures
nixis installed and flakes are enabled; if missing it will prompt to install Nix and enable flakes. SetHEADS_AUTO_INSTALL_NIX=1and/orHEADS_AUTO_ENABLE_FLAKES=1to suppress prompts and proceed automatically. - Requires either
curlorwgetto fetch the Nix installer; if neither is present the script will print how to install one and abort. - Checks disk space on
/nix(or/if/nixis absent); default minimum is 50 GB (HEADS_MIN_DISK_GB=50) — override or skip the check withHEADS_SKIP_DISK_CHECK=1. - If
flake.nixorflake.lockare dirty (uncommitted changes), the helper will rebuild the local Docker image. To intentionally trigger a rebuild, make and keep changes toflake.nix(for example update an input or a harmless comment) or updateflake.lock, then run./docker_local_dev.sh; the helper detects the dirty flake files and will rebuild automatically. To avoid an automatic rebuild, commit or stash your changes or setHEADS_SKIP_DOCKER_REBUILD=1to disable the check.
Notes on automation:
- The
./docker_local_dev.shhelper will attempt to ensure Nix and flakes are available when you run it interactively. SetHEADS_AUTO_INSTALL_NIX=1/HEADS_AUTO_ENABLE_FLAKES=1to suppress prompts. - Building the Docker image and populating
/nixcan require significant disk space — at least 50 GB free on/nix(or/if/nixis not present). Adjust viaHEADS_MIN_DISK_GBor skip the check withHEADS_SKIP_DISK_CHECK=1. - The Nix installer requires a downloader; either
curlorwgetmust be available on the host. The helper will guide you to install one if neither is present. - For reproducible builds prefer
./docker_repro.sh;./docker_local_dev.shis intended for development and will rebuild the local image whenflake.nix/flake.lockare dirty (unlessHEADS_SKIP_DOCKER_REBUILD=1).
If you don't already have Nix, install it:
[ -d /nix ] || sh <(curl -L https://nixos.org/nix/install) --no-daemon
. /home/user/.nix-profile/etc/profile.d/nix.shEnable flake support in nix:
mkdir -p ~/.config/nix
echo 'experimental-features = nix-command flakes' >>~/.config/nix/nix.conf# Manual
nix --print-build-logs --verbose build .#dockerImage && docker load < result
# Via helper (rebuilds automatically when flake files are dirty)
./docker_local_dev.shYour local docker image linuxboot/heads:dev-env is ready to use, reproducible for the
specific Heads commit used to build it, and will produce ROMs reproducible for that commit ID.
On some hardened OSes, you may encounter problems with ptrace:
> proot error: ptrace(TRACEME): Operation not permitted
The most likely reason is that your kernel.yama.ptrace_scope variable is too high and doesn't allow docker+nix to run properly. You'll need to temporarily set it to 1 while you build:
sudo sysctl kernel.yama.ptrace_scope # show current value (probably 2 or 3)
sudo sysctl -w kernel.yama.ptrace_scope=1 # lower for the build
# ... build ...
sudo sysctl -w kernel.yama.ptrace_scope=<original_value> # restore after# Verify local image matches maintainer's latest
HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.sh
# Verify against a specific version
HEADS_CHECK_REPRODUCIBILITY=1 \
HEADS_CHECK_REPRODUCIBILITY_REMOTE="tlaurion/heads-dev-env:v0.2.7" \
./docker_local_dev.sh- Setup Nix persistent layer under QubesOS (Thanks @rapenne-s!)
- Install Docker under QubesOS
Best practice: Verify that your locally-built Docker image is reproducible by comparing its digest with the published maintainer image.
The Heads project maintains the canonical tlaurion/heads-dev-env Docker image on Docker
Hub (configurable via HEADS_MAINTAINER_DOCKER_IMAGE for forks or testing). As long as
you do not modify flake.nix or flake.lock, your locally-built image should produce
an identical digest to the published image, demonstrating that your build is fully
reproducible.
| Scenario | Command |
|---|---|
| Check against latest maintainer image | HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.sh |
| Check against specific version tag | HEADS_CHECK_REPRODUCIBILITY=1 HEADS_CHECK_REPRODUCIBILITY_REMOTE="tlaurion/heads-dev-env:v0.2.7" ./docker_local_dev.sh |
| Check fork maintainer's image | HEADS_MAINTAINER_DOCKER_IMAGE="youruser/heads-dev-env" HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.sh |
| Standalone check at any time | ./docker/check_reproducibility.sh linuxboot/heads:dev-env tlaurion/heads-dev-env:v0.2.7 |
You have either:
- Built a local Docker image with
./docker_local_dev.sh(produceslinuxboot/heads:dev-env), or - Built from
nix build .#dockerImage(results inresultsymlink loadable viadocker load)
Enable reproducibility verification automatically during your build with
HEADS_CHECK_REPRODUCIBILITY=1:
# Verify against the default (maintainer's :latest image)
HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.sh
# Example output when digests MATCH (reproducible build):
# === Reproducibility Check ===
# Local image (linuxboot/heads:dev-env): sha256:5f890f3d...
# Remote image (tlaurion/heads-dev-env:latest): sha256:5f890f3d...
# ✓ MATCH: Local build is reproducible!To test against a specific version tag instead of :latest:
HEADS_CHECK_REPRODUCIBILITY=1 \
HEADS_CHECK_REPRODUCIBILITY_REMOTE="tlaurion/heads-dev-env:v0.2.7" \
./docker_local_dev.sh
# Example output when digests DIFFER (expected for different versions):
# === Reproducibility Check ===
# Local image (linuxboot/heads:dev-env): sha256:5f890f3d...
# Remote image (tlaurion/heads-dev-env:v0.2.6): sha256:75af4c81...
# ✗ MISMATCH: Local build differs from remote
# (This is expected if Nix/flake.lock versions differ or if uncommitted changes exist)Note: Docker images can have two different identifiers: a local image ID and a registry
manifest digest. If a local image has no RepoDigests entry, the reproducibility check
will compare image IDs (and may pull the remote tag) instead of manifest digests to avoid
false mismatches. This can happen for locally built images that have not been pulled from
a registry.
# Compare your local dev image with a published version
./docker/check_reproducibility.sh linuxboot/heads:dev-env tlaurion/heads-dev-env:v0.2.7
# Output (example of a match):
# ✓ SUCCESS: Digests match!
# Your local build is reproducible and identical to tlaurion/heads-dev-env:v0.2.7# Get the digest of your local image (after docker load)
docker inspect --format='{{.Id}}' linuxboot/heads:dev-env
# Compare with the published image (will pull if needed)
docker pull tlaurion/heads-dev-env:v0.2.7
docker inspect --format='{{.Id}}' tlaurion/heads-dev-env:v0.2.7✓ Digests match — your build is reproducible and trustworthy; matches the maintainer's published image for that Nix snapshot. Happens when:
flake.nixandflake.lockare not modified (repository is clean relative to these files)- The same Nix version and dependencies are used
- Build runs on the same Nix store state
✗ Digests differ — expected when:
- You have uncommitted changes in
flake.nixorflake.lock - Different Nix version or Nix dependencies resolved differently on your system
- Using a different
nixpkgsversion than the locked one inflake.lock
The tlaurion/heads-dev-env image on Docker Hub is the maintainer's canonical build
and serves as the source of truth for reproducibility. By verifying that your
locally-built image produces the same digest as the published version you confirm:
- No tampering: Your build environment has not been compromised
- Reproducibility: The Heads build system is deterministic for your specific Nix snapshot
- Auditability: You can map your build back to a specific published, reviewed version
Recommendation: Always pin to a specific version tag (e.g., tlaurion/heads-dev-env:v0.2.7)
rather than :latest, and verify the digest matches the published value before using it
for critical builds.
We do not maintain a docker/DOCKER_LATEST_DIGEST file in the repository because
latest is a user-level convenience and should be explicitly chosen. When
DOCKER_LATEST_DIGEST is unset, ./docker_latest.sh may fall back to DOCKER_REPRO_DIGEST
only when the base image matches the maintainer repo; otherwise it will prompt before
using an unpinned :latest unless HEADS_ALLOW_UNPINNED_LATEST=1 is set.
# 1) Obtain the digest for a published image
# Tip: inspect tags on Docker Hub: https://hub.docker.com/layers/tlaurion/heads-dev-env/
#
./docker/get_digest.sh tlaurion/heads-dev-env:v0.2.7
# Output (example): tlaurion/heads-dev-env@sha256:50a9110c...
# Auto-pull and return digest in one go:
./docker/get_digest.sh -y tlaurion/heads-dev-env:v0.2.7
# 2) Export and use the digest
export DOCKER_LATEST_DIGEST=$(./docker/get_digest.sh tlaurion/heads-dev-env:latest | tail -n1)
DOCKER_LATEST_DIGEST=$DOCKER_LATEST_DIGEST ./docker_latest.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2When a digest is discovered, helpers print a concise summary to help auditing:
Image: tlaurion/heads-dev-env@sha256:50a9...
Digest: sha256:50a9...
Resolved from: local|registry API|env|file
Tip: export DOCKER_LATEST_DIGEST=sha256:50a9...
To change what ./docker_latest.sh uses as the "latest" image:
- Temporary override:
./docker/pin-and-run.sh <repo:tag> -- ./docker_latest.sh <command> - Local convenience env:
export DOCKER_LATEST_DIGEST=$(./docker/get_digest.sh tlaurion/heads-dev-env:vX.Y.Z | tail -n1) - Canonical fallback: edit
docker/DOCKER_REPRO_DIGESTwith the desired digest and commit
# pin-and-run helper examples
./docker/pin-and-run.sh tlaurion/heads-dev-env:v0.2.7 -- ./docker_latest.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2
./docker/pin-and-run.sh -y tlaurion/heads-dev-env:v0.2.7 -- ./docker_latest.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2
# Omit the wrapper — helper defaults to './docker_latest.sh'
./docker/pin-and-run.sh tlaurion/heads-dev-env:v0.2.7 -- make BOARD=qemu-coreboot-fbwhiptail-tpm2
# Explicit wrapper flag (avoids ambiguity)
./docker/pin-and-run.sh -w ./docker_repro.sh tlaurion/heads-dev-env:v0.2.7 -- make BOARD=qemu-coreboot-fbwhiptail-tpm2Alternative manual commands without the helper:
docker pull tlaurion/heads-dev-env:latest
# prints full repo@digest (if available)
docker inspect --format='{{index .RepoDigests 0}}' tlaurion/heads-dev-env:latest
# to get only the digest portion:
docker inspect --format='{{index .RepoDigests 0}}' tlaurion/heads-dev-env:latest | cut -d'@' -f2Notes: some registries or Docker versions may require docker manifest inspect or
skopeo inspect to obtain an authoritative digest; the helper script tries
docker inspect first, then docker manifest inspect when available.
Acceptable digest formats for DOCKER_REPRO_DIGEST / DOCKER_LATEST_DIGEST:
sha256:<64-hex>, sha256-<64-hex>, or bare <64-hex> — all normalized to sha256:<hex>.
To update the Docker image to a new version (e.g., vx.y.z):
docker_version="vx.y.z"
docker_hub_repo="tlaurion/heads-dev-env"
# Update pinned packages to latest if needed, modify flake.nix as required
nix flake update
# Commit flake changes
git add flake.nix flake.lock
git commit --signoff -m "Bump nix develop based docker image to $docker_version"
# Verify reproducibility: ensure the local build matches (no further changes to flake files)
nix develop --ignore-environment --command true
# Build the new Docker image
nix build .#dockerImage
docker load < result
# Verify you can extract the digest (flake.nix/flake.lock must be committed)
docker inspect --format='{{.Id}}' linuxboot/heads:dev-env
# Tag the image with the new version
docker tag linuxboot/heads:dev-env "$docker_hub_repo:$docker_version"
# Push the new version to Docker Hub (requires push access)
docker push "$docker_hub_repo:$docker_version"
# Capture the digest of the pushed image (use --yes to auto-pull)
new_digest=$(./docker/get_digest.sh -y "$docker_hub_repo:$docker_version" | tail -n1)
prev_digest=$(grep '^[^#]' docker/DOCKER_REPRO_DIGEST | head -n1)
# Update the digest in the repository file
sed -i "s|$prev_digest|$new_digest|" docker/DOCKER_REPRO_DIGEST
# Update the version comment in the repository file
sed -i "s|# Version: .*|# Version: $docker_version|" docker/DOCKER_REPRO_DIGEST
# Update .circleci/config.yml (remove old comment, insert fresh one above the image line)
sed -i \
-e "/^[[:space:]]*# Docker image: /d" \
-e "/^[[:space:]]*- image: ${docker_hub_repo//\//\\/}@/ s|^\([[:space:]]*\)\(- image: ${docker_hub_repo//\//\\/}@\)|\\1# Docker image: $docker_hub_repo:$docker_version\n\\1\\2|" \
.circleci/config.yml
# Commit the digest and config changes
git add docker/DOCKER_REPRO_DIGEST .circleci/config.yml
git commit --signoff -m "Pin docker image to digest for $docker_version"
# Push the branch and create a PR for testing with CircleCI
git push origin docker/squash-docker-changes
# After PR is merged and tested, optionally tag as latest (use with caution)
# docker tag "$docker_hub_repo:$docker_version" "$docker_hub_repo:latest"
# docker push "$docker_hub_repo:latest"- Reproducibility: Before pushing, verify
nix build .#dockerImageproduces a deterministic result (flake.nixandflake.lockmust be committed and clean). - Digest verification: After pushing, use
./docker/check_reproducibility.shto verify local and remote digests match. - Supply chain: Pin digest in
docker/DOCKER_REPRO_DIGESTand.circleci/config.ymlto ensure all builds reference an immutable, auditable image. - Documentation: Update the version comment in
docker/DOCKER_REPRO_DIGESTso users know which image version is pinned. - User migration: When releasing a new version, communicate the new digest and version in release notes.
Notes:
- Local builds can use
:latesttag, which will use the latest tested successful CircleCI run - To reproduce CircleCI results, make sure to use the same versioned tag declared under
.circleci/config.yml'simage:
export HEADS_MAINTAINER_DOCKER_IMAGE="youruser/heads-dev-env"
# All scripts will now reference your repository
./docker_local_dev.sh make BOARD=x230
HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.sh
# Reproducibility check compares against youruser/heads-dev-env:latest
# resolve_docker_image uses youruser/heads-dev-env as the base imageThe repository file docker/DOCKER_REPRO_DIGEST pins the canonical reproducible image
used by ./docker_repro.sh, ensuring immutable, secure builds. Update the appropriate
file after publishing a new image to keep the repo in sync.