GitHub Actions Docker Cache: gha vs registry vs inline
April 17, 2026
Perbandingan Docker cache GitHub Actions: gha, registry, dan inline. Cek benchmark, trade-off, dan rekomendasi strategi optimasi build.
Salah satu keluhan paling umum soal CI/CD pipeline adalah: build Docker-nya lama. Dan salah satu cara paling efektif untuk mengatasinya adalah caching yang tepat.
GitHub Actions + BuildKit punya tiga strategi cache yang sering dipakai: type=gha, type=registry, dan type=inline. Ketiganya tersedia, ketiganya punya trade-off, dan pilihan yang salah bisa bikin build tetap lambat — atau malah lebih lambat dari sebelumnya.
Post ini dokumentasi eksplorasi dan benchmark yang aku lakukan di pipeline internal. Hasilnya mungkin tidak akan persis sama di environment kamu, tapi polanya cukup konsisten.
Setup yang Digunakan
Sebelum masuk ke perbandingan, ini konteks setup yang dipakai:
- Runner: GitHub-hosted
ubuntu-latest - Image: Multi-stage build — base Node.js (dependencies) → build stage → production Nginx
- Ukuran image final: ~85 MB
- Frekuensi push: ~15–20x per hari (aktif development)
- BuildKit: enabled via
docker/setup-buildx-action
Setup dasar workflow-nya seperti ini:
yamlCopy- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ env.IMAGE_TAG }}
cache-from: # <- ini yang akan berubah per strategi
cache-to: # <- ini juga
Tiga Strategi Cache
1. type=gha — GitHub Actions Cache
Cache disimpan di GitHub Actions Cache storage (bukan registry). Terintegrasi langsung dengan ekosistem GitHub.
yamlCopycache-from: type=gha
cache-to: type=gha,mode=max
Cara kerjanya: BuildKit menyimpan cache layers ke GitHub Actions Cache API. Cache ini scoped per branch dan per workflow, mirip cara kerja actions/cache.
Kelebihan:
- Setup paling simpel — zero config tambahan
- Tidak butuh credentials registry
- Cache otomatis expired sesuai retention policy GitHub (7 hari untuk cache yang tidak diakses)
- Gratis selama masih dalam quota storage GitHub Actions
Kekurangan:
- Cache storage limit: 10 GB per repository (shared dengan semua workflow lain)
- Tidak bisa di-share antar repository
- Cache miss total kalau runner baru atau branch baru tanpa history
- Upload/download speed bergantung pada GitHub infrastructure
2. type=registry — Registry Cache
Cache disimpan sebagai layer di container registry (Docker Hub, ECR, GCR, dll.) — terpisah dari image production.
yamlCopycache-from: type=registry,ref=xxxx.dkr.ecr.ap-southeast-1.amazonaws.com/myapp:cache
cache-to: type=registry,ref=xxxx.dkr.ecr.ap-southeast-1.amazonaws.com/myapp:cache,mode=max
Cara kerjanya: BuildKit push cache layers ke tag khusus di registry (biasanya :cache atau :buildcache). Setiap build pull layer yang sudah ada, dan push layer baru yang berubah.
Kelebihan:
- Persistent — tidak ada expiry seperti GitHub Cache
- Bisa di-share antar repository dan antar workflow
- Cache tersedia selama registry masih aktif
- Cocok untuk monorepo atau multiple pipeline yang share base image
Kekurangan:
- Butuh registry credentials (setup lebih banyak)
- Kalau pakai ECR: ada biaya storage dan transfer
- Cold start lambat — pull cache dari registry lebih lambat dari GitHub Cache
- Perlu manajemen manual kalau cache tumbuh besar
3. type=inline — Inline Cache
Cache metadata disimpan di dalam image itu sendiri, bukan di tempat terpisah.
yamlCopycache-from: type=registry,ref=xxxx.dkr.ecr.ap-southeast-1.amazonaws.com/myapp:latest
cache-to: type=inline
Cara kerjanya: Cache disimpan sebagai metadata di dalam image yang di-push. Saat build berikutnya, BuildKit pull image latest dan pakai metadata itu sebagai cache hint.
Kelebihan:
- Tidak butuh storage tambahan — cache ada di dalam image
- Setup paling sederhana dari sisi infrastructure
- Tidak ada overhead pull cache terpisah
Kekurangan:
- Hanya support
mode=min— artinya hanya layer final yang di-cache, bukan intermediate stages - Untuk multi-stage build, ini masalah besar — stage dependencies dan builder layers tidak ikut ter-cache
- Efektivitasnya jauh di bawah dua metode lain untuk build yang kompleks
Benchmark
Aku jalankan 30 builds per strategi selama 3 hari kerja, dengan variasi:
- Cache hit: hanya perubahan di application code (bukan dependencies)
- Cache miss: perubahan di
package.jsonatau base image update - Cold start: branch baru, tidak ada cache sama sekali
Build Time (detik)
| Kondisi | Tanpa Cache | inline |
gha |
registry |
|---|---|---|---|---|
| Cold start | 187s | 183s | 181s | 178s |
| Cache miss (deps berubah) | 187s | 179s | 142s | 138s |
| Cache hit (code only) | 187s | 164s | 48s | 52s |
| Cache hit rata-rata | 187s | 158s | 51s | 55s |
Catatan: Angka di atas adalah rata-rata dari 30 run. Variasi antar run bisa ±10–15 detik tergantung load GitHub runner.
Penghematan Waktu per Hari
Dengan ~18 build/hari dan mayoritas adalah cache hit (code-only changes):
| Strategi | Build time/hari | Hemat vs tanpa cache |
|---|---|---|
| Tanpa cache | ~56 menit | — |
inline |
~47 menit | ~9 menit |
gha |
~15 menit | ~41 menit |
registry |
~17 menit | ~39 menit |
Analisis: Kenapa inline Kalah Jauh?
Ini yang paling menarik dari benchmark ini. Untuk multi-stage build, inline hampir tidak memberikan manfaat signifikan di cache hit scenario.
Penyebabnya: inline hanya menyimpan cache untuk final stage. Sedangkan di multi-stage build, stage yang paling “berat” biasanya ada di tengah — install dependencies, compile, dll.
Ilustrasinya:
dockerfileCopy# Stage 1: deps — paling berat, ~90s
FROM node:20-alpine AS deps
COPY package*.json ./
RUN npm ci # <-- inline cache TIDAK cover ini secara efektif
# Stage 2: build — medium, ~40s
FROM deps AS builder
COPY . .
RUN npm run build
# Stage 3: production — ringan, ~5s
FROM nginx:alpine AS runner # <-- inline cache hanya cover sampai sini
COPY --from=builder /app/dist /usr/share/nginx/html
Dengan gha atau registry dan mode=max, semua stage ter-cache. Jadi kalau hanya kode yang berubah (bukan package.json), stage deps di-skip sepenuhnya.
gha vs registry: Mana yang Lebih Worth?
Dari benchmark, selisihnya tipis — gha sedikit lebih cepat di cache hit karena GitHub Cache biasanya lebih dekat dengan runner. Tapi keputusannya bukan soal speed semata.
| Faktor | gha |
registry |
|---|---|---|
| Setup complexity | Rendah | Medium |
| Persistensi cache | 7 hari (tidak diakses) | Permanent |
| Share antar repo | ❌ | ✅ |
| Biaya | Gratis (dalam quota) | Storage + transfer |
| Cocok untuk | Single repo, tim kecil | Monorepo, shared base image |
| Cache size limit | 10 GB/repo | Tergantung registry plan |
| Cold start performance | ≈ sama | ≈ sama |
Kesimpulan praktis:
- Kalau repository independent dan tidak ada shared base image →
ghasudah cukup - Kalau ada multiple pipeline yang share layer, atau butuh cache yang tidak expired →
registry inlinehanya masuk akal untuk single-stage build yang sangat sederhana
Konfigurasi Lengkap yang Aku Pakai
Ini final config yang aku pakai setelah benchmarking, menggunakan gha dengan mode=max:
yamlCopyname: Build and Push
on:
push:
branches: [main, develop]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-southeast-1
- 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: |
${{ secrets.ECR_REGISTRY }}/${{ env.APP_NAME }}:${{ github.sha }}
${{ secrets.ECR_REGISTRY }}/${{ env.APP_NAME }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
NODE_ENV=production
Satu hal yang perlu diperhatikan: kalau kamu punya banyak workflow di repo yang sama, 10 GB cache limit bisa cepat penuh. Pantau usage-nya di Settings → Actions → Caches.
Tips Tambahan
Gunakan mode=max selalu untuk multi-stage build.
mode=min (default) hanya cache output final stage. Untuk multi-stage, ini sama buruknya dengan inline.
Pisahkan package.json COPY dari source code COPY.
Ini bukan tips cache type, tapi paling impactful:
dockerfileCopy# ✅ Benar — dependencies di-cache terpisah
COPY package*.json ./
RUN npm ci
COPY . . # perubahan di sini tidak invalidate layer npm ci
# ❌ Salah — setiap perubahan file apapun invalidate npm ci
COPY . .
RUN npm ci
Jangan cache layer yang mengandung secrets.
Kalau ada ARG atau ENV yang berisi credentials di tengah build, layer itu sebaiknya tidak masuk cache. Letakkan credentials-related instructions di akhir, setelah semua layer yang cacheable.
Penutup
Tidak ada strategi cache yang universally terbaik — semuanya bergantung pada struktur build dan workflow. Tapi dari pengalaman ini, urutan rekomendasi untuk kebanyakan use case adalah:
ghadenganmode=max— paling mudah, efektif untuk single-repo pipelineregistrydenganmode=max— kalau butuh persistensi atau sharing antar repoinline— hindari untuk multi-stage build
Yang paling penting: apapun strateginya, pastikan Dockerfile-mu sudah dioptimasi untuk layer ordering. Cache terbaik tidak akan banyak membantu kalau layer ordering salah.
Referensi:
That’s it.
