Runner GitHub Actions bersifat ephemeral: setiap job berjalan di virtual machine bersih tanpa riwayat layer Docker lokal dari build sebelumnya. Tanpa konfigurasi cache terdistribusi, proses build image container akan mengeksekusi ulang seluruh instruksi dari awal pada setiap commit, membuang waktu pipeline CI dan bandwidth jaringan.

Solusi standar industri untuk masalah ini adalah memanfaatkan backend cache bawaan Docker BuildKit (type=gha) yang terintegrasi langsung dengan GitHub Actions Cache API, dikombinasikan dengan RUN --mount=type=cache pada level Dockerfile.

Konfigurasi GitHub Actions Workflow dengan Backend type=gha

Ekosistem resmi Docker menyediakan action docker/setup-buildx-action untuk mengaktifkan builder BuildKit dan docker/build-push-action untuk mengeksekusi build dengan integrasi cache backend type=gha.

Gunakan konfigurasi workflow berikut pada .github/workflows/docker-build.yml:

name: Docker Build and Cache

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build and Push Docker Image
        uses: docker/build-push-action@v6
        with:
          context: .
          push: false
          tags: app:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

Instruksi cache-from: type=gha menginstruksikan BuildKit untuk mengunduh layer cache dari GitHub Actions Cache service. Sementara itu, cache-to: type=gha,mode=max mengekspor layer hasil build kembali ke cache storage GitHub Actions.

Perbedaan cache-to mode=min vs mode=max

Parameter mode pada cache-to menentukan layer mana saja yang diekspor ke remote backend:

  • mode=min (default): Hanya menyimpan layer milik image target akhir (final stage). Layer perantara (intermediate stages) pada multi-stage build akan dibuang. Mode ini menghasilkan ukuran payload cache lebih kecil, tetapi memaksa intermediate build stage (misalnya stage builder atau test) dieksekusi ulang di run berikutnya.
  • mode=max: Menyimpan seluruh layer dari semua stage, termasuk intermediate stages yang tidak masuk ke image akhir.

Rekomendasi pemilihan: Jika Dockerfile menggunakan multi-stage build (seperti mengompilasi TypeScript, aset frontend, atau binary Go sebelum menyalin ke image minimal), gunakan mode=max. Jika Dockerfile hanya terdiri dari single-stage sederhana tanpa dependency compilation, gunakan mode=min untuk menghemat alokasi kuota cache.

Akselerasi Package Manager dengan RUN --mount=type=cache

Lapisan cache layer Docker biasa bekerja berdasarkan checksum file. Jika file package.json atau requirements.txt berubah satu karakter saja, seluruh layer RUN npm ci atau RUN pip install akan terkena cache invalidation, memaksa download ulang ribuan dependency dari internet registry.

BuildKit mengatasi keterbatasan ini melalui fitur Cache Mounts (--mount=type=cache). Direktori cache package manager dipertahankan di luar siklus invalidasi layer Docker reguler.

Berikut contoh implementasi multi-stage build Node.js deterministik:

# syntax=docker/dockerfile:1
FROM node:20-alpine AS base
WORKDIR /app

FROM base AS dependencies
COPY package.json package-lock.json ./
# Mount cache npm global directory agar tarball tidak diunduh ulang saat lockfile berubah
RUN --mount=type=cache,target=/root/.npm \
    npm ci --prefer-offline --no-audit

FROM dependencies AS builder
COPY . .
RUN npm run build

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci --omit=dev --prefer-offline --no-audit
COPY --from=builder /app/dist ./dist

USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]
Catatan: Syntax parser directive # syntax=docker/dockerfile:1 di baris pertama Dockerfile wajib disertakan untuk memastikan Docker daemon menggunakan parser BuildKit versi terbaru yang mendukung sintaks --mount.

Untuk package manager lain, sesuaikan path target:

  • Python (pip): RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt
  • Go: RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build go build -o app
  • Rust (Cargo): RUN --mount=type=cache,target=/usr/local/cargo/registry --mount=type=cache,target=/app/target cargo build --release

Prinsip Layer Ordering Deterministik

BuildKit mengevaluasi invalidasi layer dari atas ke bawah. Sekali sebuah layer terinvalasi, semua layer di bawahnya otomatis diabaikan dari cache. Untuk memaksimalkan cache hits:

  1. Letakkan instruksi dengan frekuensi perubahan terendah di posisi paling atas: Dependensi sistem operasi (apt-get / apk) harus berada sebelum instalasi dependensi bahasa (npm / pip).
  2. Pisahkan dependensi manifes dari source code: Jangan lakukan COPY . . sebelum instalasi dependensi. Selalu salin file definisi (misal package.json) secara spesifik lebih dahulu.
  3. Gunakan versi paket yang terkunci (pinned versions): Hindari instruksi non-deterministik seperti RUN apt-get update && apt-get install -y curl tanpa versi tetap, atau script eksternal via curl | sh yang isinya bisa berubah sewaktu-waktu.

Batas Kuota 10 GB, Scoping Branch, dan Mitigasi Cache Miss

GitHub Actions memberlakukan batas kuota cache sebesar 10 GB per repositori. Jika ukuran kumulatif cache melebihi batas ini, GitHub akan menghapus cache lama menggunakan kebijakan Least Recently Used (LRU).

1. Isolasi dan Scoping Cache Antar-Branch

Secara default, GitHub Actions memberlakukan aturan isolasi cache berikut:

  • Job pada branch main/master dapat membaca dan menulis cache branch main.
  • Job pada Pull Request (PR) dapat membaca cache dari base branch (misal: main) dan branch PR itu sendiri, tetapi tidak dapat menimpa cache milik main.

Untuk menghindari tabrakan cache antar-branch fitur yang aktif secara bersamaan, gunakan parameter scope secara eksplisit:

- name: Build and Push Docker Image
  uses: docker/build-push-action@v6
  with:
    context: .
    push: false
    tags: app:latest
    cache-from: |
      type=gha,scope=${{ github.workflow }}-${{ github.ref_name }}
      type=gha,scope=${{ github.workflow }}-main
    cache-to: type=gha,mode=max,scope=${{ github.workflow }}-${{ github.ref_name }}

Konfigurasi di atas mencoba mengambil cache dari branch saat ini terlebih dahulu. Jika terjadi cache miss (seperti pada PR baru), BuildKit akan mengambil cache fallback dari branch main.

2. Mitigasi Pertumbuhan Cache yang Melebihi Kuota

Jika repository memiliki image berukuran besar, mode cache-to: type=gha,mode=max pada setiap branch akan dengan cepat memicu batas 10 GB, menyebabkan cache branch main terhapus oleh PR aktif. Terapkan strategi berikut:

  • Hanya tulis cache (cache-to) pada branch utama (main), sedangkan PR hanya diizinkan membaca (cache-from):
cache-from: type=gha,scope=build-main
cache-to: ${{ github.ref == 'refs/heads/main' && 'type=gha,mode=max,scope=build-main' || '' }}

Strategi ini menjaga agar alokasi 10 GB difokuskan untuk branch utama sebagai acuan baseline seluruh PR, mencegah cache evictions prematur, dan menjaga build pipeline tetap konsisten di bawah 2 menit.