Multi-stage builds: the stage that only exists for tests

A named stage that installs dev dependencies and runs the suite gives the pipeline a single command and the runtime image none of it.

FROM php:8.0-fpm-alpine AS base
# ... extensions, config

FROM base AS vendor-dev
RUN composer install --no-progress

FROM vendor-dev AS test
COPY . .
RUN vendor/bin/phpunit && vendor/bin/phpstan analyse

FROM base AS runtime
COPY --from=vendor-prod /app/vendor /app/vendor

# docker build --target test .
# docker build --target runtime -t app:latest .

The value is that the test environment is the runtime environment plus dev dependencies, rather than a separate description that drifts. The cost is that a failing test is a failing build with the output buried in build logs, which is worse for readability than a test step — so the pipeline usually still runs the suite separately and this stage exists to guarantee they cannot diverge. Stages not reached by the target are not built at all, which is what keeps it affordable.