Caching a package manager’s downloads in a layer bloats the image and is invalidated by anything above it. A cache mount lives outside the image entirely.
# syntax=docker/dockerfile:1
FROM php:7.4-fpm-alpine
COPY composer.json composer.lock ./
RUN --mount=type=cache,target=/root/.composer/cache
composer install --no-dev --no-scripts --no-autoloader
The syntax directive selects a frontend that understands the flag, and forgetting it produces a parse error rather than a helpful message. The cache is per machine, so a fresh CI runner still starts cold — combining it with --cache-from against a registry image is what covers both cases. Note that the cache is shared between concurrent builds by default, and sharing=locked is the flag for anything that cannot tolerate that.