CertGrid CertGrid
Best Practices·Docker

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

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.

One Docker host is all this guide needs. Nothing here depends on a second machine, and the hardware above is modest on purpose - a 2 core, 4 GB VM runs everything in this path.
Server NameIP AddressOSRolesCPURAMHDD
DOCKER01192.168.0.21Ubuntu 26.04 LTSDocker Host2 Core4 GB50 GB

Before you start

  1. 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 - ARG is recorded in the image history forever. BuildKit's RUN --mount solves 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.txt

    Expected 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.

  2. A cache mount survives a cache-busting build

    This is the point. --no-cache discards every layer, so the apk add genuinely 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.1s

    Expected 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.

  3. 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 it

    Expected resultThe anti-pattern, not executed.

    Success conditionYou can explain why a later rm does not remove a secret from an image.

  4. A build secret is a file that exists only during one instruction

    --mount=type=secret makes 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-secret

    Expected resultThe build succeeding, having read the secret.

    Success conditionThe id in the Dockerfile and the id on the command line must match; the file the build reads is /run/secrets/<id>.

  5. 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 directory

    Expected 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.

  6. 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, and env= avoids writing it to disk at all. Never commit the file, and add it to .dockerignore as well as .gitignore so 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' >> .dockerignore

    Expected 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

Official sources