COPY the lock file before the source, always

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.