Chapter 36Lesson 02~220 minutes

Docker in CI/CD: Reproducible Builds, Buildx, Registry Caching, Ephemeral Runners, and Release Promotion: Guided Hands-On Workflow and Core Operations

Run a disposable source-to-image workflow with an ephemeral Buildx builder, bounded cache, tests, digest capture, local registry simulation, and exact digest promotion.

Hands-onRegistry cacheImage testsDigest evidencePromotion

Learning objectives

  • Create a disposable per-lab Buildx builder and record its exact worker/version evidence.
  • Build a synthetic image from a recorded source revision, run a deterministic image test, and capture the resulting digest metadata.
  • Use an explicit local cache directory first, then model a registry-cache path without making cloud services mandatory.
  • Publish to a disposable local registry where the platform supports it, promote the same digest under a release alias, and verify identity.
  • Clean up only exact lab-owned resources.

1. Lab topology and portability note

The mandatory lab uses only Docker, Buildx, Git, shell tools and synthetic content. A local registry is used when your Docker/BuildKit installation can push to loopback without daemon reconfiguration. If your platform blocks that path, execute the build/test/digest/cache steps and use the supplied OCI-layout promotion simulation; the lesson explains which registry claims remain unverified.

Resource prefix: dca36-. No broad prune is used.

2. Preflight and source identity

set -eu
LAB=dca36-lab
mkdir -p "$LAB/src" "$LAB/evidence" "$LAB/cache"

date -u +%Y-%m-%dT%H:%M:%SZ | tee "$LAB/evidence/time-start.txt"
docker context show | tee "$LAB/evidence/context.txt"
docker version | tee "$LAB/evidence/docker-version.txt"
docker buildx version | tee "$LAB/evidence/buildx-version.txt"

git rev-parse HEAD 2>/dev/null | tee "$LAB/evidence/source-sha.txt" || printf 'synthetic-no-git
' | tee "$LAB/evidence/source-sha.txt"

3. Create the deterministic application

cat > dca36-lab/src/app.sh <<'EOF'
#!/bin/sh
set -eu
printf 'release=%s source=%s
' "${RELEASE:-dev}" "${SOURCE_SHA:-unknown}"
EOF
chmod +x dca36-lab/src/app.sh

