Problem summary
When a small source change makes a Docker build re-run dependency installs, many developers say “the cache didn’t work.” The clearer diagnosis is that Docker has at least two different cache mechanisms: instruction/layer cache (which can skip an instruction entirely) and cache mounts (which provide a persistent directory to a step when that step runs). They solve different problems and have different guarantees.
Implementation approach
This article keeps the implementation narrow and verifiable: create a helper that validates the fields Docker documents for RUN –mount=type=cache, emits the mount declaration exactly, and uses a sensible default identity. Concretely, the helper verifies that the mount target is a non-empty absolute path composed of safe characters, that an explicit id (if provided) is a non-empty safe string, and that sharing is one of the documented values (shared, private, locked). The helper then returns the canonical shape for the mount options and formats the –mount fragment you will paste into a Dockerfile instruction.
Expected behavior
- Default id equals the mount target path. That mirrors Docker’s documented default and gives a stable, readable identity in multi-stage builds.
- When the install step runs, the mount provides the package manager’s actual cache directory so downloads can be reused if the builder still holds the storage.
- Choose sharing=locked when a tool expects exclusive access to the cache files (helps with file-locking semantics on CI with concurrent builds).
Limitations and practical notes
- A correct mount declaration does not force persistence across fresh builder instances. Docker stores cache mounts in the builder’s local storage; if your CI creates a fresh builder each job or the builder garbage-collects storage, the mount will start empty.
- Cache mounts make reruns cheaper by preserving downloaded artifacts, but they do not change Docker’s invalidation rules. If a dependency manifest changes, the install step should run again; the mount only reduces the download cost during that rerun.
- The helper intentionally restricts allowable characters in target and id to avoid injection into the flag syntax. Projects that need broader naming schemes should provide a separate, reviewed configuration path.
Why this framing helps
By separating what the Dockerfile can guarantee (a valid, stable mount declaration) from what the environment must provide (builder-local persistence), you get a predictable place to reason about performance: fix the declaration first, then address your CI/builder topology if persistence is still missing.
(The server will insert the verified implementation example as a fenced code block below this explanation.)
Working example
const SHARING_VALUES = new Set(['shared', 'private', 'locked']);
const SAFE_COMPONENT = /^[A-Za-z0-9_./-]+$/;
export function validateCacheMountInput({ target, id, sharing = 'shared' } = {}) {
if (typeof target !== 'string' || target.length === 0) {
throw new TypeError('target must be a non-empty string');
}
if (!target.startsWith('/')) {
throw new RangeError('target must be an absolute path');
}
if (!SAFE_COMPONENT.test(target)) {
throw new RangeError('target must contain only letters, digits, slash, dot, underscore or hyphen');
}
if (id !== undefined) {
if (typeof id !== 'string' || id.length === 0) {
throw new TypeError('id must be a non-empty string when provided');
}
if (!SAFE_COMPONENT.test(id)) {
throw new RangeError('id must contain only letters, digits, slash, dot, underscore or hyphen');
}
}
if (!SHARING_VALUES.has(sharing)) {
throw new RangeError('sharing must be one of shared, private, locked');
}
}
export function buildCacheMountOptions({ target, id, sharing = 'shared' } = {}) {
validateCacheMountInput({ target, id, sharing });
return { type: 'cache', target, id: id ?? target, sharing };
}
export function formatRunMount(options) {
const { type, target, id, sharing } = buildCacheMountOptions(options);
return `--mount=type=${type},target=${target},id=${id},sharing=${sharing}`;
}
Read the full article: https://chriseugenerodriguez.com/blog/reuse-cache-mounts-multi-stage-dockerfiles
