Every instruction produces a layer, and a layer is reused only if every instruction before it is unchanged. Copying the source before installing dependencies therefore reinstalls everything on each edit.
# slow: any source change re-runs composer install
COPY . /app
RUN composer install
# fast: dependencies rebuild only when the manifests change
COPY composer.json composer.lock /app/
RUN composer install --no-scripts --no-autoloader
COPY . /app
RUN composer dump-autoload --optimize
Order instructions from least to most frequently changed, which for almost any application means manifests, dependencies, then source. Splitting the Composer install in two is what makes it work at all, since the autoloader needs source that has not been copied yet. COPY compares file content and metadata, so a build touching every file — a chmod in CI, for instance — busts the cache without changing anything.