0Pricing
PHP Academy · Lesson

Multi-Stage Builds and Optimization

Shrink images and separate build from runtime.

Multi-Stage Builds and Optimization is a free PHP Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the PHP Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Why Multi-Stage

A single-stage image carries Composer, build deps, dev headers, and your tests/ folder into production. Multi-stage builds let you compile/install in a fat builder stage and copy only the finished artifacts into a slim runtime stage.

Result: smaller images, smaller attack surface, faster pulls, and no compilers shipped to prod.

Named Stages

Each FROM ... AS name starts a new stage. Later stages can COPY --from=name files out of earlier ones. Only the final stage becomes your image; intermediate stages are discarded (but cached).

# Stage 1: dependencies
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --prefer-dist --ignore-platform-reqs

# Stage 2: runtime
FROM php:8.3-fpm-alpine AS runtime
WORKDIR /app
COPY --from=vendor /app/vendor ./vendor
COPY . .

Separating Build Deps

Compiling extensions needs autoconf, gcc, dev headers — none of which belong in runtime. Use the installer in a builder stage, then copy the compiled .so files and the matching conf.d ini into a clean runtime stage.

FROM php:8.3-fpm-alpine AS ext-builder
ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
RUN chmod +x /usr/local/bin/install-php-extensions && \
    install-php-extensions redis igbinary opcache intl

FROM php:8.3-fpm-alpine AS runtime
# Copy compiled extensions + their enable configs
COPY --from=ext-builder /usr/local/lib/php/extensions/ /usr/local/lib/php/extensions/
COPY --from=ext-builder /usr/local/etc/php/conf.d/ /usr/local/etc/php/conf.d/

Layer Caching Order

Docker caches layers top-to-bottom and invalidates everything below a changed layer. Order from least to most frequently changing:

  • Base + extensions (rare)
  • composer.lock + install (occasional)
  • Application source (every commit)
  • Autoload dump (every commit)

This means a code-only change reuses the cached vendor layer entirely.

# BAD: copying all source before composer install
# busts the vendor layer on every code change
COPY . .
RUN composer install

# GOOD: lock first, then source
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-autoloader
COPY . .
RUN composer dump-autoload --optimize

BuildKit Cache Mounts

With BuildKit (DOCKER_BUILDKIT=1) you can mount a persistent cache that survives across builds without ending up in the image. Perfect for Composer's global cache so repeat builds skip re-downloading packages.

# syntax=docker/dockerfile:1
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN --mount=type=cache,target=/tmp/composer-cache \
    COMPOSER_CACHE_DIR=/tmp/composer-cache \
    composer install --no-dev --prefer-dist

Measuring Image Size

Inspect the layer breakdown to find bloat. docker history shows the size each instruction added; tools like dive show wasted space. The goal is a runtime stage with no compilers, no Composer, no dev deps.

# Compare sizes
docker images myapp

# Per-layer contribution
docker history --no-trunc --format '{{.Size}}\t{{.CreatedBy}}' myapp:latest

# Deep inspection of wasted bytes
dive myapp:latest

Stripping the Final Stage

The runtime stage should NOT contain Composer, the extension-installer script, or your test suite. Copy vendor and source from builders; never run composer in the final stage. Remove the installer after use if you must run it there.

FROM php:8.3-fpm-alpine AS runtime
WORKDIR /app

# bring extensions + vendor in from builders — no Composer here
COPY --from=ext-builder /usr/local/lib/php/extensions/ /usr/local/lib/php/extensions/
COPY --from=ext-builder /usr/local/etc/php/conf.d/ /usr/local/etc/php/conf.d/
COPY --from=vendor /app/vendor ./vendor
COPY . .

USER www-data
CMD ["php-fpm"]

Distroless / Scratch Limits

PHP can't run on truly empty scratch — it needs libc and shared libs. The practical floor is alpine (musl) or distroless-style minimal Debian. Alpine is smallest, but watch for native libs expecting glibc; if you hit segfaults with NSS/ICU, fall back to php:8.3-fpm-bookworm.

# Smallest practical PHP runtime
FROM php:8.3-fpm-alpine

# If musl causes native-lib issues (e.g., some ICU edge cases),
# the glibc Debian slim variant is the safe fallback:
# FROM php:8.3-fpm-bookworm

Targeting Stages

One Dockerfile can serve dev and prod via --target. Add a dev stage on top of runtime that re-adds Composer dev deps and Xdebug; build --target=runtime for prod and --target=dev locally.

FROM runtime AS dev
ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
RUN chmod +x /usr/local/bin/install-php-extensions && \
    install-php-extensions xdebug @composer
USER root
RUN composer install   # includes dev deps

# Build prod:  docker build --target runtime -t app:prod .
# Build dev:   docker build --target dev     -t app:dev  .

Reasoning About Layer Sizes

A quick mental model helps you predict cache behavior. This CLI snippet simulates the classic mistake of unbounded layer growth versus a capped one, illustrating why combining cleanup into the same RUN matters.

<?php
// Simulate layer sizes (MB) for two strategies
$installSteps = [120, 8, 8, 8];

// Separate RUN per step keeps temp files in layers
$separate = array_sum($installSteps);

// Single RUN with cleanup removes temp files before commit
$combined = max($installSteps); // peak, then cleaned

echo "Separate layers total: {$separate} MB\n";
echo "Combined+cleanup:       {$combined} MB\n";
echo 'Saved: ' . ($separate - $combined) . " MB\n";
?>

Combine and Clean in One RUN

Each RUN is a layer; deleting files in a later layer doesn't shrink the image because earlier layers still hold the bytes. Install, use, and clean within a single RUN so the temp files never get committed.

RUN apk add --no-cache --virtual .build-deps $PHPIZE_DEPS && \
    pecl install redis && \
    docker-php-ext-enable redis && \
    apk del .build-deps && \
    rm -rf /tmp/pear /var/cache/apk/*

Quick Check

Why must build deps be removed in the same RUN that installed them?

Recap

Multi-stage builds keep compilers and dev deps out of production. You learned to: name stages and COPY --from artifacts, order layers least-to-most volatile for caching, use BuildKit cache mounts for Composer, measure size with docker history/dive, pick alpine vs glibc deliberately, target dev/prod stages, and combine install+cleanup in one RUN.

Frequently asked questions

Is the “Multi-Stage Builds and Optimization” lesson free?

Yes — the full text of “Multi-Stage Builds and Optimization” is free to read here on the web, and the PHP Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the PHP Academy course, upgrade to CoddyKit PRO.

What will I learn in “Multi-Stage Builds and Optimization”?

Shrink images and separate build from runtime. You practise PHP Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start PHP Academy?

No prior experience is required. PHP Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Multi-Stage Builds and Optimization” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this PHP Academy lesson?

Yes. Every PHP Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Containerizing a PHP Application
  2. Multi-Stage Builds and Optimization
  3. Docker Compose for Local Stacks
  4. CI/CD with GitHub Actions
← Back to PHP Academy