Our Docker builds were eating 12 minutes of CI time. Here's the exact caching strategy using BuildKit and GitHub's cache backend that cut it to 2 minutes.
The Problem
Every PR was triggering a 12-minute Docker build in GitHub Actions. Developers were losing flow state waiting for CI. Monthly CI bill was climbing.
The Solution
Three changes: proper layer ordering in Dockerfile, BuildKit cache mounts, and GitHub Actions cache backend.
Step 1: Optimize Dockerfile layer order
FROM node:20-alpine AS deps
WORKDIR /app
# Copy package files FIRST (changes rarely)
COPY package.json package-lock.json ./
RUN npm ci --only=production
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
EXPOSE 3000
CMD ["npm", "start"]
Step 2: GitHub Actions workflow with cache
name: Docker Build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: false
tags: myapp:latest
cache-from: type=gha
cache-to: type=gha,mode=max
Results
| Before | After |
|---|---|
| 12 min | 2 min |
| No caching | Layer + registry cache |
| Full rebuild on every push | Only changed layers rebuild |
The mode=max flag caches ALL intermediate layers, not just the final image. This is the key setting most tutorials miss.
Dealing with a similar problem?
I offer production DevOps consulting. Let's fix it together.
Hire Me →