1. Order Instructions to Leverage Cache#
Put rarely-changing instructions first, frequently-changing ones last. Once a layer’s cache is invalidated, every subsequent layer is rebuilt.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
# ❌ Wrong: reinstall dependencies on every code change
FROM node:18-alpine
WORKDIR /app
COPY . . # code changes often
RUN npm ci --production # deps rarely change
CMD ["node", "server.js"]
# ✅ Right: reinstall deps only when they change
FROM node:18-alpine
WORKDIR /app
COPY package.json package-lock.json ./ # dep definitions (rarely change)
RUN npm ci --production # install deps (rarely change)
COPY . . # source code (changes often)
CMD ["node", "server.js"]
|
1
2
|
# Check cache hits
docker build -t myapp . 2>&1 | grep -E "(CACHED|Using cache)"
|
2. Use .dockerignore to Shrink the Build Context#
docker build sends the entire directory to the Docker daemon, including .git, node_modules, and other files you don’t need.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# .dockerignore
.git
.github
.vscode
.idea
node_modules
dist
build
*.md
*.log
.env
.env.local
.DS_Store
coverage
.nyc_output
|
1
2
3
4
|
# Check context size
docker build --no-cache -t test . 2>&1 | grep "Sending build context"
# Before: Sending build context to Docker daemon 1.2GB
# After: Sending build context to Docker daemon 50MB
|
3. Use Lightweight Base Images#
| Base image |
Size |
Notes |
ubuntu:22.04 |
~77MB |
Full Ubuntu |
python:3.11 |
~1GB |
Full Python |
python:3.11-slim |
~130MB |
Stripped down |
python:3.11-alpine |
~50MB |
Alpine-based |
1
2
3
4
5
6
7
8
|
# ❌ Too large
FROM python:3.11 # 1GB
# ✅ Recommended
FROM python:3.11-slim # 130MB
# ✅ Smallest (watch out for musl compatibility)
FROM python:3.11-alpine # 50MB
|
Alpine uses musl instead of glibc, which can break some software. If you hit issues, fall back to slim.
4. Combine RUN Instructions to Reduce Layers#
Every RUN creates a new layer — more layers means slower builds and larger images.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
# ❌ 5 layers
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y wget
RUN apt-get clean
RUN rm -rf /var/lib/apt/lists/*
# ✅ 1 layer
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
wget && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
|
1
2
|
# Check image layer count
docker history myapp:latest
|
5. Use Multi-Stage Builds#
Build in one stage, run in another, and copy only what’s needed.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# Stage 1: build
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o app .
# Stage 2: runtime (only ~15MB)
FROM alpine:3.18
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/app .
EXPOSE 8080
CMD ["./app"]
|
| Approach |
Image size |
Build time |
| Single-stage |
~1.2GB |
5 min |
| Multi-stage |
~15MB |
2 min |
6. Use BuildKit to Speed Up Builds#
BuildKit supports parallel builds and advanced caching.
1
2
3
4
5
6
7
|
# Enable BuildKit
export DOCKER_BUILDKIT=1
# Or enable it permanently in /etc/docker/daemon.json
# {
# "features": { "buildkit": true }
# }
|
1
2
3
4
5
6
7
8
9
10
|
# Use cache mounts (a BuildKit feature)
# syntax=docker/dockerfile:1
FROM node:18-alpine
WORKDIR /app
COPY package.json package-lock.json ./
# Cache mount avoids re-downloading deps every build
RUN --mount=type=cache,target=/root/.npm \
npm ci --production
COPY . .
CMD ["node", "server.js"]
|
1
2
|
# Check whether BuildKit is enabled
docker build --progress=plain -t myapp . 2>&1 | head -5
|
7. Use Cache Import/Export#
In CI/CD, the cache is lost after every build. Cache import/export keeps it across builds.
1
2
3
4
5
6
7
8
9
10
11
|
# Export to a local directory
docker build \
--cache-from type=local,src=/tmp/cache \
--cache-to type=local,dest=/tmp/cache \
-t myapp .
# Use a registry as cache backend
docker build \
--cache-from type=registry,ref=myregistry/myapp:cache \
--cache-to type=registry,ref=myregistry/myapp:cache,mode=max \
-t myapp .
|
1
2
3
4
5
6
7
8
9
|
# GitHub Actions example
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: myapp:latest
cache-from: type=gha
cache-to: type=gha,mode=max
|
8. Install Dependencies in Parallel#
1
2
3
4
5
6
7
8
9
10
11
12
|
# Node.js — npm ci is faster than npm install
RUN npm ci --production
# Python — disable pip cache
RUN pip install --no-cache-dir -r requirements.txt
# Go — download modules in parallel
RUN go mod download
# Rust — use cargo-chef
COPY --from=chef /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json
|
9. Clean Up Build Cache and Temp Files#
Clean up in the same RUN instruction to avoid leaving files in earlier layers.
1
2
3
4
5
6
7
8
9
10
11
12
|
# ✅ Right: clean up in the same layer
RUN apt-get update && \
apt-get install -y --no-install-recommends build-essential && \
make && make install && \
apt-get purge -y --auto-remove build-essential && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# ❌ Wrong: cleanup in another layer — files still exist
RUN apt-get update && apt-get install -y build-essential
RUN make && make install
RUN apt-get clean # too late! earlier layers still contain the files
|
Per-language cleanup commands:
1
2
3
4
5
6
7
8
|
# Node.js
RUN npm ci --production && npm cache clean --force
# Python
RUN pip install --no-cache-dir -r requirements.txt
# Go
RUN go build -o app . && go clean -cache
|
10. Pin a Specific Version Tag#
1
2
3
4
5
6
7
8
|
# ❌ latest may change, cache is unpredictable
FROM node:latest
# ✅ Specific version
FROM node:18.17.0-alpine3.18
# ✅ Safest: use the SHA256 digest
FROM node:18.17.0-alpine3.18@sha256:abc123...
|
How Layers Work#
1
2
|
# View image layer history
docker history <image-name>
|
Instructions that create new layers: FROM, RUN, COPY, ADD
Instructions that don’t create new layers (just metadata): CMD, ENTRYPOINT, ENV, EXPOSE, WORKDIR, USER, LABEL, ARG, VOLUME
1
2
3
4
5
6
7
8
9
10
11
|
# This creates 4 layers
FROM node:18-alpine # layer 1
RUN apk add --no-cache curl # layer 2
COPY package.json . # layer 3
RUN npm install # layer 4
# These instructions add no layer
WORKDIR /app # no layer
ENV NODE_ENV=production # no layer
EXPOSE 3000 # no layer
CMD ["node", "server.js"] # no layer
|
Before / After Comparison#
1
2
3
4
5
6
7
8
9
|
# ❌ Before optimization (8 min, 1.2GB)
FROM ubuntu:22.04
RUN apt-get update
RUN apt-get install -y python3 python3-pip
RUN apt-get install -y build-essential
WORKDIR /app
COPY . .
RUN pip3 install -r requirements.txt
CMD ["python3", "app.py"]
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
# ✅ After optimization (1.5 min, 150MB)
# syntax=docker/dockerfile:1
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --no-cache-dir -r requirements.txt
FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY . .
EXPOSE 8000
CMD ["python", "app.py"]
|
| Metric |
Before |
After |
Improvement |
| Build time |
8 min |
1.5 min |
81% ⬇️ |
| Image size |
1.2GB |
150MB |
87% ⬇️ |
| Context size |
500MB |
50MB |
90% ⬇️ |
| Cache hit rate |
10% |
90% |
80% ⬆️ |
Monitoring and Debugging#
1
2
3
4
5
6
7
8
9
10
11
|
# Show detailed build time
time docker build -t myapp .
# BuildKit detailed progress
DOCKER_BUILDKIT=1 docker build --progress=plain -t myapp .
# Analyze image layers
docker history myapp:latest
# Use dive for deep layer analysis
dive myapp:latest
|