Originally published on docker.learnio.dev — part of the Docker fundamentals course. View this lesson on the original site.

Measure layers instead of guessing

Image size and rebuild time come from layer history, not from how tidy the Dockerfile looks. Build two near-identical images, change one source file, and let docker history show which Dockerfile wasted work.

The builder walks instructions in order. When one step's cache invalidates, every later step rebuilds. A COPY . . placed too early is a common way to turn a quick edit into a full dependency reinstall.

GitHub example (clone and run)

Use the ready-made project with Dockerfile.bad, Dockerfile.good, and a tiny PHP app:

Academy repo: 2-images-layers/history-size-layer-dedup

git clone https://github.com/stuarttodd-dev/halfshellstudios-academy.git
cd halfshellstudios-academy/docker-fundamentals/2-images-layers/history-size-layer-dedup
docker build -f Dockerfile.bad -t php-layers:bad .
docker build -f Dockerfile.good -t php-layers:good .
echo "<?php echo 'hi v2';" > src/index.php
docker build -f Dockerfile.bad -t php-layers:bad .
docker build -f Dockerfile.good -t php-layers:good .
docker image history --no-trunc php-layers:good

The folder should include both Dockerfiles, composer.json, src/index.php, and a README.md with the full step list.

Set up a small PHP project (if you are not using the repo)

mkdir -p /tmp/php-layers && cd /tmp/php-layers
cat > composer.json <<'EOF'
{
  "name": "demo/app",
  "require": { "monolog/monolog": "^3.0" }
}
EOF
mkdir -p src
echo "<?php echo 'hi';" > src/index.php

Dockerfile that busts cache early

Save as Dockerfile.bad:

FROM php:8.3-cli-bookworm
WORKDIR /app
COPY . .
RUN apt-get update \
 && apt-get install -y --no-install-recommends git unzip \
 && curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \
 && composer install --no-dev --no-interaction --no-ansi

Any change under the project directory invalidates COPY . . and forces the expensive RUN again.

Dockerfile that protects dependency layers

Save as Dockerfile.good:

FROM php:8.3-cli-bookworm
RUN apt-get update \
 && apt-get install -y --no-install-recommends git unzip \
 && rm -rf /var/lib/apt/lists/*
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /app
COPY composer.json composer.lock* ./
RUN composer install --no-dev --no-interaction --no-ansi --no-scripts
COPY . .

Dependency install sits behind composer.json only. Application source copies last.

First build

docker build -f Dockerfile.bad -t php-layers:bad .
docker build -f Dockerfile.good -t php-layers:good .
docker image ls php-layers

Note image sizes. The bad image is not always larger on disk, but rebuild behaviour will diverge on the next edit.

Change one file and rebuild

echo "<?php echo 'hi v2';" > src/index.php

time docker build -f Dockerfile.bad -t php-layers:bad .
time docker build -f Dockerfile.good -t php-layers:good .

Watch build output:

  • bad: expect COPY . . and the following RUN to execute again (apt and Composer work repeat)
  • good: expect only the final COPY . . to rebuild; Composer layer should show CACHED

Read the history

docker image history --no-trunc php-layers:good
docker image history --no-trunc php-layers:bad

Each row is a layer. Large SIZE on a RUN composer install line is a signal that instruction order or cache misses are costing you.

Layer deduplication across images

Pull two tags that share a base:

docker pull php:8.3-cli-bookworm
docker image ls php

Build both variants above. They share the FROM php:8.3-cli-bookworm layers on disk when the base digest matches. Registries apply the same idea: identical layer digests are stored once and referenced from many manifests.

Try it yourself

  1. Clone the GitHub example or create the project and both Dockerfiles locally.
  2. Build :bad and :good and record wall-clock time for each first build.
  3. Edit src/index.php only.
  4. Rebuild both and compare which steps re-execute in the build log.
  5. Run docker image history --no-trunc on both tags and find the largest layer on the bad image after the source edit.

You have understood the lesson when a one-line PHP change rebuilds Composer on :bad but stays cached on :good, and you can point at the history line that proves it.

From Docker fundamentals — Images, layers, tags, and everyday commands