Docker Build Cache Mounts and Secrets
Two BuildKit features that solve problems the layer cache cannot: a package cache that survives --no-cache, and a way to use a private token during a build without it ending up in the image.
Dockerfiles and Builds Guide 18 of 46 Advanced
- OSUbuntu 26.04 LTS (resolute)
- Docker Engine29.7.2
- Shellbash
- Architectureamd64
- TimeAbout 13 min
- Reviewed21 August 2026
Tested on the versions above. Build timings vary with network and disk. The behaviour - what persists and what is absent from the image - is what to match.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| DOCKER01 | 192.168.0.21 | Ubuntu 26.04 LTS | Docker Host | 2 Core | 4 GB | 50 GB |
Before you start
- How the layer cache works - guide 17 in this path.
- ARG versus ENV, and why ARG is unsafe for secrets - guide 15 in this path.
-
Two problems the layer cache cannot solve
The layer cache is all-or-nothing: change a dependency manifest and the whole install layer rebuilds, re-downloading every package even the ones that did not change. And there is no safe way to hand a build a private token -
ARGis recorded in the image history forever. BuildKit'sRUN --mountsolves both, and needs no flags to enable because BuildKit is the default builder.bash cat Dockerfile.cacheFROM python:3.13-alpineWORKDIR /appCOPY requirements.txt .RUN --mount=type=cache,target=/root/.cache/pip \ pip install -r requirements.txtExpected resultA RUN instruction with a mount attached.
Success conditionYou can read the syntax: the mount exists only while that instruction runs, and never becomes part of the image.
-
A cache mount survives a cache-busting build
This is the point.
--no-cachediscards every layer, so theapk addgenuinely re-runs - but the package cache directory persists across builds independently of layers, so the downloads are reused and the step completes in about a second.bash Example session docker build --no-cache -f Dockerfile.cache -t cg-bm:cache1 .#6 DONE 0.4s# second build, also --no-cache, so no layer is reused:docker build --no-cache -f Dockerfile.cache -t cg-bm:cache2 .#5 [stage-0 2/2] RUN --mount=type=cache,target=/var/cache/apk apk add curl jq#5 DONE 1.1sExpected resultThe install step re-running, but quickly.
Success conditionThe step executed rather than reporting CACHED - and still finished fast, because the downloads came from the cache mount. Point it at your package manager's cache:
/root/.npm,/root/.cache/pip,/go/pkg/mod,~/.m2. -
The secret problem, stated precisely
A private dependency needs a token at build time. Passed as
ARG, that token is written into the image metadata and stays readable by anyone who can pull the image - deleting the file in a later layer does not help, because the layer that contained it still ships.bash # DO NOT do this - the value is permanently recordedARG NPM_TOKENRUN npm installdocker history --no-trunc IMAGE | grep -i token# on an image built that way, this finds itExpected resultThe anti-pattern, not executed.
Success conditionYou can explain why a later
rmdoes not remove a secret from an image. -
A build secret is a file that exists only during one instruction
--mount=type=secretmakes the value available at a path while that RUN executes. It is not a layer, not an environment variable and not build metadata - when the instruction finishes it is gone.bash Example session cat Dockerfile.secretFROM alpine:3.22RUN --mount=type=secret,id=tok \ test -s /run/secrets/tok && echo "token was readable during build"docker build --secret id=tok,src=token.txt -f Dockerfile.secret -t cg-bm:secret .#5 [stage-0 2/3] RUN --mount=type=secret,id=tok cat /run/secrets/tok > /dev/null && echo used-the-secretExpected resultThe build succeeding, having read the secret.
Success conditionThe
idin the Dockerfile and theidon the command line must match; the file the build reads is/run/secrets/<id>. -
Prove it did not ship
Two independent checks. The image history does not contain the value, and the secret path does not exist in the resulting image at all. That is the difference between this and every ARG-based approach.
bash docker history --no-trunc cg-bm:secret | grep -c "super-secret-token"0 - the secret is NOT in the image historydocker run --rm cg-bm:secret ls /run/secrets/ls: /run/secrets/: No such file or directoryExpected resultZero occurrences, and no secrets directory in the image.
Success conditionBoth checks are negative. Run these against any image you have built with credentials in the past - the ARG mistake is common and silent.
-
Where the secret comes from in CI
src=reads a file, which is fine locally. In CI the value is usually already an environment variable, andenv=avoids writing it to disk at all. Never commit the file, and add it to.dockerignoreas well as.gitignoreso it cannot reach the build context - see guide 16.bash docker build --secret id=tok,env=NPM_TOKEN -t app .# reads the variable directly, nothing touches the filesystem# and keep it out of the build context entirely:echo 'token.txt' >> .dockerignoreExpected resultA form that needs no file on disk.
Success conditionYour pipeline can pass a credential without writing it anywhere. In GitHub Actions,
secrets:on build-push-action does this for you.
Troubleshooting
failed to compute cache key, or the mount syntax is rejected
Why: The build is not using BuildKit, or the Dockerfile syntax is older than the feature.
Fix:BuildKit is the default with current Docker. Confirm the builder, and add the syntax directive if you target older engines.
bash # syntax=docker/dockerfile:1docker buildx lsThe secret is empty inside the build
Why: The
idon the command line does not match theidin the Dockerfile, or the file path insrc=is wrong.Fix:The two ids must be identical, and the build reads
/run/secrets/<id>, not the source filename.bash docker build --secret id=tok,src=./token.txt --progress=plain -t app . 2>&1 | tail -20The cache mount does not seem to help
Why: It is pointed at the wrong directory, or the tool was told not to cache -
apk --no-cacheandpip --no-cache-dirdisable exactly what you are trying to keep.Fix:Target the tool's real cache directory and stop disabling it in that RUN. The
--no-cachehabit exists to keep caches OUT of layers; a cache mount is never in a layer, so it is unnecessary there.bash docker build --progress=plain . 2>&1 | grep -A2 "mount=type=cache"Concurrent builds interfere over one cache mount
Why: Cache mounts are shared by default, and two builds writing the same package cache can conflict.
Fix:Give the mount an explicit
id=per project, or setsharing=lockedso builds take turns.bash RUN --mount=type=cache,id=npm-myapp,sharing=locked,target=/root/.npm npm ci