🎉 DevOps Interview Prep Bundle is live — 1000+ Q&A across 20 topicsGet it →
All Articles

Docker Build Slow and Not Using Cache: 5 Fixes

Docker builds ignoring cache and rebuilding from scratch every time? These 5 fixes — Dockerfile layer ordering, BuildKit cache mounts, .dockerignore issues, and multi-stage optimizations — cut build times by 60-80%.

Shubham4 min read
Share:Tweet

Docker cache invalidation is subtle. One wrong layer order, one missing .dockerignore entry, or one timestamp issue makes every build start from scratch. Here are the five most common causes and how to fix each.

Why Docker Cache Breaks

Docker cache is sequential and layer-based. When any layer changes, all subsequent layers are invalidated and rebuilt. The key insight: order matters more than content.

Fix 1: Wrong Layer Order (Most Common)

The problem:

dockerfile
# BAD — copies entire source before installing dependencies
FROM node:20-alpine
WORKDIR /app
COPY . .                          # Invalidated by ANY file change
RUN npm install                   # Reinstalls all packages every time

Every time any file changes (including source code, README, .env), the COPY . . layer invalidates the npm install cache.

The fix:

dockerfile
# GOOD — install dependencies first, copy source last
FROM node:20-alpine
WORKDIR /app
 
# These layers only rebuild when package.json changes
COPY package.json package-lock.json ./
RUN npm ci --only=production
 
# This layer rebuilds on source changes, but npm install stays cached
COPY . .

Python version:

dockerfile
FROM python:3.12-slim
WORKDIR /app
 
# Copy requirements first — cached until requirements.txt changes
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
 
# Copy source last
COPY . .

Go version:

dockerfile
FROM golang:1.22-alpine
WORKDIR /app
 
COPY go.mod go.sum ./
RUN go mod download          # Cached until go.mod/go.sum changes
 
COPY . .
RUN go build -o /app/server ./cmd/server

Fix 2: Missing or Wrong .dockerignore

If .dockerignore is missing or incomplete, COPY . . includes files that change frequently (logs, node_modules, .git), constantly invalidating the cache.

Create .dockerignore:

# Version control
.git
.gitignore

# Dependencies (don't copy — rebuild from package files)
node_modules/
vendor/
__pycache__/
*.pyc
.venv/

# Build artifacts
dist/
build/
*.o
*.so

# Environment and secrets
.env
.env.*
*.pem
*.key

# IDE and OS files
.vscode/
.idea/
*.DS_Store
Thumbs.db

# Test files (not needed in production image)
**/*_test.go
**/test/
**/tests/
coverage.out

# Docs
README.md
CHANGELOG.md
docs/

Verify your .dockerignore is working:

bash
# Build with DOCKER_BUILDKIT=1 and check what's being sent
DOCKER_BUILDKIT=1 docker build . --progress=plain 2>&1 | head -20
# Look for "transferring context" — the size should be small (not hundreds of MB)

Fix 3: Enable BuildKit and Use Cache Mounts

BuildKit (the modern Docker build engine) provides cache mounts that persist across builds — even on CI.

bash
# Enable BuildKit
export DOCKER_BUILDKIT=1
 
# Or use docker buildx (always uses BuildKit)
docker buildx build .

Cache mounts in Dockerfile:

dockerfile
# syntax=docker/dockerfile:1
FROM python:3.12-slim
 
WORKDIR /app
COPY requirements.txt .
 
# --mount=type=cache persists pip cache between builds
# Even on CI with a fresh container, cache is preserved on the host
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt
 
COPY . .

For apt packages:

dockerfile
RUN --mount=type=cache,target=/var/cache/apt \
    apt-get update && apt-get install -y curl wget

For npm:

dockerfile
RUN --mount=type=cache,target=/root/.npm \
    npm ci --prefer-offline

Cache mounts are especially powerful in CI — add them to your GitHub Actions runner and package installs become nearly instant after the first run.

Fix 4: Multi-Stage Build for Faster Final Images

Multi-stage builds keep your final image small and avoid rebuilding heavyweight build tools:

dockerfile
# syntax=docker/dockerfile:1
 
# Stage 1: Build
FROM golang:1.22-alpine AS builder
WORKDIR /build
 
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
    go mod download
 
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build \
    CGO_ENABLED=0 go build -ldflags="-w -s" -o server ./cmd/server
 
# Stage 2: Final image — only the binary
FROM gcr.io/distroless/static-debian12
COPY --from=builder /build/server /server
ENTRYPOINT ["/server"]

Result: build image is 500MB+, final image is 10-20MB. CI downloads the small final image.

Fix 5: Cache in CI (GitHub Actions)

GitHub Actions throws away everything after each run. Without explicit caching, Docker layer cache is lost.

yaml
name: Build and Push
 
on:
  push:
    branches: [main]
 
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
 
      - name: Login to ECR
        uses: aws-actions/amazon-ecr-login@v2
 
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: 123456789.dkr.ecr.ap-south-1.amazonaws.com/myapp:${{ github.sha }}
          # Cache from/to GitHub Actions cache
          cache-from: type=gha
          cache-to: type=gha,mode=max

type=gha uses GitHub's Actions cache storage. mode=max caches all layers including intermediate build stages. First run is slow; subsequent runs use cache.

Alternative: cache in ECR:

yaml
cache-from: type=registry,ref=123456789.dkr.ecr.ap-south-1.amazonaws.com/myapp:cache
cache-to: type=registry,ref=123456789.dkr.ecr.ap-south-1.amazonaws.com/myapp:cache,mode=max

Quick Diagnosis

bash
# Check if cache is being used
docker build . --progress=plain 2>&1 | grep -E "CACHED|RUN"
# Lines starting with "CACHED" are cache hits
# Lines starting with "RUN" are cache misses
 
# Check context size being sent to Docker daemon
docker build . 2>&1 | grep "Sending build context"
# Should be KB, not MB
 
# Check layer history
docker history myapp:latest --no-trunc
 
# Force rebuild without cache (to establish baseline)
docker build --no-cache .

Summary

ProblemFix
Dependencies reinstall every buildMove COPY package.json before COPY . .
Large context sent to daemonAdd .dockerignore
Slow package installsUse --mount=type=cache
Big final imagesMulti-stage builds
CI cache lost between runsAdd cache-from/cache-to in docker/build-push-action

Most Docker build speed problems are solved by fixing layer ordering and adding .dockerignore. The rest (cache mounts, CI caching) are optimizations that multiply on top.


More Docker optimization? Read our Docker multi-stage build patterns and Docker security scanning with Trivy.

🔧

Today I Fixed

Short real fixes from production — posted daily

Browse fixes
Newsletter

Stay ahead of the curve

Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam — just practical engineering content.

Related Articles

Comments