Dockerfile hygiene & multi‑stage builds — Performance Tuning Guide — Practical Guide (Dec 20, 2025)
Dockerfile hygiene & multi‑stage builds — Performance Tuning Guide
Level: Intermediate
Date: December 20, 2025
Docker remains a cornerstone for containerising applications, and in 2025 its core features have stabilised around versions 24.x.x. Optimising your Dockerfiles not only improves build speed and image size but also enhances runtime performance and security. This guide explores practical principles for Dockerfile hygiene and multi-stage builds, focusing on best practices relevant for Docker CE 20.10+ through the latest stable releases of 24.x. We’ll steer you away from common pitfalls and help you validate your optimisations effectively.
Prerequisites
- Basic knowledge of Docker CLI and Dockerfile syntax.
- Docker Engine version 20.10 or later (includes BuildKit enabled by default).
- Familiarity with container image concepts such as layers and manifests.
- Access to Docker Hub or equivalent registry for pulling base images.
Hands-on steps
1. Understand Dockerfile hygiene
Maintaining a clean, efficient Dockerfile is crucial for build performance and image size. Here are key tactics:
- Minimise layers: Combine related commands with
&&to reduce the overall layer count. EachRUN,COPY, andADDcreates a new layer. - Order your instructions: Place less frequently changing instructions near the top to leverage build cache better. For example, install dependencies before copying your application code.
- Avoid unnecessary tools in final images: Limit build-time tools and debugging utilities to intermediate stages.
- Clean up in the same layer: Remove package caches and temporary files inside the same
RUNcommand where you install or generate them.
FROM python:3.11-slim AS builder
RUN apt-get update && apt-get install -y build-essential
&& pip install --no-cache-dir wheel setuptools &&
apt-get clean && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip wheel -r requirements.txt --wheel-dir=wheels
FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /app/wheels /wheels
RUN pip install --no-cache-dir /wheels/*
COPY . .
CMD ["python", "app.py"]
2. Use multi-stage builds
Introduced officially since Docker 17.05, multi-stage builds let you separate build-time dependencies from runtime images, significantly reducing image size and attack surface. By copying only necessary artefacts from build stages, you keep your final image lean.
- Use descriptive stage names for clarity (
AS builder,AS tester, etc.). - Minimise copy operations to only required artefacts.
- Choose minimal base images for the final stage, such as
alpine,distroless(Google Distroless images are stable and recommended for security-conscious workloads), or slim variants.
3. Optimise caching and build context
Build cache is key to performance. Ensure that:
- Files are copied strategically. Copy files that rarely change before volatile ones. This increases the likelihood of cache hits.
- .dockerignore is well-maintained: Exclude unnecessary files (logs, local environment configs, node_modules, etc.) from the build context to reduce upload time and cache invalidations.
# .dockerignore example
.git
node_modules
*.log
.env
.vscode
4. Leverage BuildKit features
Since Docker 18.09, BuildKit is stable and the default builder in Docker 24.x. It dramatically improves build parallelism and caching. Some tips:
- Use
--mount=type=cachefor persistent cache between builds (e.g., for package managers or language-specific caches). - Use
RUN --mount=type=secretfor injecting sensitive build-time secrets (like API keys) without retaining them in final images. - Enable BuildKit explicitly if needed with
DOCKER_BUILDKIT=1(usually not necessary on recent Docker).
FROM node:20-alpine AS builder
WORKDIR /app
RUN --mount=type=cache,target=/root/.npm
npm install
COPY . .
RUN npm run build
Common pitfalls
- Too many layers from RUN instructions: Fragmented installs cause extra overhead and slower builds.
- Copying large directories unnecessarily: Leads to slower build context uploads and cache invalidation.
- No .dockerignore or poorly maintained .dockerignore: Causes large contexts which slow builds.
- Installing build tools in final image: Results in larger attack surface and unnecessary image weight.
- Ignoring cache implications when changing Dockerfile order: Changing order can invalidate cache leading to longer builds.
- Overusing
ADDinstead ofCOPY: UseCOPYunless leveragingADDfor tar extraction or URL fetches (which have their own risks).
Validation
To confirm that your Dockerfile improvements yield genuine benefits:
- Measure build time: Use
time docker build .or automated CI build logs. - Check image size: Use
docker image inspectordocker imagesand look at SIZE column. - Examine layers: Use
docker history <image>to see layer size and changes. Tools like container-diff aid analysis. - Scan for security: Scan your final images with tools like Docker Scan (powered by Snyk) to detect bloat or vulnerabilities.
Checklist / TL;DR
- Use multi-stage builds to isolate build dependencies from runtime environment.
- Write concise Dockerfiles with minimal layers by combining commands sensibly.
- Order instructions to maximise caching on rarely changing steps.
- Maintain a comprehensive
.dockerignoreto reduce build context size. - Enable and leverage BuildKit features for caching, secrets, and mounts.
- Avoid copy of unnecessary files or debugging tools in the final image.
- Validate improvements by measuring build time and image size before/after.
- Scan final images for security and remove unneeded components.