cat > dca36-lab/src/Dockerfile <<'EOF'
# syntax=docker/dockerfile:1
FROM alpine:3.22
ARG SOURCE_SHA=unknown
LABEL org.opencontainers.image.revision=$SOURCE_SHA
COPY app.sh /usr/local/bin/app
ENTRYPOINT ["/usr/local/bin/app"]
EOF
sha256sum dca36-lab/src/* | tee dca36-lab/evidence/source-files.sha256

4. Provision one ephemeral builder

docker buildx create --name dca36-ci --driver docker-container --bootstrap
docker buildx inspect dca36-ci | tee dca36-lab/evidence/builder.txt

The docker-container driver gives this lab its own BuildKit worker/cache namespace. In hosted CI, runner provisioning should be outside untrusted repository code, as Chapter 35 established.

5. Build with an explicit local cache and metadata file

SOURCE_SHA=$(cat dca36-lab/evidence/source-sha.txt)
docker buildx build   --builder dca36-ci   --build-arg SOURCE_SHA="$SOURCE_SHA"   --cache-from type=local,src=dca36-lab/cache   --cache-to type=local,dest=dca36-lab/cache-next,mode=max   --metadata-file dca36-lab/evidence/build-metadata.json   --load   -t dca36-app:ci   dca36-lab/src
rm -rf dca36-lab/cache
mv dca36-lab/cache-next dca36-lab/cache

The local cache is a CI simulation of an explicit external cache. In a real ephemeral runner, choose a registry or provider backend and use a distinct cache reference/scope per image and trust domain.

6. Test the built image before release publication

SOURCE_SHA=$(cat dca36-lab/evidence/source-sha.txt)
OUT=$(docker run --rm -e RELEASE=test -e SOURCE_SHA="$SOURCE_SHA" dca36-app:ci)
printf '%s
' "$OUT" | tee dca36-lab/evidence/test-output.txt
printf '%s
' "$OUT" | grep -F "source=$SOURCE_SHA"

This is an image-level smoke test. Real pipelines should layer unit/integration/security tests as appropriate, but every test record should identify the digest or build result it evaluated.

7. Record the loaded image identity

docker image inspect dca36-app:ci \
  --format 'id={{.Id}} repoDigests={{json .RepoDigests}} labels={{json .Config.Labels}}'   | tee dca36-lab/evidence/image-inspect.txt
cat dca36-lab/evidence/build-metadata.json

8. Optional local registry publication path

docker run -d --name dca36-registry --label academy.lab=dca36 -p 127.0.0.1:5000:5000 registry:2

docker tag dca36-app:ci 127.0.0.1:5000/dca36/app:build

docker push 127.0.0.1:5000/dca36/app:build   | tee dca36-lab/evidence/push.log

docker buildx imagetools inspect 127.0.0.1:5000/dca36/app:build   | tee dca36-lab/evidence/registry-build.txt

If loopback registry push is unavailable on your platform, stop here and use the OCI-layout simulation in the next section rather than changing daemon trust or adding a global insecure-registry setting.

9. Promote by registry metadata, not rebuild

docker buildx imagetools create   --tag 127.0.0.1:5000/dca36/app:release-1   127.0.0.1:5000/dca36/app:build

docker buildx imagetools inspect 127.0.0.1:5000/dca36/app:release-1   | tee dca36-lab/evidence/registry-release.txt

imagetools create creates a new registry reference from existing registry content. No Dockerfile build step occurs. Compare the digest in the build and release inspection outputs.

10. Registry-cache architecture for ephemeral runners

Production pattern (not required by local lab):
--cache-from type=registry,ref=REGISTRY/ORG/app:buildcache-main
--cache-to   type=registry,ref=REGISTRY/ORG/app:buildcache-main,mode=max

Fork/untrusted PR pattern:
read trusted cache only if policy permits
write to a separate non-release cache scope, or disable cache export
never give fork code release-registry credentials

11. Attestation path

# For a trusted registry that supports OCI attestations:
docker buildx build --builder dca36-ci   --provenance=mode=min --sbom=true   --push -t REGISTRY/ORG/app:ci SOURCE_DIR

This is a reference pattern, not a command to paste with literal placeholders. Current BuildKit adds minimal provenance by default for supported registry outputs; --sbom=true opts into SBOM. In GitHub Actions, current Docker-maintained actions have provider-specific provenance defaults. Bind evidence to the resulting digest.

12. Exact cleanup

docker rm -f dca36-registry 2>/dev/null || true
docker image rm dca36-app:ci 127.0.0.1:5000/dca36/app:build 2>/dev/null || true
docker buildx rm dca36-ci
printf 'Keep dca36-lab/evidence until reviewed.
'

13. Challenge: choose the layer

Your release alias points to the expected digest, but a deployment node pulls a different digest. Which layer do you inspect first? Answer: registry/name-resolution and deployment reference evidence—not BuildKit cache. Confirm the exact repository, registry, platform, tag-to-index resolution and deployment configuration before rebuilding anything.

Knowledge check

Why is the builder explicitly named in every build command?

What proves promotion did not rebuild?

Why is local cache moved from cache-next after the build?

Should you enable a global insecure registry just to complete this lab?

What should a forked PR be allowed to write?

Next lesson

Next: Docker in CI/CD: Reproducible Builds, Buildx, Registry Caching, Ephemeral Runners, and Release Promotion: Configuration, Design Choices, and Tradeoffs

Continue with the next lesson in the course sequence and carry forward the evidence-first Docker operating model.

Official references and version notes

Lab baseline: 2026-09-22. The mandatory workflow uses a disposable docker-container builder and local cache. Registry and hosted-provider integrations are layered on top without making paid/cloud features mandatory.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.