Chapter 11Lesson 02~140 minutes

Build Cache, Cache Mounts, Bind Mounts, Secret Mounts, SSH Mounts, and Cache Import/Export: Guided Hands-On Workflow and Core Operations

Use one disposable Buildx builder to observe cold and warm builds, instruction reordering, a persistent package-manager cache, a read-only build bind mount, a fake BuildKit secret, an ephemeral SSH-agent mount, and a local external cache. Every step records plain-progress evidence so speed never replaces explainability.

Hands-onCold vs warmCache mountSecret/SSH mountLocal cache

Learning objectives

  • Create and inspect an isolated docker-container builder without changing production daemon configuration or shared builders.
  • Measure cold and warm builds with plain progress and distinguish layer-cache reuse from package-manager cache-mount reuse.
  • Use read-only bind, cache, secret, and SSH RUN mounts and explain what persists after each instruction.
  • Export a local external cache and import it into a fresh builder while keeping output identity and cache identity separate.
  • Clean up only chapter-owned builders/images/files and preserve the evidence needed to explain every observed speedup.
Chapter 11 evidence baseline — verified 2026-09-21. Mandatory exercises use only synthetic source/data, a fake token, an ephemeral local SSH identity, isolated chapter-owned Buildx builders, a local cache directory, and public base images. At verification time Docker Engine 29.8.1, Buildx 0.37.1, and BuildKit 0.33.0 with built-in Dockerfile frontend 1.27.0 are the current course baselines, but every lab records the actual installed versions, builder driver, worker platform, and frontend behavior. Docker currently documents bind/cache/secret/SSH RUN mounts; cache sharing modes shared/private/locked; secret values excluded from cache checksum; and local/inline/registry/GHA cache backends with driver-dependent support. Cache is never treated as release identity, and no real credentials, production registry, production daemon, or broad prune operation is required.

1. Lab scope and safety boundaries

This workflow creates one isolated docker-container builder, a synthetic Python application, a fake secret, an optional ephemeral SSH agent, and a local cache directory. It does not push an image, contact a private repository, modify daemon configuration, mount the host Docker socket into a container, or use real credentials. Internet access is useful for the first pinned Python dependency download; if your environment is offline, the cache and secret/SSH mechanics can still be studied with the synthetic fallback described below.

Use only fake secrets. The lesson intentionally inspects history/configuration and build logs. Never substitute a real API token or private production key.

2. Preflight: capture the builder before changing anything

mkdir -p ch11-lab/{evidence,cache-out}
cd ch11-lab

docker version | tee evidence/00-docker-version.txt
docker context show | tee evidence/01-context.txt
docker buildx version | tee evidence/02-buildx-version.txt
docker buildx ls | tee evidence/03-builders-before.txt

docker buildx create \
  --name devops-academy-ch11 \
  --driver docker-container \
  --use

docker buildx inspect --bootstrap \
  | tee evidence/04-builder.txt

Record the BuildKit version and worker platform from inspect --bootstrap. The builder name is intentionally unique and chapter-scoped so cleanup never depends on list ordering.

3. Create bounded synthetic inputs

cat > app.py <<'PYAPP'
import requests
print("chapter11", requests.__version__)
PYAPP

cat > requirements.txt <<'REQ'
requests==2.32.5
REQ

printf '%s\n' 'chapter11-fake-token-v1' > fake-secret.txt
printf '%s\n' 'not-copied-build-note' > build-note.txt

sha256sum app.py requirements.txt build-note.txt \
  | tee evidence/05-input-sha256.txt

The dependency is pinned by version for the exercise, but a fully reproducible Python supply chain would also record package hashes, index identity, base-image digest, and platform. The lab’s purpose is cache mechanics, not a claim that PyPI state is immutable.

4. Dockerfile: four mount types with visible boundaries

# syntax=docker/dockerfile:1
FROM python:3.13-alpine AS build
WORKDIR /work

COPY requirements.txt .
RUN --mount=type=cache,id=ch11-pip,target=/root/.cache/pip,sharing=locked \
    pip install --prefix=/install -r requirements.txt

RUN --mount=type=bind,source=build-note.txt,target=/mnt/build-note.txt,ro \
    sha256sum /mnt/build-note.txt > /tmp/build-note.sha256

RUN --mount=type=secret,id=demo,required=true \
    test -s /run/secrets/demo && echo 'secret-present-during-build' > /tmp/secret-status

RUN --mount=type=ssh,required=true \
    test -n "$SSH_AUTH_SOCK" && test -S "$SSH_AUTH_SOCK" && \
    echo 'ssh-agent-socket-present' > /tmp/ssh-status

COPY app.py .

FROM python:3.13-alpine AS runtime
COPY --from=build /install /usr/local
COPY --from=build /work/app.py /app.py
COPY --from=build /tmp/build-note.sha256 /evidence/build-note.sha256
COPY --from=build /tmp/secret-status /evidence/secret-status
COPY --from=build /tmp/ssh-status /evidence/ssh-status
USER 65532:65532
ENTRYPOINT ["python", "/app.py"]

