Build Cache Invalidation: Why Dockerfile Order Determines Your Build Times
AI generated
FROM
RUN
Docker · Build Performance · CI/CD
Build Cache Invalidation in the Dockerfile
Why instruction order decides between minutes and seconds

A poorly ordered Dockerfile can rebuild every single layer on every build, even when only one line of application code changed. Understanding how Docker invalidates its layer cache based on Dockerfile order lets teams cut CI build times dramatically, without spending a cent more on hardware.

16 min read Layer caching COPY order Build time comparison

1. How Docker Builds Images From Layers

Every instruction in a Dockerfile that changes the filesystem state, such as RUN, COPY, or ADD, produces its own immutable layer at build time. These layers stack on top of each other to form the final image. The key advantage of this model is that Docker can cache each layer individually, so a rebuild does not necessarily have to start from scratch.

For this cache to be usable, Docker checks before each layer whether an identical instruction with an identical context has already been executed once before. If so, the cached layer is reused without actually re-running the instruction. That saves valuable time for unchanged sections, but the wrong order can produce exactly the opposite effect.

2. How a Single Change Breaks the Chain

A layer's cache becomes invalid as soon as either the instruction itself changes, or, in the case of COPY and ADD, the content of the copied files changes. Once a layer is invalidated, every subsequent layer in the Dockerfile is automatically invalidated too, even if those later instructions themselves remained completely unchanged. Docker cannot bypass this chain reaction, since every layer builds on the filesystem state of the previous one.

That is exactly what makes Dockerfile order the decisive factor: a layer that changes frequently should always sit as late as possible in the file, while rarely changing but computationally expensive steps such as package installation should be placed as early as possible, to benefit from the longest possible reusable cache prefix.


# Cache chain: if layer 2 changes, 3 and 4 are invalidated too
FROM node:20-alpine     # Layer 1: rarely changes
COPY package.json .     # Layer 2: changes occasionally
RUN npm install          # Layer 3: depends on layer 2
COPY . .                  # Layer 4: changes on every commit

3. The Most Common Anti-Pattern: COPY . . Too Early

By far the most common mistake is a Dockerfile that puts the entire source code at the very top with a single COPY . ., followed by installing the dependencies. Since that COPY instruction includes the entire project content, its hash changes on practically every commit, regardless of whether a dependency actually changed or just a single line of code in a completely unrelated file.

The consequence is that the following installation step, such as npm install or composer install, re-runs on every single build, even if package.json or composer.json has not changed in weeks. On larger projects with many dependencies, that means several minutes of unnecessary wait time per build, which quickly adds up to significant cost in a CI pipeline with a high build frequency.


# Anti-pattern: every code change invalidates npm install
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]

4. The Fix: Copy Dependency Files First

The established fix is to copy only the dependency manifest, i.e. package.json (plus package-lock.json) or composer.json (plus composer.lock), separately and before the rest of the source code. Only after that comes the installation command, and only in the last step is the remaining source code added with its own COPY instruction.

With this order, the expensive installation step stays valid in the cache until a dependency actually changes. Pure code changes, which make up the vast majority of everyday commits, then only invalidate the final, cheap COPY layer, while the entire installation step is reused from cache.


# Optimized: npm install stays cached
# as long as package.json/lock does not change
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
CMD ["node", "server.js"]

5. The Same Principle for PHP Projects Using Composer

PHP projects using Composer follow exactly the same principle, except composer.json and composer.lock are the relevant manifest files here. It is also worth running composer install with the --no-scripts flag before the rest of the project code is present, as long as the Composer scripts themselves do not strictly depend on the application code, to keep that cache layer as lean as possible.

In Magento projects with many dependencies, this difference is especially noticeable, since composer install there can take several minutes depending on the number of modules and network speed. A correct layer order turns this step from several minutes into practically zero seconds for pure code changes.


# Composer example with an optimized layer order
FROM php:8.4-fpm
WORKDIR /var/www/html
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader
COPY . .
RUN composer dump-autoload --optimize

6. Real Build Times Compared

In a test project with roughly 40 npm dependencies and a small code change in a single JavaScript file, the unoptimized Dockerfile with a leading COPY . . took about 90 seconds on every build, since npm install was fully re-run each time. After switching to the optimized order, build time for the exact same code change dropped to under 5 seconds, since only the final COPY layer and the final build step had to run again.

For an actual dependency change, such as adding a new npm package, build time stays similarly high in both variants, since the installation step has to run again regardless in that case. The benefit of the optimized order therefore shows up specifically in the vast majority of everyday commits, which touch code but not dependencies.

7. The Role of .dockerignore in Cache Behavior

An often overlooked factor in cache invalidation is the .dockerignore file. If it is missing or incomplete, directories like node_modules, .git, or local log files end up in the build context and potentially in the hash of a COPY instruction, even though they are entirely irrelevant to the build and often change unintentionally between two commits.

