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 followingRUNto execute again (apt and Composer work repeat) - good: expect only the final
COPY . .to rebuild; Composer layer should showCACHED
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
- Clone the GitHub example or create the project and both Dockerfiles locally.
- Build
:badand:goodand record wall-clock time for each first build. - Edit
src/index.phponly. - Rebuild both and compare which steps re-execute in the build log.
- Run
docker image history --no-truncon 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