Layers, Caching, Rebuilds and Registry Strategies
A Docker image is not a black box. It is an ordered sequence of read-only layers that together form the container's filesystem. Anyone who understands how layers are built, why the build cache gets invalidated, and how registries distribute images efficiently writes Dockerfiles that build fast and produce small images.
Table of Contents
- 1. The layer model: how a Docker image is built
- 2. Build cache: rules and invalidation
- 3. Dockerfile order for maximum caching
- 4. Multi-stage builds: reducing image size
- 5. BuildKit: parallel builds and advanced caching features
- 6. Image size: what actually helps
- 7. Registry strategies: local, CI and production
- 8. Registry types compared
- 9. Image security: tags, digests and scanning
- 10. Summary
- 11. FAQ
1. The layer model: how a Docker image is built
A Docker image consists of a sequence of read-only layers that, stacked on top of each other, form a union filesystem. Every Dockerfile instruction that changes the filesystem (RUN, COPY, ADD) creates a new layer. FROM, ENV, ARG and LABEL either create no layer of their own or a very compact metadata layer. When a container starts, Docker adds a writable container layer on top of the read-only image layers, and that layer is removed once the container is deleted.
Layer sharing is the model's decisive efficiency advantage. If ten Docker images build on the same base image, that base image sits on disk only once. When pulling from a registry, only the layers not already present locally are transferred. That makes layer sharing the central performance mechanism, both for local disk usage and for registry transfer. Choosing the right base image is therefore not a cosmetic decision, it has a direct impact on build time, image size and transfer costs.
2. Build cache: rules and invalidation
The build cache is the most powerful performance feature when building Docker images. Before every build instruction, Docker checks whether a cached layer already exists for the same instruction and the same input state. If it does, the cached layer is reused and the instruction does not run again. That saves many minutes of build time for instructions such as RUN apt-get install or RUN composer install.
The build cache is invalidated as soon as an instruction changes or its inputs change. For COPY instructions, Docker checks the content of the copied files (checksums), so a change in any copied file invalidates the layer. Every subsequent instruction is then re-executed as well. This is the most important rule for ordering Dockerfile instructions well: rarely changing instructions come first, frequently changing instructions come last. Ignore this order and Docker images in CI end up building from scratch on every commit.
# Dockerfile: optimized layer order for maximum cache reuse
# Rarely-changing instructions first, frequently-changing last
FROM php:8.4-fpm-alpine AS base
# System dependencies: change rarely, cached for weeks
RUN apk add --no-cache \
libpng-dev libjpeg-dev libwebp-dev libzip-dev icu-dev \
&& docker-php-ext-install gd intl pdo_mysql zip bcmath opcache
# Composer installation: changes only on version upgrades
COPY --from=composer:2.8 /usr/bin/composer /usr/bin/composer
# composer.json and composer.lock: change on dependency updates
# Copy only these files first, not the whole source tree
COPY composer.json composer.lock ./
# Install PHP dependencies: cached until lock file changes
RUN composer install --no-dev --no-scripts --prefer-dist --optimize-autoloader
# Application source: changes on every commit (put last)
COPY . .
# Post-install scripts: run after source is in place
RUN composer run-script post-install-cmd --no-interaction
# Set file permissions
RUN chown -R www-data:www-data /var/www/html
CMD ["php-fpm"]
A common mistake is placing COPY . . at the top of the Dockerfile. That copies all source files in a single step and invalidates the cache on every file change, whether a PHP file or composer.lock changed. The fix is to separate COPY composer.json composer.lock ./ before RUN composer install and to move the broader COPY . . to after the dependency installation. That way composer install only reruns when the lock file actually changes.
3. Dockerfile order for maximum caching
The optimal order of instructions in a Dockerfile for Docker images follows a clear hierarchy: base image and system dependencies first, then dependency management files, then dependency installation, then application code. This order ensures that the most expensive step, system package installation and dependency installation, stays cached until a dependency actually changes. A commit that only changes PHP code reuses every previous layer from cache and only rebuilds the final COPY layer.
Another important optimization is combining RUN commands with &&. Every separate RUN instruction creates its own layer that persists its intermediate state. If apt-get install and apt-get clean sit in two separate RUN instructions, the first layer still contains the full package cache files, even though apt-get clean removes them in the second layer. Deleted files do not disappear from earlier layers. Any commands that together form one logical step and produce temporary files need to be combined into a single RUN instruction.
4. Multi-stage builds: reducing image size
Multi-stage builds are the most important feature for keeping Docker images small in production environments. The idea: multiple FROM instructions in the same Dockerfile define separate build phases. Build tools, compilers, test dependencies and cache data from earlier phases never end up in the final image. The final image only contains what is actually needed at runtime. The difference between a naive PHP image with Composer, all dev dependencies and build tools, and an optimized multi-stage image can be 500 to 800 MB.
For Docker images combining PHP and Node (for example Hyva with Tailwind CSS), a three-stage setup makes sense: a PHP builder stage that installs Composer dependencies, a Node builder stage that runs the Tailwind CSS build, and a final production stage that only contains runtime dependencies. The vendor/ directory is copied from the PHP builder stage, and the compiled CSS output in pub/static is copied from the Node builder stage. The production image contains neither Composer nor Node nor npm packages.
# Multi-stage Dockerfile for a PHP + Node application
# Three stages: PHP builder, Node builder, production image
# Stage 1: PHP dependency installation
FROM php:8.4-fpm-alpine AS php-builder
COPY --from=composer:2.8 /usr/bin/composer /usr/bin/composer
WORKDIR /app
COPY composer.json composer.lock ./
# Install production dependencies only; adjust later if needed
RUN composer install --no-dev --no-scripts --prefer-dist \
&& composer dump-autoload --optimize --no-dev
# Stage 2: Node/Tailwind CSS build
FROM node:22-alpine AS node-builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production=false # install all including devDependencies for build
COPY . .
RUN npm run build # Tailwind CSS production build
# Stage 3: Production image, minimal, no build tools
FROM php:8.4-fpm-alpine AS production
RUN apk add --no-cache libpng libjpeg libwebp libzip icu-libs \
&& docker-php-ext-install gd intl pdo_mysql zip bcmath opcache
WORKDIR /var/www/html
# Copy only what is needed at runtime from previous stages
COPY --from=php-builder /app/vendor ./vendor
COPY --from=node-builder /app/pub/static ./pub/static
COPY . .
# Remove development files from the final image
RUN rm -rf tests/ .git/ node_modules/ package*.json \
&& chown -R www-data:www-data /var/www/html
USER www-data
CMD ["php-fpm"]
5. BuildKit: parallel builds and advanced caching features
BuildKit has been the default build backend since Docker Engine 23 and brings substantial improvements over the classic builder. The most important features for Docker images: parallel execution of independent build stages, external cache sources (--cache-from), mount types for secrets and build caches, and the newer --cache-to flag for exporting the build cache to a registry. Parallel stages significantly cut the build time of multi-stage Dockerfiles when the stages are independent of each other, for instance the PHP builder stage and the Node builder stage from the earlier example can run at the same time.
The BuildKit cache mount feature (RUN --mount=type=cache) lets package manager caches persist between builds without baking them into an image layer. A pip install or apt-get cache survives between builds but never lands in the final Docker image. That speeds up incremental builds significantly, since the package manager only has to download what actually changed. For registry-based caching in CI pipelines, --cache-to type=registry,ref=registry.example.com/cache/myapp persists the build cache between pipeline runs without needing a local Docker daemon.
6. Image size: what actually helps
When optimizing Docker image size, some measures have a large effect and others barely move the needle. The biggest lever is the base image: switching from ubuntu:24.04 (roughly 80 MB compressed) to debian:bookworm-slim (roughly 30 MB) or alpine:3.19 (roughly 7 MB). Alpine-based images are small, but PHP extensions have to be compiled from source, which increases build time. The official PHP Alpine image is a solid compromise. Google's distroless images are even smaller but harder to debug.
The second-biggest lever is multi-stage builds, which remove build tools from the final image. Smaller effects come from aggressive RUN apt-get clean (useful, but base image choice matters more) and manual layer reduction through && chaining (important for correctness, but rarely the main factor for size). Inspecting Docker images with docker image history imagename shows exactly how much space each layer takes up, which is the first step toward targeted optimization.
7. Registry strategies: local, CI and production
A Docker image registry is the central distribution system for container images in a team. Docker Hub is free for public images but limited for private images and pulls. Professional teams are better served by self-hosted registries (Harbor, Gitea Container Registry) or cloud provider registries (GitHub Container Registry, GitLab Registry, AWS ECR). The choice depends on access control, scanning requirements, transfer costs and whichever CI/CD system is already in use.
An effective registry strategy for teams with CI/CD uses several namespaces or repositories: one namespace for cached base images (mirrored locally to work around Docker Hub rate limits), one namespace for CI build artifacts (tagged with the commit SHA, short lived), and one namespace for release images (tagged with a semantic version, long lived). Docker images in CI get tagged with the commit SHA, release images get a semantic version tag. That makes it possible to go back to the exact state of any commit at any time while still having human-readable release tags.
8. Registry types compared
Choosing the right registry for Docker images has a direct impact on build times, cost and security.
| Registry | Cost | Scanning | CI integration |
|---|---|---|---|
| Docker Hub | Free (rate limited) | Basic (Pro) | Universal |
| GitHub GHCR | Free (GitHub Actions) | Dependabot alerts | Seamless with GitHub Actions |
| GitLab Registry | Included in GitLab plan | Trivy integration | Seamless with GitLab CI |
| AWS ECR | Pay per use | ECR Enhanced Scanning | AWS ecosystem |
| Harbor (self-hosted) | Infrastructure cost only | Trivy, Clair built in | Any CI platform |
For teams using GitLab CI, the GitLab Container Registry is the natural choice: no separate login needed, automatic credentials through $CI_REGISTRY_* variables, built-in scanning, and easy cleanup of old Docker images through GitLab policies. The CI job can push straight to the registry without configuring separate secrets, which significantly cuts the setup effort.
9. Image security: tags, digests and scanning
Tags are mutable in Docker registries: a latest tag can point to a different Docker image today than it did yesterday. That makes tags unsuitable for reproducible deployments. Image digests (sha256:abc123...) are immutable references to exactly one image content. Production deployments should always use the digest, not a tag: image: nginx@sha256:abc123 instead of image: nginx:1.26. That guarantees a deployment always starts the same image.
Image scanning with tools like Trivy or Grype checks Docker images for known CVEs in installed packages and libraries. Scanning should run in the CI pipeline after the build and block the build on critical CVEs. Trivy can run as a container in GitLab CI and produces structured output for GitLab's Security Dashboard integration. Regularly rebuilding base images, even without code changes, ensures that security patches from the base image reach your own images promptly.
10. Summary
Building Docker images efficiently requires understanding the layer model and the build cache logic. The most important optimization is the order of Dockerfile instructions: rarely changing steps first, frequently changing steps last. Multi-stage builds reduce the final image size by keeping build tools and temporary files out of the production image. BuildKit parallelizes independent build stages and enables registry-based caching for CI pipelines.
Registry strategies with several namespaces, base image mirrors, CI artifacts and release images, enable reproducible deployments and easy rollback. Image digests instead of tags in production environments guarantee immutability. Regular scanning with Trivy and automatic rebuilds of base images keep Docker images secure. Anyone who understands these principles and applies them consistently builds images that build fast, stay small, and can be deployed safely.
Mironsoft
Dockerfile optimization, CI/CD pipelines and container infrastructure
Slow Docker builds in your CI?
We optimize Dockerfiles, set up registry-based caching with BuildKit, and configure image scanning, so your CI builds run fast and only secure images get deployed.
Dockerfile audit
Analysis of layer order, cache invalidations and unused build steps
Multi-stage refactoring
Remove build tools from production images, cut image size in half
Registry setup
Registry strategy, BuildKit caching in CI and automatic image scanning
Docker Images: the essentials at a glance
Layer order
Rarely changing steps first. COPY composer.lock plus RUN composer install before COPY . .. Avoid cache invalidation on every source file change.
Multi-stage builds
Keep build tools and dev dependencies in earlier stages. Copy only runtime artifacts into the final image. Size reductions of 500 to 800 MB are realistic.
Registry strategy
Tag CI artifacts with the commit SHA. Tag release images with a semantic version. Use digests instead of tags for reproducible production deployments.
Security
Trivy scanning in CI after every build. Rebuild base images regularly for security patches. Use image digests in production deployments.
11. FAQ: Docker images, layers and registry strategies
1Why does Dockerfile order matter?
2What is a multi-stage Dockerfile?
3Use digests instead of tags?
4What is BuildKit?
5Use the build cache in CI?
--cache-from type=registry plus --cache-to type=registry. Cache is persisted in the registry and loaded on the next run.6apt-get clean in the same RUN?
7Registry for GitLab CI?
8Analyze layer sizes?
docker image history imagename shows every layer with its size and the instruction that created it.