
Docker Impl Build Optimization
- 14 installs
- 9 repo stars
- Updated July 8, 2026
- openaec-foundation/docker-claude-skill-package
Helps with devops & ci/cd tasks.
About
docker-impl-build-optimization is a Claude Code skill for devops & ci/cd. It helps solo builders move faster with AI-assisted development.
- docker-impl-build-optimization
- DevOps & CI/CD
- AI-coding skill
Docker Impl Build Optimization by the numbers
- 14 all-time installs (skills.sh)
- +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #957 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/docker-claude-skill-package --skill docker-impl-build-optimizationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 14 |
|---|---|
| repo stars | ★ 9 |
| Last updated | July 8, 2026 |
| Repository | openaec-foundation/docker-claude-skill-package ↗ |
What it does
Helps with devops & ci/cd tasks.
Files
docker-impl-build-optimization
Quick Reference
Layer Caching Rules
Docker checks each instruction against its cache before executing. If the instruction and its inputs match a cached layer, the cached version is reused.
Critical rule: Once ANY layer's cache is invalidated, ALL subsequent layers MUST rebuild.
Cache Invalidation Triggers
| Instruction | Cache Key | Invalidation Trigger |
|---|---|---|
FROM | Image reference | Base image tag/digest changed |
RUN | Command string only | Command text changed (NOT external resources) |
COPY | File content checksums | File content changed (mtime is NOT checked) |
ADD | File checksums + URL content | File content or URL content changed |
ENV | Key=Value pair | Value changed |
ARG | Name=Value pair | Value changed |
WORKDIR | Path + SOURCE_DATE_EPOCH | Path or epoch changed |
Critical Warnings
NEVER separate apt-get update and apt-get install into different RUN instructions -- the cached update layer becomes stale and subsequent installs may fail or use outdated packages.
NEVER use COPY . . before dependency installation -- ANY file change invalidates the COPY layer and forces a full reinstall of all dependencies.
NEVER rely on RUN cache for external resources -- Docker only checks the command string, not what apt-get install or curl fetches. Use --no-cache or --no-cache-filter to force fresh downloads.
ALWAYS include a .dockerignore file -- without it, the entire build context (including .git/, node_modules/, test data) is sent to the builder.
ALWAYS use # syntax=docker/dockerfile:1 at the top of every Dockerfile to enable BuildKit cache mounts and other optimizations.
---
Instruction Ordering Strategy
Order instructions from LEAST frequently changed to MOST frequently changed:
+--------------------------------------------------+
| FROM base-image (rarely changes) |
+--------------------------------------------------+
| RUN install system packages (rarely changes) |
+--------------------------------------------------+
| COPY package.json / go.mod / *.csproj (dep changes) |
+--------------------------------------------------+
| RUN install dependencies (dep changes) |
+--------------------------------------------------+
| COPY . . (every commit) |
+--------------------------------------------------+
| RUN build application (every commit) |
+--------------------------------------------------+
| CMD / ENTRYPOINT (rarely changes) |
+--------------------------------------------------+
CACHE FLOWS TOP-DOWN
First invalidation breaks ALL belowPrinciple: Expensive, slow-changing operations go at the top. Frequently changing source code goes at the bottom.
---
.dockerignore Template
ALWAYS create a .dockerignore in the project root:
# Version control
.git
.gitignore
.gitattributes
# Dependencies (rebuilt inside container)
node_modules
vendor
__pycache__
*.pyc
.venv
# Build artifacts
dist
build
target
*.o
*.exe
# IDE and OS files
.vscode
.idea
*.swp
*.swo
.DS_Store
Thumbs.db
# Docker files (not needed in build context)
Dockerfile*
docker-compose*.yml
.dockerignore
# Documentation and non-essential files
*.md
LICENSE
docs/
# Environment and secrets
.env
.env.*
*.pem
*.key
*.cert
# Test and CI files
.github
.gitlab-ci.yml
tests/
coverage/Negation syntax: Use ! to re-include files excluded by a broader pattern:
*.md
!README.md---
Cache Mount Patterns
Cache mounts (--mount=type=cache) persist package manager caches across builds. Even when a layer rebuilds, only new or changed packages are downloaded.
apt-get (Debian/Ubuntu)
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && apt-get install -y --no-install-recommends \
curl gitALWAYS use sharing=locked for apt -- concurrent access corrupts the cache.
npm
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ciyarn
COPY package.json yarn.lock ./
RUN --mount=type=cache,target=/usr/local/share/.cache/yarn \
yarn install --frozen-lockfilepnpm
COPY package.json pnpm-lock.yaml ./
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfilepip (Python)
COPY requirements.txt ./
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txtGo modules
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go build -o /app/server ./cmdCargo (Rust)
COPY Cargo.toml Cargo.lock ./
RUN --mount=type=cache,target=/app/target/ \
--mount=type=cache,target=/usr/local/cargo/git/db \
--mount=type=cache,target=/usr/local/cargo/registry/ \
cargo build --releaseMaven (Java)
COPY pom.xml ./
RUN --mount=type=cache,target=/root/.m2/repository \
mvn dependency:resolve
COPY src ./src
RUN --mount=type=cache,target=/root/.m2/repository \
mvn package -DskipTestsNuGet (.NET)
COPY *.csproj ./
RUN --mount=type=cache,target=/root/.nuget/packages \
dotnet restore
COPY . ./
RUN --mount=type=cache,target=/root/.nuget/packages \
dotnet publish -c Release -o /app---
Bind Mounts for Large Contexts
When source code is only needed to produce an artifact, use bind mounts instead of COPY to avoid persisting source files in any layer:
FROM golang:1.22 AS build
WORKDIR /src
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=bind,source=go.sum,target=go.sum \
--mount=type=bind,source=go.mod,target=go.mod \
go mod download
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
--mount=type=bind,target=. \
go build -o /bin/app ./cmdAdvantages:
- Mounted files are NOT persisted in any layer
- Only the RUN output is kept in the image
- Avoids bloating the build cache with source files
- Bind mounts are read-only by default (safe)
---
CI/CD Cache Backends
Registry Cache (recommended for teams)
docker buildx build --push -t registry/app:latest \
--cache-to type=registry,ref=registry/app:buildcache,mode=max \
--cache-from type=registry,ref=registry/app:buildcache .GitHub Actions Cache
- uses: docker/build-push-action@v7
with:
push: true
tags: user/app:latest
cache-from: type=gha
cache-to: type=gha,mode=maxMulti-Branch Cache Strategy
docker buildx build --push -t registry/app:latest \
--cache-to type=registry,ref=registry/app:cache:$BRANCH \
--cache-from type=registry,ref=registry/app:cache:$BRANCH \
--cache-from type=registry,ref=registry/app:cache:main .ALWAYS fall back to the main branch cache when the feature branch cache misses.
Cache Modes
| Mode | Behavior | Use Case |
|---|---|---|
min (default) | Caches only exported layers | Smaller cache, faster export |
max | Caches ALL layers including intermediates | More cache hits, larger storage |
ALWAYS use mode=max in CI/CD to maximize cache reuse across builds.
---
Layer Squashing Considerations
Docker does NOT support true layer squashing natively. Options:
| Approach | How | Trade-off |
|---|---|---|
| Multi-stage builds | Copy only final artifacts to clean stage | Best approach -- no extra tooling |
--squash (experimental) | Merge all layers into one | Loses all intermediate cache |
docker export/import | Flatten to single layer | Loses metadata, CMD, ENV, etc. |
ALWAYS prefer multi-stage builds over squashing -- they preserve caching while producing minimal final images.
---
Forcing Cache Invalidation
# Invalidate ALL cache
docker build --no-cache .
# Invalidate a specific stage only
docker build --no-cache-filter install .
# Pull fresh base images
docker build --pull .
# Clear entire builder cache
docker builder prune
# Clear with size limit
docker builder prune --keep-storage 5GB---
Reference Links
- references/caching-rules.md -- Complete cache invalidation rules per instruction type
- references/examples.md -- Optimized Dockerfiles before/after, .dockerignore patterns
- references/anti-patterns.md -- Caching and optimization mistakes with explanations
Official Sources
- https://docs.docker.com/build/cache/
- https://docs.docker.com/build/cache/invalidation/
- https://docs.docker.com/build/cache/optimize/
- https://docs.docker.com/build/cache/backends/
- https://docs.docker.com/reference/dockerfile/
- https://docs.docker.com/build/building/best-practices/
Build Optimization Anti-Patterns
Reference file for docker-impl-build-optimization.
Each anti-pattern includes what goes wrong and the correct approach.
---
AP-001: COPY Everything Before Installing Dependencies
The mistake:
FROM node:20
WORKDIR /app
COPY . . # Copies ALL source files
RUN npm install # Reinstalls on EVERY source changeWhy it fails: COPY . . creates a cache key based on ALL files in the build context. Any change to any file -- even editing a comment in a source file -- invalidates the COPY layer and forces a complete npm install from scratch.
The fix:
FROM node:20
WORKDIR /app
COPY package.json package-lock.json ./ # Only dependency files
RUN npm ci # Cached until deps change
COPY . . # Source changes only affect this layer---
AP-002: Separate apt-get update and install
The mistake:
RUN apt-get update
RUN apt-get install -y curlWhy it fails: The apt-get update layer is cached based on the command string. Days later, when you add nginx to the install line, Docker reuses the stale apt-get update cache. The package index is outdated, and apt-get install may fail or install old versions.
The fix:
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
nginx \
&& rm -rf /var/lib/apt/lists/*ALWAYS combine update and install in a single RUN.
---
AP-003: Missing .dockerignore
The mistake: No .dockerignore file in the project.
Why it fails: The entire project directory becomes the build context, including:
node_modules/(often 500MB+).git/(entire repository history, can be gigabytes)- Test data, documentation, IDE configs
.envfiles with secrets
This slows down every build because the entire context must be sent to the Docker daemon. It also causes unnecessary cache invalidation -- any file change in any ignored directory triggers COPY . . to rebuild.
The fix: ALWAYS create a .dockerignore file. See the SKILL.md template for a comprehensive starter.
---
AP-004: Not Using Cache Mounts
The mistake:
COPY requirements.txt ./
RUN pip install -r requirements.txtWhy it fails: When the requirements change, pip downloads ALL packages from scratch, even those already downloaded in a previous build. Without a cache mount, the pip download cache is discarded with the old layer.
The fix:
COPY requirements.txt ./
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txtThe cache mount persists across builds. Even when the RUN layer rebuilds, pip reuses previously downloaded packages and only fetches what changed.
---
AP-005: Expecting RUN Cache to Detect External Changes
The mistake:
RUN curl -sL https://example.com/latest-release.tar.gz | tar xzWhy it fails: Docker caches the RUN layer based ONLY on the command string. If the URL content changes (new release uploaded), Docker still uses the cached layer because the command text is identical. The build silently uses stale content.
The fix:
# Option 1: Use --no-cache for the specific stage
docker build --no-cache-filter download .
# Option 2: Use a build arg as cache buster
ARG RELEASE_VERSION=1.0.0
RUN curl -sL "https://example.com/release-${RELEASE_VERSION}.tar.gz" | tar xz
# Option 3: Force full rebuild
docker build --no-cache .---
AP-006: Cache Mounts Without Correct Sharing Mode for apt
The mistake:
RUN --mount=type=cache,target=/var/cache/apt \
--mount=type=cache,target=/var/lib/apt \
apt-get update && apt-get install -y curlWhy it fails: The default sharing mode is shared, which allows concurrent read/write. apt's lock mechanism conflicts with this, causing corruption when parallel builds access the same cache.
The fix:
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && apt-get install -y curlALWAYS use sharing=locked for apt caches.
---
AP-007: Using min Cache Mode in CI/CD
The mistake:
docker buildx build \
--cache-to type=registry,ref=myapp:cache \
--cache-from type=registry,ref=myapp:cache .Why it fails: The default cache mode is min, which only exports the layers present in the final image. Intermediate build stage layers (dependency downloads, compilation steps) are NOT cached. On the next CI run, those expensive intermediate steps must be repeated.
The fix:
docker buildx build \
--cache-to type=registry,ref=myapp:cache,mode=max \
--cache-from type=registry,ref=myapp:cache .ALWAYS use mode=max in CI/CD to cache all intermediate layers.
---
AP-008: Ignoring Build Context Size
The mistake: Running docker build . in a directory with large files, vendor directories, or data sets without measuring context size.
Why it fails: The build context is transferred in its entirety to the Docker daemon before any instruction executes. A 2GB context takes significant time to transfer, even if the Dockerfile only copies a few files.
How to detect:
# Check context size (first line of build output)
docker build . 2>&1 | head -1
# Output: "Sending build context to Docker daemon 2.1GB"The fix: 1. Add a .dockerignore file (AP-003) 2. Use bind mounts instead of COPY for large source trees 3. Use a subdirectory as the build context: docker build -f Dockerfile ./src
---
AP-009: Squashing Instead of Multi-Stage
The mistake:
docker build --squash -t myapp .Why it fails: Squashing merges ALL layers into one, destroying all intermediate layer cache. Every rebuild starts from scratch. The --squash flag is experimental and not recommended for production.
The fix: Use multi-stage builds to produce a clean final image while preserving layer caching:
FROM golang:1.22 AS build
# ... build steps with full caching ...
FROM alpine:3.19
COPY --from=build /app/binary /usr/bin/binaryThe final image contains only what you explicitly COPY into it, with zero build artifacts.
---
AP-010: Not Falling Back to Main Branch Cache in CI
The mistake:
# Feature branch CI
docker buildx build \
--cache-from type=registry,ref=myapp:cache-feature-123 \
--cache-to type=registry,ref=myapp:cache-feature-123 .Why it fails: A new feature branch has no cache yet. Every layer rebuilds from scratch on the first CI run, which can take 10-30 minutes for complex builds.
The fix:
docker buildx build \
--cache-from type=registry,ref=myapp:cache-feature-123 \
--cache-from type=registry,ref=myapp:cache-main \
--cache-to type=registry,ref=myapp:cache-feature-123,mode=max .ALWAYS include the main branch cache as a fallback source. Docker tries cache sources in order and uses the first match.
---
AP-011: Unnecessary Layer Creation
The mistake:
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
RUN apt-get install -y wget
RUN rm -rf /var/lib/apt/lists/*Why it fails: Each RUN creates a separate layer. The rm in the last layer does NOT reduce image size -- the files still exist in the previous layers. Docker images are additive: deleting a file in a later layer only hides it, the bytes remain.
The fix:
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
git \
wget \
&& rm -rf /var/lib/apt/lists/*ALWAYS combine related operations (install + cleanup) in a single RUN so removed files never exist in a committed layer.
---
AP-012: Secrets in Build Args or ENV
The mistake:
ARG DATABASE_PASSWORD=secret123
ENV API_KEY=sk-1234567890
RUN deploy.shWhy it fails: ARG values are visible in docker history. ENV values persist in the image metadata and are visible via docker inspect. Both leak secrets.
Additionally, changing a secret value in ARG invalidates the cache, causing unnecessary rebuilds. Secret mounts do NOT affect cache keys.
The fix:
RUN --mount=type=secret,id=db_pass,target=/run/secrets/db_pass \
--mount=type=secret,id=api_key,env=API_KEY \
deploy.shdocker build \
--secret id=db_pass,src=./db_pass.txt \
--secret id=api_key,src=./api_key.txt .NEVER use ARG or ENV for secrets. ALWAYS use --mount=type=secret.
---
AP-013: Using COPY --link Without Understanding Its Behavior
The mistake: Blindly adding --link to all COPY instructions expecting faster builds.
Why it fails: COPY --link creates layers that are independent of preceding layers. This means:
- The layer can be cached even if a preceding layer changes
- BUT the files are placed in a new snapshot, not on top of the existing filesystem
- If the COPY destination depends on a directory created by a previous RUN,
--linkmay not work as expected
When to use `--link`:
- Copying final artifacts into a clean runtime stage
COPY --link --from=build /app/binary /usr/bin/binary
When NOT to use `--link`:
- When the COPY target directory is created by a preceding instruction
- When you need the copied files to interact with the existing layer filesystem
---
Summary: Cache Optimization Checklist
1. Does the Dockerfile have # syntax=docker/dockerfile:1? 2. Is there a .dockerignore file? 3. Are dependency files copied BEFORE source code? 4. Are apt-get update and install in the SAME RUN? 5. Do package managers use --mount=type=cache? 6. Does CI use mode=max for cache export? 7. Does CI include main branch as fallback cache source? 8. Are secrets using --mount=type=secret (not ARG/ENV)? 9. Are install and cleanup in the same RUN layer? 10. Is the final stage using a minimal base image?
Cache Invalidation Rules Per Instruction Type
Reference file for docker-impl-build-optimization.
Source: https://docs.docker.com/build/cache/invalidation/
---
How Docker Layer Caching Works
1. Docker processes each instruction in order, top to bottom. 2. For each instruction, Docker checks whether a cached layer exists. 3. If the cache key matches, the cached layer is reused (cache HIT). 4. If the cache key does NOT match, the layer is rebuilt (cache MISS). 5. Once a cache miss occurs, ALL subsequent layers MUST rebuild -- even if their own cache keys have not changed.
This cascade behavior is the single most important rule for build optimization.
---
Instruction-by-Instruction Cache Rules
FROM
Cache key: Image reference (name + tag or digest).
Invalidation triggers:
- The image tag resolves to a different digest (e.g.,
node:20was updated on Docker Hub) - The digest is explicitly different
--pullflag is used (forces fresh pull and re-evaluation)
Behavior:
- With tag only: Docker checks if the local image matches the remote. If not pulled recently, uses local cache.
- With digest: Exact match required -- fully deterministic.
docker build --pullforces a fresh pull, which may invalidate the FROM cache.
Best practice: Pin to digest for reproducible builds. Use tags for development convenience.
---
RUN
Cache key: The command string (the exact text after RUN).
Invalidation triggers:
- The command text changes (even whitespace or comments)
- A preceding layer was invalidated (cascade)
NOT an invalidation trigger:
- External resource changes (package repository updates, remote file changes)
- Different output from the same command on a different day
- Environment variables set outside the Dockerfile
Important details:
RUN apt-get updatecaches the layer. Running the same command days later still uses the cache, even though the package index is stale. This is whyapt-get update && apt-get installMUST be in a single RUN.RUN --mount=type=cachedoes NOT affect cache key computation -- the mount target is separate from the layer cache.RUN --mount=type=secretdoes NOT invalidate cache when the secret content changes.
Example -- same cache key:
# These two produce the SAME cache key:
RUN echo "hello"
RUN echo "hello"
# This produces a DIFFERENT cache key:
RUN echo "hello " # trailing space---
COPY
Cache key: File content checksums of all source files.
Invalidation triggers:
- Any source file's content changed (even one byte)
- Files were added or removed from the source glob pattern
--chmodor--chownvalues changed
NOT an invalidation trigger:
- File modification timestamp (mtime) changed without content change
- File access time changed
- File ownership changed on the host (only the content matters)
Important details:
- Docker computes a checksum of every file matching the source pattern.
- For directories, checksums include all files recursively.
- The
.dockerignorefile affects which files are in the build context, which indirectly affects COPY cache. COPY --linkcreates an independent layer that can be cached separately from preceding layers.
Example -- understanding COPY cache:
# This caches based on ONLY package.json and lock file content:
COPY package.json package-lock.json ./
# This caches based on ALL files in the build context:
COPY . .The first pattern is dramatically better for caching because it only invalidates when dependencies change.
---
ADD
Cache key: File content checksums + URL response content.
Invalidation triggers:
- Local file content changed (same as COPY)
- Remote URL content changed (HTTP response body differs)
- Git repository changed (for Git URL sources)
--checksumvalue changed
NOT an invalidation trigger:
- HTTP headers changing without body change
- Remote server returning different headers with same content
Important details:
- For remote URLs, Docker checks the actual response content, not just the URL string.
- For Git sources, Docker checks the commit hash at the specified ref.
- ADD has auto-extraction behavior for tar archives -- the extracted content is what gets cached.
---
ENV
Cache key: The key=value pair.
Invalidation triggers:
- The value changed
- The key name changed
Important details:
- ENV values persist across layers and into the final image.
- Changing an ENV value invalidates that layer AND all subsequent layers.
- ENV set in a parent image (FROM) is inherited and does NOT trigger invalidation unless overridden.
---
ARG
Cache key: The name=value pair (only when the ARG is USED in subsequent instructions).
Invalidation triggers:
- The build-arg value changed AND the ARG is referenced in a subsequent instruction
- An unused ARG does NOT invalidate any cache
Important details:
- ARG declared before FROM is only available in FROM itself, not in subsequent instructions.
- ARG must be re-declared after FROM to be used within a stage.
--build-argvalues that differ from the default trigger cache invalidation.- Predefined ARGs (like HTTP_PROXY) do NOT cause cache invalidation unless explicitly referenced.
Example -- ARG cache behavior:
ARG VERSION=1.0
FROM alpine:3.19
ARG VERSION # Re-declare to use within stage
RUN echo $VERSION # Cache key includes VERSION valueChanging --build-arg VERSION=2.0 invalidates the RUN layer because it references VERSION.
---
WORKDIR
Cache key: The directory path + SOURCE_DATE_EPOCH value.
Invalidation triggers:
- The path changed
SOURCE_DATE_EPOCHbuild-arg changed (affects directory creation timestamp)
Important details:
- WORKDIR creates the directory if it does not exist.
- Multiple WORKDIR instructions stack (relative paths accumulate).
- WORKDIR itself rarely causes cache issues -- it is the instructions AFTER it that matter.
---
EXPOSE, LABEL, USER, VOLUME, STOPSIGNAL, SHELL
Cache key: The instruction arguments.
Invalidation triggers:
- The arguments changed.
These metadata instructions have simple cache behavior. They rarely cause optimization issues because they are typically static.
---
HEALTHCHECK
Cache key: The full instruction including options and command.
Invalidation triggers:
- Any option changed (interval, timeout, retries, start-period)
- The command changed
---
Cache Cascade Visualization
Layer 1: FROM node:20 [CACHE HIT]
Layer 2: WORKDIR /app [CACHE HIT]
Layer 3: COPY package.json . [CACHE HIT] -- file unchanged
Layer 4: RUN npm ci [CACHE HIT] -- command unchanged, no prior miss
Layer 5: COPY . . [CACHE MISS] -- source file changed
Layer 6: RUN npm run build [MUST REBUILD] -- cascade from Layer 5
Layer 7: CMD ["node", "dist/"] [MUST REBUILD] -- cascade from Layer 5In this example, only layers 5-7 rebuild. Layers 1-4 are reused from cache, saving the expensive npm ci step.
---
Cache Key Summary Table
| Instruction | What Docker Checks | What Docker Ignores |
|---|---|---|
FROM | Image reference + digest | Pull frequency |
RUN | Command string text | External resource state |
COPY | File content checksums | mtime, permissions on host |
ADD | File checksums + URL body | HTTP headers, URL string alone |
ENV | Key=Value text | Runtime overrides |
ARG | Name=Value (if used) | Unused ARGs |
WORKDIR | Path + SOURCE_DATE_EPOCH | Directory contents |
---
Practical Rules for Cache Optimization
1. ALWAYS put instructions that change least at the top of the Dockerfile. 2. ALWAYS separate dependency file copying (package.json, go.mod) from source code copying. 3. ALWAYS combine apt-get update and apt-get install in a single RUN to prevent stale index cache. 4. NEVER rely on RUN cache for fetching latest versions -- the command string is the only cache key. 5. ALWAYS use .dockerignore to exclude files that cause unnecessary COPY cache invalidation. 6. ALWAYS use --mount=type=cache for package manager caches -- they survive layer rebuilds. 7. NEVER put COPY . . before RUN install-dependencies -- any source file change triggers a full dependency reinstall.
Build Optimization Examples
Reference file for docker-impl-build-optimization.
All examples verified against Docker Engine 24+ with BuildKit.
---
Before/After: Node.js Application
Before (poor cache usage)
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/index.js"]Problems:
COPY . .beforenpm install-- ANY file change triggers full dependency reinstall- No
.dockerignore-- sendsnode_modules/,.git/, test files to builder - No cache mounts -- npm downloads everything from scratch on rebuild
- No multi-stage -- build tools and devDependencies remain in final image
- Using full
node:20image (~1GB) for runtime
After (optimized)
# syntax=docker/dockerfile:1
# Build stage
FROM node:20-bookworm-slim AS build
WORKDIR /app
# Install dependencies first (changes less often than source code)
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --production=false
# Copy source and build (changes every commit)
COPY . .
RUN npm run build
# Runtime stage
FROM node:20-bookworm-slim AS runtime
WORKDIR /app
# Copy only production dependencies and built output
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json ./
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]Improvements:
- Dependency files copied separately --
npm cionly reruns whenpackage.jsonor lockfile changes - Cache mount on
/root/.npm-- npm reuses downloaded packages across builds - Multi-stage build -- build tools excluded from final image
- Slim base image -- ~200MB smaller than full image
- Non-root user for security
---
Before/After: Python Application
Before
FROM python:3.12
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "app.py"]After
# syntax=docker/dockerfile:1
FROM python:3.12-slim AS runtime
WORKDIR /app
# Install dependencies first
COPY requirements.txt ./
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --no-compile -r requirements.txt
# Copy application source
COPY . .
RUN groupadd -r appuser && useradd -r -g appuser appuser
USER appuser
CMD ["python", "app.py"]---
Before/After: Go Application
Before
FROM golang:1.22
WORKDIR /app
COPY . .
RUN go build -o server ./cmd/server
EXPOSE 8080
CMD ["./server"]After
# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS build
ARG TARGETOS TARGETARCH
WORKDIR /src
# Download dependencies (cached separately from source)
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=bind,source=go.sum,target=go.sum \
--mount=type=bind,source=go.mod,target=go.mod \
go mod download
# Build with bind mount (source not persisted in layers)
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
--mount=type=bind,target=. \
GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 \
go build -ldflags="-s -w" -o /bin/server ./cmd/server
# Minimal runtime
FROM alpine:3.19 AS runtime
RUN addgroup -S app && adduser -S app -G app
COPY --from=build /bin/server /usr/bin/server
USER app
EXPOSE 8080
ENTRYPOINT ["/usr/bin/server"]Key techniques:
- Bind mount for source -- no COPY layer, source not in any image layer
- Separate cache mounts for Go modules and build cache
- Cross-compilation with BUILDPLATFORM and TARGETARCH
CGO_ENABLED=0for static binary -- can run onscratchoralpine-ldflags="-s -w"strips debug info, reducing binary size ~30%
---
Before/After: Java (Maven) Application
Before
FROM maven:3.9-eclipse-temurin-21
WORKDIR /app
COPY . .
RUN mvn package
CMD ["java", "-jar", "target/app.jar"]After
# syntax=docker/dockerfile:1
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /app
# Resolve dependencies first
COPY pom.xml ./
RUN --mount=type=cache,target=/root/.m2/repository \
mvn dependency:resolve dependency:resolve-plugins
# Build application
COPY src ./src
RUN --mount=type=cache,target=/root/.m2/repository \
mvn package -DskipTests -o
# Runtime with JRE only
FROM eclipse-temurin:21-jre-alpine AS runtime
WORKDIR /app
COPY --from=build /app/target/app.jar ./app.jar
RUN addgroup -S app && adduser -S app -G app
USER app
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]---
Before/After: Rust Application
Before
FROM rust:1.77
WORKDIR /app
COPY . .
RUN cargo build --release
CMD ["./target/release/myapp"]After
# syntax=docker/dockerfile:1
FROM rust:1.77-slim AS build
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
COPY src ./src
RUN --mount=type=cache,target=/app/target/ \
--mount=type=cache,target=/usr/local/cargo/git/db \
--mount=type=cache,target=/usr/local/cargo/registry/ \
cargo build --release && \
cp /app/target/release/myapp /usr/local/bin/myapp
FROM debian:bookworm-slim AS runtime
RUN groupadd -r app && useradd -r -g app app
COPY --from=build /usr/local/bin/myapp /usr/local/bin/myapp
USER app
ENTRYPOINT ["/usr/local/bin/myapp"]---
.dockerignore Pattern Examples
Node.js Project
.git
.gitignore
node_modules
npm-debug.log*
dist
build
coverage
.nyc_output
.env
.env.*
*.md
LICENSE
.vscode
.idea
Dockerfile*
docker-compose*.yml
.dockerignore
tests/
__tests__/
*.test.js
*.spec.js
.eslintrc*
.prettierrc*
jest.config.*Python Project
.git
.gitignore
__pycache__
*.pyc
*.pyo
.venv
venv
env
.env
.env.*
*.egg-info
dist
build
.pytest_cache
.mypy_cache
.tox
coverage.xml
htmlcov
*.md
LICENSE
.vscode
.idea
Dockerfile*
docker-compose*.yml
.dockerignore
tests/
docs/Go Project
.git
.gitignore
bin/
vendor/
*.test
*.out
coverage.txt
.env
*.md
LICENSE
.vscode
.idea
Dockerfile*
docker-compose*.yml
.dockerignore
*_test.go
testdata/
docs/Monorepo / General
.git
.github
.gitlab-ci.yml
.circleci
.travis.yml
# IDE
.vscode
.idea
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Environment
.env
.env.*
*.pem
*.key
# Docker
Dockerfile*
docker-compose*.yml
.dockerignore
# Documentation
*.md
LICENSE
docs/
# Tests
tests/
test/
__tests__
coverage/
.nyc_output---
CI/CD Cache Configuration Examples
GitHub Actions with Registry Cache
name: Build and Push
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: user/app:${{ github.sha }}
cache-from: type=registry,ref=user/app:buildcache
cache-to: type=registry,ref=user/app:buildcache,mode=maxGitHub Actions with GHA Cache
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: user/app:latest
cache-from: type=gha
cache-to: type=gha,mode=maxGitLab CI with Registry Cache
build:
image: docker:24
services:
- docker:24-dind
variables:
DOCKER_BUILDKIT: 1
script:
- docker buildx create --use
- docker buildx build
--push
--tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
--cache-from type=registry,ref=$CI_REGISTRY_IMAGE:buildcache
--cache-to type=registry,ref=$CI_REGISTRY_IMAGE:buildcache,mode=max
.Multi-Branch Cache Strategy
#!/bin/bash
BRANCH=$(git rev-parse --abbrev-ref HEAD | tr '/' '-')
docker buildx build \
--push \
--tag registry/app:${BRANCH}-${GITHUB_SHA:0:7} \
--cache-from type=registry,ref=registry/app:cache-${BRANCH} \
--cache-from type=registry,ref=registry/app:cache-main \
--cache-to type=registry,ref=registry/app:cache-${BRANCH},mode=max \
.ALWAYS include cache-main as a fallback source -- new branches immediately benefit from the main branch cache.