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.
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.
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.
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.
Knowledge check
Why use a fresh builder after exporting cache?
It separates external-cache reuse from the original builder’s internal cache.
What persists from the build bind mount?
Only outputs explicitly written outside the mount and committed by the RUN result. The mounted source itself is temporary.
What should be absent from the runtime image after secret and SSH mounts?
The secret file/value and SSH agent socket/key material; only intentionally created non-secret outputs may remain.
Does --output type=oci imply the image is
available to docker run by tag?
No. Export destination and local image-store state are separate.
Why is exact builder cleanup safer than global prune?
It removes only chapter-owned resources and preserves unrelated caches/evidence.
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 --mountreference — 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-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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.