← Back to Blog

GitHub Actions Docker Build: 12min → 2min with Layer Caching

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 →