A layer cache is invalidated by a lock file change; a cache mount survives it, because the downloaded packages are not in the layer at all.
# syntax=docker/dockerfile:1.3
RUN --mount=type=cache,target=/root/.composer/cache
composer install --no-dev --prefer-dist --no-progress
RUN --mount=type=cache,target=/root/.npm
npm ci --no-audit --no-fund
# adding one package now re-downloads one package,
# rather than all 340.
The syntax directive at the top is required and is the thing people forget, at which point the --mount flag is a parse error rather than a silent no-op — a good failure. The cache is per builder rather than in the image, so it does nothing for a pipeline that starts on a fresh runner unless the builder cache is itself exported; the GitHub Actions cache backend covers that. It also means the build is no longer reproducible from the Dockerfile alone, which is a real trade for a real speedup.