In this lesson: Order a Dockerfile for cache reuse and use a multi-stage build.
Every instruction in a Dockerfile creates a layer — a recorded set of filesystem changes. Layers stack to form the image, and Docker caches them. Understanding the cache is the difference between a build you run happily on every push and one you avoid.
The cache rule
For each instruction Docker asks: have I built this exact step, on this exact parent layer, before? If yes it reuses the result instantly. The moment one step misses, every step after it must be rebuilt — the cache is a chain, not a set.
The mistake everyone makes once
# Slow
COPY . .
RUN composer install
COPY . . includes every source file. Change one line of a template and that layer changes, so composer install reruns — downloading every dependency again because you edited some HTML.
# Fast
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader
COPY . .
Now the dependency layer only rebuilds when the lock file actually changes. Editing source code rebuilds one cheap layer at the end.
Layers are additive — deleting does not shrink
# The build tools are still in the image
RUN apk add --no-cache build-base
RUN make
RUN apk del build-base
Layer three records a deletion, but layers one and two are still in the image and still downloaded by anyone pulling it. To actually keep something out, it must never be committed to a layer:
RUN apk add --no-cache --virtual .build build-base \
&& make \
&& apk del .build
Multi-stage builds
The proper answer. Build in one image, then copy only the finished artefact into a clean one. The compilers, the source, the caches — none of it reaches the final image.
# Stage 1: build the front-end assets
FROM node:20-alpine AS assets
WORKDIR /build
COPY package.json package-lock.json ./
RUN npm ci
COPY resources/ resources/
COPY webpack.mix.js ./
RUN npm run production
# Stage 2: PHP dependencies
FROM composer:2 AS vendor
WORKDIR /build
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --optimize-autoloader
# Stage 3: the image that actually ships
FROM php:8.2-fpm-alpine
WORKDIR /app
COPY --from=vendor /build/vendor ./vendor
COPY --from=assets /build/public/asset ./public/asset
COPY . .
RUN adduser -D -u 1000 app && chown -R app:app /app
USER app
CMD ["php-fpm"]
Node and Composer never appear in the shipped image. It is smaller, it starts faster, and it has far less in it that could be exploited.
Checking your work
docker images # how big is it?
docker history yanjye:latest # what did each layer cost?
docker build --progress=plain -t x . # see which steps hit the cache
docker build --no-cache -t x . # prove a clean build still works
Try it yourself
Take a Dockerfile with COPY . . before the dependency install. Time docker build. Change one character in a source file and time it again. Now reorder so the manifest is copied first, and repeat both timings. Write the four numbers down — that difference runs on every push for the life of the project.