Docker caches per instruction, and the cache is invalidated by the first changed file. Copying the whole source before installing dependencies means every code change reinstalls everything.
# rebuilds vendor on every source change
COPY . /app
RUN composer install --no-dev
# rebuilds vendor only when the dependencies change
COPY composer.json composer.lock /app/
RUN composer install --no-dev --no-scripts --no-autoloader
COPY . /app
RUN composer dump-autoload --optimize
The split into install-then-dump exists because the autoloader needs the source and the packages do not. --no-scripts in the first step is what prevents a post-install hook reaching for a file that has not been copied yet. On a project with sixty packages this is the difference between a four-minute rebuild and a six-second one, which changes whether people build locally at all.