A carefully maintained .dockerignore file not only reduces the amount of data sent to the Docker daemon, it also ensures that a COPY layer's hash only changes when actually relevant source files change, which significantly improves the predictability of cache behavior.


# Typical .dockerignore for a Node/PHP project
node_modules
vendor
.git
*.log
var/cache
var/log
.env

8. Multi-Stage Builds as an Additional Lever

Multi-stage builds further amplify the effect of a good layer order, since build dependencies like compilers or dev tools live in a separate stage that only needs to run again on an actual code change, while the final, lean production stage simply takes over the finished artifacts. Combined with an optimized COPY order, this reduces both build time and the final image size.

It matters that cache rules apply independently within each stage, which means the instruction order in every single stage has to be optimized separately, and a good structure in the build stage does not automatically carry over to the runtime stage.


# Multi-stage build with optimized cache order
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html

9. A Checklist for a Cache-Friendly Dockerfile

Anyone auditing an existing Dockerfile for cache-friendliness should work through it systematically from top to bottom, asking for every instruction how often its content actually changes. Instructions with low change frequency belong near the top, instructions with high change frequency, above all copying the application code itself, consistently belong at the very end.

The table below summarizes the key principles once more together with their impact on build time, ordered by the effect observed in practice on the cache hit rate for everyday commits.

Principle Placed Incorrectly Placed Correctly Effect on Pure Code Changes
Copy dependencies first COPY . . before RUN npm install COPY package.json before RUN npm install Installation step stays cached
Rarely changing layers on top apt-get install after COPY . apt-get install before COPY . Package installation stays cached
Maintain .dockerignore node_modules in the build context node_modules excluded More stable hash for the COPY layer
Use multi-stage builds Build tools in the final image Build tools only in the build stage Smaller image, independent cache
Include lock files Only package.json copied package.json and package-lock.json copied Reproducible installation

Mironsoft

Container infrastructure, CI pipelines and deployment automation

Docker setups that hold up across the team and in production?

We review existing Dockerfiles and Compose stacks for security gaps, bloated images and fragile build pipelines, then build a container infrastructure that builds fast, runs securely and stays understandable across the team.

Dockerfile Review

Systematically optimizing multi-stage builds, layer caching and image size.

Security Audit

Hardening container isolation, secrets handling and image scanning against real attack surfaces.

CI/CD Integration

Building build pipelines, registries and deployment strategies for reproducible releases.

10. Summary

Build Cache Invalidation: The Essentials at a Glance

Core rule

Rarely changing instructions first, frequently changing instructions last in the Dockerfile.

Biggest lever

Copy and install manifest files like package.json before the rest of the code.

Observed effect

Build time for pure code changes dropped from 90 to under 5 seconds in a test project.

Additional lever

Maintain .dockerignore and use multi-stage builds to separate build and runtime.

11. FAQ: Build Cache Invalidation: The Essentials at a Glance

1What does layer caching actually mean in Docker?
Every instruction in a Dockerfile produces its own layer. Docker caches these layers and only re-runs an instruction if the instruction itself or its copied files changed since the last build.
2Why should package.json be copied before the rest of the source code?
Because the hash of a COPY . . changes on practically every commit, even if no dependency changed. A separate COPY for just package.json keeps the installation step cached until a dependency actually changes.
3What happens when a layer in the Dockerfile gets invalidated?
Every subsequent layer in the Dockerfile is automatically invalidated too, even if their own instructions remained unchanged, since each layer builds on the filesystem state of the previous one.
4How large can the realistic time savings from an optimized order be?
In a test project with roughly 40 npm dependencies, build time for pure code changes dropped from about 90 seconds to under 5 seconds, since the expensive installation step was reused from cache.
5Does the same principle apply to composer install for PHP projects?
Yes, exactly the same principle applies to composer.json and composer.lock. The installation step should also come before copying the rest of the application code.
6What role does the .dockerignore file play in caching?
A carefully maintained .dockerignore file prevents irrelevant files like node_modules or log files from entering the build context, which would otherwise unnecessarily change the hash of a COPY layer.
7Do multi-stage builds improve caching further?
Yes, by isolating build dependencies like compilers in a separate stage that is cached independently of the final, lean production stage, reducing both build time and image size.
8Does the cache still help on an actual dependency change?
No, if an actual dependency in package.json or composer.json changes, the installation step re-runs normally regardless of Dockerfile order, since the hash of the copied file changes.
9Why should lock files like package-lock.json also be copied?
Because they pin the exact, reproducible versions of every dependency. Without copying them, the installation inside the container could diverge from the locally tested version.
10Can a poor Dockerfile order also affect CI costs?
Yes, unnecessarily long build times add up quickly across frequent builds in a CI pipeline, resulting in significant extra compute time and measurable additional cost, especially under usage-based billing.