Notice what is not copied: the pip cache, bind-mounted file, secret file, and SSH socket. Only explicit non-secret outputs cross into the runtime stage.

5. Start an ephemeral SSH agent for a mount-only proof

eval "$(ssh-agent -s)"
ssh-keygen -q -t ed25519 -N '' -f ./ch11_ephemeral_key
ssh-add ./ch11_ephemeral_key
ssh-add -l | tee evidence/06-ssh-agent.txt

This key never contacts a remote host. BuildKit receives the agent socket only so the Dockerfile can prove an SSH mount exists. If your host lacks OpenSSH client tools, skip this live SSH portion and use the documented expected-state path: preserve docker buildx build --help and the Docker RUN --mount=type=ssh reference, then mark live SSH evidence as not observed rather than fabricating it.

6. Cold build: no external cache imported

START=$(date +%s)
docker buildx build \
  --builder devops-academy-ch11 \
  --progress=plain \
  --secret id=demo,src=fake-secret.txt \
  --ssh default="$SSH_AUTH_SOCK" \
  --metadata-file evidence/07-cold-metadata.json \
  --label devops-academy.lab=ch11 \
  --load \
  -t devops-academy-ch11:app \
  . 2>&1 | tee evidence/07-cold-build.txt
END=$(date +%s)
printf 'cold_seconds=%s\n' "$((END-START))" \
  | tee evidence/08-cold-time.txt

In plain progress, identify which vertex downloads Python packages, which consumes the bind input, and which receives secret/SSH mounts. Build output alone does not prove the image is local; --load is the explicit exporter choice that places a single-platform result in the local image store.

7. Warm build: distinguish instruction hits from cache-mount reuse

START=$(date +%s)
docker buildx build \
  --builder devops-academy-ch11 \
  --progress=plain \
  --secret id=demo,src=fake-secret.txt \
  --ssh default="$SSH_AUTH_SOCK" \
  --metadata-file evidence/09-warm-metadata.json \
  --load \
  -t devops-academy-ch11:app \
  . 2>&1 | tee evidence/09-warm-build.txt
END=$(date +%s)
printf 'warm_seconds=%s\n' "$((END-START))" \
  | tee evidence/10-warm-time.txt

Most unchanged vertices should report CACHED. That is instruction/result-cache reuse. The pip cache mount matters when the pip vertex actually executes again; a fully cached vertex does not need the package-manager process at all.

8. Force one dependency vertex to execute while keeping its package cache

Add a harmless build argument that affects only the dependency step, then rebuild. This separates “the RUN vertex executed” from “the package manager had a warm cache.”

python - <<'PY'
from pathlib import Path
p=Path('Dockerfile')
s=p.read_text()
s=s.replace('COPY requirements.txt .\nRUN --mount=type=cache', 'COPY requirements.txt .\nARG DEP_CACHE_BUST=0\nRUN echo "dependency-cache-bust=$DEP_CACHE_BUST" && \\\n    --mount=type=cache')
# Restore valid RUN syntax by moving the mount before the command.
s=s.replace('RUN echo "dependency-cache-bust=$DEP_CACHE_BUST" && \\\n    --mount=type=cache,id=ch11-pip,target=/root/.cache/pip,sharing=locked \\\n    pip install', 'RUN --mount=type=cache,id=ch11-pip,target=/root/.cache/pip,sharing=locked \\\n    echo "dependency-cache-bust=$DEP_CACHE_BUST" && pip install')
p.write_text(s)
PY

docker buildx build \
  --builder devops-academy-ch11 \
  --progress=plain \
  --build-arg DEP_CACHE_BUST=1 \
  --secret id=demo,src=fake-secret.txt \
  --ssh default="$SSH_AUTH_SOCK" \
  --load -t devops-academy-ch11:app \
  . 2>&1 | tee evidence/11-cache-mount-build.txt

Read the pip output. The RUN vertex should execute because the argument value changed; the package-manager cache can still reduce downloads. If your package manager or network behaves differently, record the observed bytes/timing rather than forcing the expected result.

9. Prove runtime state and mount disappearance

docker image inspect devops-academy-ch11:app \
  --format 'ID={{.Id}} User={{.Config.User}} Env={{json .Config.Env}}' \
  | tee evidence/12-image-inspect.txt

docker image history --no-trunc devops-academy-ch11:app \
  | tee evidence/13-image-history.txt

docker run --rm devops-academy-ch11:app \
  | tee evidence/14-runtime.txt

docker run --rm --entrypoint /bin/sh devops-academy-ch11:app -c \
  'ls -l /evidence; test ! -e /run/secrets/demo; test -z "$SSH_AUTH_SOCK"; cat /evidence/*' \
  | tee evidence/15-mount-boundary.txt

grep -R --fixed-strings 'chapter11-fake-token-v1' evidence Dockerfile app.py requirements.txt || true

The grep deliberately searches only lab files you control; the fake secret file itself is expected to match if included in the search path, so do not claim “no match anywhere” unless you exclude it. The important checks are image config/history/runtime filesystem and logs. The final image should not contain the secret file, SSH agent socket, or pip cache directory.

