SuperAPI AI Directory

docker-impl-build-optimization

Deployment & operations

View repository

Updated 10-15 22:18

About this skill

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 below

Principle: 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 git

ALWAYS 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 ci

yarn

COPY package.json yarn.lock ./
RUN --mount=type=cache,target=/usr/local/share/.cache/yarn \
    yarn install --frozen-lockfile

pnpm

COPY package.json pnpm-lock.yaml ./
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
    pnpm install --frozen-lockfile

pip (Python)

COPY requirements.txt ./
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

Go 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 ./cmd

Cargo (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 --release

Maven (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 -DskipTests

NuGet (.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 ./cmd

Advantages:

  • 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=max

Multi-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

Official Sources