
How to Speed Up Docker Builds: Utilizing Layer Caching and Multi-Stage Builds
Slow CI/CD build pipelines are a major bottleneck for modern deployment workflows. A developer commits a change, pushes to git, and then waits ten minutes for Docker to download dependencies, compile code, and packaging assets.
Often, this slowness is caused by poorly structured Dockerfiles that invalidate cache layers unnecessarily, copying raw debug assets, or shipping bloated runtimes.
In this guide, we will explore Docker's layer cache mechanics, learn how to reorder instructions, write efficient multi-stage build scripts, and reduce image sizes by 80 percent.
How Docker Cache Invalidations Happen
Docker builds images sequentially, executing each line in your Dockerfile as a distinct, cached read-only layer.
When you trigger a build:
- Docker checks if a layer matching the command and the files referenced by it already exists in the cache.
- If it does, Docker skips executing the step and imports the cached layer.
- If a step experiences a change (e.g., source file edit), that layer and all subsequent layers are invalidated, forcing Docker to execute them from scratch.
This means the ordering of your Dockerfile instructions dictates your build performance.
Strategy 1: Ordering Layers by Volatility
The most common mistake is copying all project files before installing dependencies.
Consider this slow pattern:
# SLOW PATTERN
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN pnpm install
CMD ["node", "server.js"]Because COPY . . is executed before RUN pnpm install, any minor edit to a single code file invalidates the COPY layer cache. Docker is then forced to re-run the time-consuming pnpm install command on every single commit.
Here is the fast, optimized pattern:
# FAST PATTERN
FROM node:20-alpine
WORKDIR /app
# Copy dependency configs first
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# Copy the rest of the source code later
COPY . .
CMD ["node", "server.js"]Since package lists change rarely compared to code files, Docker will cache the install layer. When you modify your application logic, Docker skips dependency installation entirely, completing builds in seconds.
Strategy 2: Using Multi-Stage Builds
A typical compilation workflow requires heavy SDKs, compile tools, and development dependencies. However, your production runtime only needs the final compiled static bundle or production build.
Multi-Stage Builds allow you to use multiple temporary FROM statements in a single Dockerfile. You compile assets in a heavy build stage, and then copy only the finalized assets into a separate, clean runtime stage.
# Stage 1: Build environment
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm run build
# Stage 2: Clean production runtime
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# Install only production dependencies
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --prod --frozen-lockfile
# Copy only the compiled build folder from the first stage
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/server.js"]By separating the build stage, your final image drops build tools and source code, shrinking the footprint from 800MB to under 150MB, speeding up deployments.
Strategy 3: Write a Precise .dockerignore File
When you build a Docker image, Docker copies all files in the current folder to the Docker daemon. If you have massive local directories (like node_modules or .git) they are uploaded to the builder, slowing down startup time.
Create a .dockerignore file in your root folder:
node_modules
.git
.github
dist
.env
Dockerfile
.dockerignoreThis ensures only source files are transferred, protecting local credentials and accelerating container startup speeds.
Conclusion
Optimizing Docker build pipelines is essential for modern continuous integration. By ordering commands from least volatile to most volatile to maximize layer caching, implementing multi-stage builds to prune build-time packages, and writing precise .dockerignore exclusions, you can build secure, lightweight containers in seconds.