Build Cache & Optimization
Docker caches each layer. If a layer's instruction and its inputs haven't changed, the cached layer is reused — skipping re-execution.
Build cache = a chef who skips steps if the recipe hasn't changed. 'COPY package.json first' = handing the chef the ingredient list early. Only when ingredients change does the chef re-shop. Source code changes? Chef already has groceries cached.
Cache invalidation rules: (1) if an instruction changes, it and all subsequent layers are invalidated. (2) COPY/ADD invalidate the cache if the file contents change (based on checksum, not timestamp). (3) RUN never checks if the command's output changed — it only checks if the instruction text changed. Key pattern: COPY package.json ./; RUN npm install; COPY . . — dependency install is cached unless package.json changes, even if source code changes.
.dockerignore works like .gitignore — excludes files from the build context sent to the daemon. Critical for performance: without it, COPY . . includes node_modules, .git, build artifacts. Large build contexts slow down the initial send to the Docker daemon. BuildKit (default since Docker 23) improves cache performance with parallel stage execution, secret mounts (--mount=type=secret), and SSH agent forwarding. BUILDKIT_INLINE_CACHE=1 embeds cache metadata in the image for registry-based caching across CI runs. Cache mounts (--mount=type=cache) persist package manager caches between builds without adding to the image.
Docker's build cache is layer-by-layer and invalidates from the first changed layer downward. The key optimization: put slow, rarely-changing steps first (install dependencies) and fast, frequently-changing steps last (copy source code). Always create a .dockerignore to exclude node_modules, .git, and build artifacts from the build context. With BuildKit, use --mount=type=cache for package manager caches — npm, pip, apt — to get near-instant installs on rebuilds.
RUN apt-get update should always be combined with apt-get install in one layer. If they're separate, the update result is cached and stale — future builds skip the update but run install against old package lists, potentially installing outdated versions.