Docker Multi-Stage Builds
debt(d5/e5/b5/t5)
Closest to 'specialist tool catches it' (d5) — hadolint, trivy, and dive (all listed in detection_hints.tools) can identify single-stage Dockerfiles with bloated images, missing .dockerignore, or dev dependencies in production. These are specialist DevOps tools, not default linters.
Closest to 'touches multiple files / significant refactor in one component' (e5) — the quick_fix describes creating a multi-stage Dockerfile, which requires restructuring the Dockerfile, potentially creating/updating .dockerignore, and adjusting CI/CD pipelines. It's not a one-liner but stays within the deployment component.
Closest to 'persistent productivity tax' (b5) — applies to web and cli contexts per applies_to, affecting all containerized deployments. Once established, the multi-stage pattern becomes a template that shapes how all future services are built, but doesn't reach the 'defines system shape' level of b7+.
Closest to 'notable trap' (t5) — the misconception explicitly states developers believe 'Multi-stage builds are only for compiled languages' when PHP benefits equally. This is a documented gotcha that most DevOps-oriented developers eventually learn, but it contradicts intuition from compiled-language contexts.
Also Known As
TL;DR
Explanation
Multi-stage builds eliminate the need for separate Dockerfiles for build and production. The builder stage installs Composer and dev dependencies, runs tests, and compiles assets. The final stage copies only the production files — resulting in an image without Composer, dev packages, Xdebug, or build tools. A typical PHP app goes from a 800MB single-stage image to a 80MB multi-stage image. Smaller images: faster CI pulls, smaller attack surface, and lower registry storage costs.
Diagram
flowchart TD
subgraph Builder Stage
BASE1[php:8.3-fpm-alpine]
COMP[Composer install<br/>dev dependencies]
TEST[Run PHPUnit tests]
BASE1 --> COMP --> TEST
end
subgraph Production Stage
BASE2[php:8.3-fpm-alpine]
COPY[COPY only src + vendor<br/>no Composer no dev deps]
BASE2 --> COPY
end
TEST -->|copy artifacts| COPY
COPY --> IMG[Final image<br/>80MB vs 800MB]
style IMG fill:#238636,color:#fff
Common Misconception
Why It Matters
Common Mistakes
- COPY --from=builder . . — copies everything including dev files; be specific about what to copy.
- Not using .dockerignore — the build context includes node_modules, .git, and test files without it.
- Running tests in the production stage — tests belong in the builder stage.
- Not pinning base image versions — FROM php:latest changes unexpectedly.
Code Examples
# Single stage — dev deps in production:
FROM php:8.3-fpm
RUN apt-get install -y git zip unzip
COPY composer.json composer.lock ./
RUN curl -sS https://getcomposer.org/installer | php
RUN php composer.phar install # Installs dev deps including Xdebug!
COPY . .
# Image: ~800MB with all dev tools
# Multi-stage — minimal production image:
FROM php:8.3-fpm-alpine AS builder
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader
COPY . .
RUN vendor/bin/phpunit # Tests in build stage
# Production — no Composer, no dev deps:
FROM php:8.3-fpm-alpine
WORKDIR /app
COPY --from=builder /app/public ./public
COPY --from=builder /app/src ./src
COPY --from=builder /app/vendor ./vendor
# Image: ~80MB