Prerequisites
- Basic Dockerfile multi-stage knowledge (build stages, COPY, RUN)
- Familiarity with your package manager’s cache path (npm, Yarn, pip, etc.)
Implementation decision
Use a small configuration helper that enforces Docker’s documented options for RUN --mount=type=cache and keeps a predictable default: the mount id defaults to the target path. The helper validates that target is an absolute path composed of safe characters, that an explicit id is a non-empty safe string, and that sharing is one of Docker's documented modes (shared, private, locked). Format the mount fragment exactly so you paste a single, well-formed --mount flag into the dependency-install step.
Expected result
- The dependency-install step receives a mount attached to the package manager’s real cache directory, which reduces repeated download time when the builder preserves local storage between runs.
- The default id equal to the target path keeps cache identity readable and stable across stages. If you need separate namespaces, supply an explicit id to avoid collisions.
- Use sharing=locked when your package manager requires exclusive access to cache files; it prevents concurrent writers from corrupting shared caches on shared CI runners.
One limitation
Cache mounts persist in the builder's local storage only. If your CI spins up a fresh builder for every job or the builder evicts local cache storage, the mount will be empty at each run. In that case, address persistence at the CI/builder layer (external caches, preserved builder instances, or cache export/import) rather than by changing the Dockerfile mount syntax.
(The server will insert the verified implementation example as a fenced code block in place of the demonstration.)
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