10. Export a local external cache

rm -rf cache-out
mkdir cache-out

docker buildx build \
  --builder devops-academy-ch11 \
  --progress=plain \
  --build-arg DEP_CACHE_BUST=1 \
  --secret id=demo,src=fake-secret.txt \
  --ssh default="$SSH_AUTH_SOCK" \
  --cache-to type=local,dest=cache-out,mode=max \
  --metadata-file evidence/16-cache-export-metadata.json \
  --output type=oci,dest=ch11-app.oci.tar \
  . 2>&1 | tee evidence/16-cache-export.txt

find cache-out -maxdepth 2 -type f | sort | head -40 \
  | tee evidence/17-local-cache-files.txt

The local backend uses an OCI-layout directory for cache data. The OCI application output and the local cache are two different artifacts even though both are produced by the same build.

11. Import cache into a fresh builder

docker buildx create \
  --name devops-academy-ch11-fresh \
  --driver docker-container

docker buildx inspect devops-academy-ch11-fresh --bootstrap \
  | tee evidence/18-fresh-builder.txt

START=$(date +%s)
docker buildx build \
  --builder devops-academy-ch11-fresh \
  --progress=plain \
  --build-arg DEP_CACHE_BUST=1 \
  --secret id=demo,src=fake-secret.txt \
  --ssh default="$SSH_AUTH_SOCK" \
  --cache-from type=local,src=cache-out \
  --metadata-file evidence/19-import-metadata.json \
  --output type=oci,dest=ch11-app-fresh.oci.tar \
  . 2>&1 | tee evidence/19-cache-import-build.txt
END=$(date +%s)
printf 'fresh_import_seconds=%s\n' "$((END-START))" \
  | tee evidence/20-fresh-import-time.txt

Search the plain progress for imported/cached vertices. A fresh builder reusing external cache is strong proof that reuse was not coming from its internal cache. It still does not prove the result was loaded into Docker’s local image store because this command used an OCI exporter.

12. Challenge: identify three distinct reuse mechanisms

From your evidence packet, identify one vertex that was skipped because of instruction/result cache, one operation that executed while benefiting from a cache mount, and one vertex reused by the fresh builder through external cache import. If your environment does not produce all three naturally, explain why from the log instead of inventing a hit.

13. Exact cleanup

docker image rm devops-academy-ch11:app 2>/dev/null || true

docker buildx rm devops-academy-ch11-fresh
docker buildx rm devops-academy-ch11

ssh-add -D 2>/dev/null || true
ssh-agent -k 2>/dev/null || true
rm -f ch11_ephemeral_key ch11_ephemeral_key.pub

docker buildx ls | tee evidence/21-builders-after.txt

Do not prune all builders, images, or system cache. The lab owns exact builder names and one image tag, so exact cleanup is sufficient. Keep cache-out, OCI files, and evidence/ until you finish the checkpoint comparison.

Next lesson

Next: Configuration, Design Choices, and Tradeoffs

Turn the observed mount and cache behaviors into deliberate policies for local development and CI.

Knowledge check

Why use a fresh builder after exporting cache?

What persists from the build bind mount?

What should be absent from the runtime image after secret and SSH mounts?

Does --output type=oci imply the image is available to docker run by tag?

Why is exact builder cleanup safer than global prune?

Official references and version notes

  • Docker build cache — current cache model, optimization guidance, invalidation, and storage backends.
  • Build cache invalidation — instruction matching, COPY/ADD/bind-input checksums, RUN behavior, and the rule that secret contents do not invalidate cache.
  • Optimize cache usage — instruction ordering, small contexts, build bind mounts, cache mounts, and external cache patterns.
  • Cache storage backends — inline, registry, local, GHA, and availability notes for other backends.
  • Local cache backend — OCI-layout local cache export/import used by the mandatory checkpoint.
  • Registry cache backend — separate cache artifacts, min/max modes, and driver/containerd-image-store requirements.
  • RUN --mount reference — bind, cache, tmpfs, secret, and SSH mount semantics and options.
  • Build secrets — passing and consuming secret/SSH mounts without baking credentials into image layers.
  • docker buildx build — --cache-from, --cache-to, --secret, --ssh, progress, metadata, and output behavior.
  • Build drivers — driver capabilities that affect cache backend availability and builder isolation.
  • Docker Engine 29 release notes — current Engine baseline and bundled component updates.
  • Buildx releases — current Buildx release history.
  • BuildKit releases — current BuildKit and built-in Dockerfile frontend release history.
Version and compatibility note

Version-sensitive statements were rechecked against primary documentation on 2026-09-21. The course baseline is Docker Engine 29.8.1, Buildx 0.37.1, BuildKit 0.33.0, and the built-in Dockerfile frontend 1.27.0, but each executable lab records the versions and builder driver actually present. Cache backend support varies by driver and image-store configuration; the mandatory path uses an isolated docker-container builder plus the local cache backend because it is free, inspectable, and does not require registry credentials.

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
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.