Skip to main content
DevOps dari nol · Pelajaran 9 dari 15 · 13 menit

Percepat Build Docker di GitHub Actions: Panduan Cache

Percepat build Docker di GitHub Actions: ekspor layer dengan cache-to type=gha, tahu kapan cache mount gagal, dan pulihkan cache tool lewat actions/cache.

Oleh Matthews WongDitinjau pada

Poin utama

Build Docker di GitHub Actions lambat secara default karena setiap job dimulai di virtual machine baru tanpa cache apa pun. Anda mempercepatnya dengan mengekspor cache layer lewat cache-to type=gha dan mengimpornya dengan cache-from, mengurutkan Dockerfile agar langkah lambat tidak berubah, serta memulihkan cache tool seperti milik Next.js dengan actions/cache.

Setelah pelajaran ini, Anda bisa

  • Menjelaskan kenapa runner baru membangun ulang semuanya, dan cache mana yang memperbaiki bagian mana.
  • Mengatur cache-from dan cache-to type=gha, serta menjelaskan kenapa multi-stage build butuh mode=max.
  • Menulis key actions/cache yang terus menyimpan, serta memahami batas ukuran, usia, dan branch.
Lihat cara kerjanya

Ikuti diagramnya langkah demi langkah

Tekan Putar, atau melangkah dengan panah. Setiap langkah juga ditulis di bawah diagram.

Cara runner dingin mendapatkan cache-nya kembali

Runner baru mulai kosong. cache-from memulihkan layer, perubahan source hanya menjalankan ulang layer sesudahnya, actions/cache memulihkan cache kompilasi Next, dan cache-to menyimpan semuanya kembali ke penyimpanan GitHub.

Langkah 1 dari 7. Di atas, satu job build di runner: dua layer, next build, dan folder cache milik Next. Di bawah, penyimpanan cache GitHub, satu-satunya yang bertahan setelah runner hilang.

Tombol: ← → langkah · Home / End · K putar atau jeda

  1. Di atas, satu job build di runner: dua layer, next build, dan folder cache milik Next. Di bawah, penyimpanan cache GitHub, satu-satunya yang bertahan setelah runner hilang.

  2. Setiap job hosted berjalan di virtual machine baru. Tidak ada sisa build sebelumnya di disknya, jadi setiap layer dan setiap halaman akan dibangun dari nol.

    Tebak langkah berikutnya Apa yang mengembalikan layer npm ci ke runner yang kosong? (cache-from: type=gha · git checkout · Docker menyimpannya sendiri)
  3. cache-from mengimpor layer tersimpan dari penyimpanan GitHub. Layer npm ci dipakai ulang, bukan dibangun lagi, karena inputnya tidak berubah.

    cache-from: type=gha
  4. COPY . . melihat file berubah, jadi ia berjalan lagi, begitu juga setiap layer sesudahnya. Layer npm ci di atasnya tetap dipakai ulang.

    Tebak langkah berikutnya next build harus berjalan lagi. Apakah ia harus mengompilasi semua halaman dari nol? (Ya, selalu · Tidak, cache Next yang dipulihkan membuatnya melanjutkan)
  5. Layer tidak bisa menyimpan file yang berada di luarnya. actions/cache memulihkan .next/cache ke runner, dan build context membawanya masuk ke next build.

    uses: actions/cache@v4 path: .next/cache
  6. Dengan mode=max, cache-to mengekspor setiap layer, bukan hanya yang ada di image akhir. Job yang sukses juga menyimpan entri actions/cache baru.

    cache-to: type=gha,mode=max
  7. GitHub menyimpan 10 GB per repository secara default dan menghapus entri yang paling lama tidak dipakai lebih dulu, serta yang tidak dipakai lebih dari 7 hari.

Kenapa build Docker lambat di runner GitHub Actions yang baru?

Di laptop, Docker menyimpan layer dari build terakhir, jadi rebuild melewati setiap langkah yang inputnya tidak berubah. Runner hosted GitHub tidak punya kemewahan itu: dokumentasi GitHub menggambarkan setiap runner hosted sebagai virtual machine baru. Job selesai, mesinnya hilang, dan job berikutnya mulai dengan disk kosong dan cache layer kosong.

Baca selengkapnya (2)

Jadi cache harus disimpan di tempat yang bertahan setelah runner hilang, dan Docker menyebut empat tempat: inline (di dalam image), registry (image terpisah), local (sebuah folder), dan gha (layanan cache milik GitHub). Di dalam workflow GitHub Actions, dokumentasi Docker menyebut gha sebagai pilihan yang direkomendasikan, dan itulah yang dipakai situs ini.

Apa pun penyimpanannya, aturan pemakaian ulang datang dari Docker, dan aturannya ketat. RUN dicocokkan lewat string perintahnya. COPY dan ADD dicocokkan lewat checksum metadata file, bukan waktu modifikasi. Dan begitu satu layer tidak valid, setiap instruksi sesudahnya berjalan lagi, walaupun inputnya sendiri sama. Itu sebabnya Dockerfile menyalin package.json dan lockfile lebih dulu, menjalankan npm ci, baru menyalin sisa source.

Bagaimana cache-from dan cache-to type=gha bekerja?

cache-from mengimpor cache di awal build dan cache-to mengekspornya di akhir. Dengan docker/build-push-action, dua baris itu saja yang Anda tulis, dan URL serta token yang dibutuhkan backend gha diisi otomatis. Itu juga sebabnya backend ini hanya berfungsi di dalam workflow GitHub Actions: nilai-nilai itu tidak ada di tempat lain.

Baca selengkapnya (2)

Parameter yang paling penting adalah mode, dan hanya berlaku untuk cache-to. Pada mode min, yang menjadi default, hanya layer yang diekspor ke image hasil yang di-cache. Pada mode max, semua layer di-cache. Stage runner di situs ini hanya menyalin hasil build, jadi stage deps dan builder tidak pernah masuk ke image yang dikirim, dan pada mode min layer mereka, termasuk npm ci dan next build, tidak akan pernah tersimpan. mode=max yang membuatnya bisa dipakai ulang.

Dua parameter lain perlu diketahui. scope menamai objek cache dan default-nya buildkit, dan build yang berbagi scope saling menimpa cache, jadi beri setiap image scope sendiri jika satu repository membangun beberapa image. Dan akses mengikuti branch: workflow hanya bisa membaca cache dari branch saat ini, branch dasar, dan branch default.

Cache mana untuk masalah apa: layer, cache mount, atau actions/cache?

Cache layer membuat seluruh instruksi bisa dilewati ketika inputnya tidak berubah. Cache mount, RUN --mount=type=cache, berbeda: Docker menggambarkannya sebagai lokasi cache persisten yang dipakai selama build, kumulatif antar build, sehingga ketika sebuah layer harus berjalan lagi, npm hanya mengunduh paket baru atau yang berubah. Halaman CI milik Docker menambahkan syaratnya: BuildKit tidak menyimpan cache mount di cache GitHub Actions secara default. Di runner dingin, jangan mengandalkan mount itu sudah terisi.

Baca selengkapnya (2)

Celah itu alasan situs ini memakai cache ketiga untuk satu hal yang tidak bisa dipegang layer. next build hanya inkremental bila folder .next/cache dari build sebelumnya ada, jadi workflow memulihkannya dengan actions/cache dan satu baris negasi di .dockerignore membuat folder itu ikut masuk ke build context. Komentar di workflow dan di .dockerignore mencatat bahwa cache mount adalah alternatif yang jelas tetapi tidak berfungsi di sini, persis karena alasan di atas.

actions/cache mencocokkan dengan urutan tetap: key yang persis lebih dulu, lalu setiap entri restore-keys secara berurutan, dan di antara beberapa kecocokan parsial ia mengembalikan yang paling baru dibuat. Isi sebuah cache tidak bisa diubah, dan kecocokan key yang persis tidak membuat entri baru. Karena itu key di sini diakhiri SHA commit: tidak pernah cocok persis, setiap run menyimpan entri baru, dan restore-keys mundur ke cache terbaru untuk lockfile yang sama.

Seberapa besar cache boleh tumbuh, dan siapa yang boleh menulis?

Batas GitHub adalah 10 GB cache per repository secara default. Saat penuh, entri paling lama diakses dihapus lebih dulu, dan entri yang tidak diakses lebih dari 7 hari dihapus. Docker menyebut backend gha hanya dipakai dalam batas yang ditetapkan GitHub, dan API cache-nya bisa dibatasi lajunya, yang muncul sebagai error maximum timeout reached saat ekspor.

Baca selengkapnya (2)

Cache yang dipulihkan, ditambah, lalu disimpan di setiap run hanya akan terus membesar, jadi workflow ini menambah batas sendiri: di atas 1.500 MB cache Next.js tidak disimpan, dan build berikutnya mulai dingin sekali lalu kecil lagi. Cache juga hanya disimpan bila job selesai dengan sukses, jadi build yang gagal tidak mencemari cache dengan hasil setengah jadi.

Menulis adalah bagian yang sensitif. Dokumentasi Docker memperingatkan bahwa beberapa event hanya mendapat akses cache read-only, sehingga ekspor gagal dengan failed to reserve cache, dan bahwa memberi akses tulis ke workflow yang tidak tepercaya menaikkan risiko cache poisoning. Bentuk yang aman: pertahankan cache-from, hapus cache-to untuk event tersebut, dan biarkan push ke branch default yang mengisi cache.

Cara situs ini melakukannya

Konfigurasi nyata di balik pelajaran ini

Kutipan dari repositori yang membangun dan men-deploy situs ini, apa adanya.

Dockerfile:4-11
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
# Cache npm's download directory, not node_modules. This survives within a build
# even when the layer above is invalidated, so a lockfile change re-links from
# cache instead of re-downloading every tarball. Still `npm ci`, so the lockfile
# stays authoritative.
RUN --mount=type=cache,target=/root/.npm npm ci

Urutan mengerjakan separuh pertama: file package, lalu npm ci, dengan source lengkap disalin belakangan di stage builder. Cache mount menunjuk ke folder unduhan npm, jadi ketika perubahan lockfile memaksa npm ci berjalan lagi, paket yang sudah ada di mount tidak diunduh ulang. Di runner dingin mount itu mulai kosong, karena itu ia bonus, bukan rencana utama.

.github/workflows/deploy.yml:120-128
      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: |
            ${{ steps.meta.outputs.ref }}:latest
            ${{ steps.meta.outputs.ref }}:${{ github.sha }}
          cache-from: type=gha

Seluruh setelan cache layer ada di dua baris terakhir. cache-from mengimpor, cache-to mengekspor, dan mode=max ada karena stage deps dan builder tidak masuk ke image yang dikirim. Tag di atasnya tidak terkait cache: cache hit mengubah seberapa cepat image dibangun, bukan isinya.

.github/workflows/deploy.yml:87-97
      - name: Restore Next.js build cache
        uses: actions/cache@v4
        with:
          path: .next/cache
          # SHA in the key so every run writes a fresh entry; restore-keys then
          # falls back to the most recent cache for the same lockfile, and
          # finally to any cache at all, rather than starting cold.
          key: next-cache-${{ hashFiles('package-lock.json') }}-${{ github.sha }}
          restore-keys: |
            next-cache-${{ hashFiles('package-lock.json') }}-
            next-cache-

Key yang berakhir dengan SHA commit selalu baru di setiap run, jadi step setelah job selalu punya sesuatu untuk disimpan. restore-keys adalah prefix yang dicoba berurutan: cache terbaru untuk lockfile yang persis sama menang, dan cache Next.js apa pun tetap lebih baik daripada mulai dingin.

.dockerignore:20-21
.next
!.next/cache

Dua baris ini membawa banyak makna. .next dikeluarkan dari build context, lalu negasi mengembalikan hanya .next/cache. Aturan Docker: baris terakhir yang cocok yang menentukan, jadi pengecualian harus ditulis setelah aturan pengecualian yang ditimpanya.

.github/workflows/deploy.yml:175-183
          # The cache is restored, grown and saved again on every run, so
          # nothing ever shrinks it. Past the cap, save nothing: the next
          # build starts cold once, and the cache starts small again instead
          # of growing until the runner's disk runs out.
          size_mb="$(du -sm .next/cache | cut -f1)"
          if [ "$size_mb" -gt "$NEXT_CACHE_CAP_MB" ]; then
            echo "::warning title=Next.js cache over cap::.next/cache is ${size_mb} MB (cap ${NEXT_CACHE_CAP_MB} MB), so this run saves none and the next build starts cold."
            rm -rf .next/cache
          fi

Batas atas untuk cache Next.js. Melewati batas, step menghapus foldernya, sehingga actions/cache melewati penyimpanan dengan peringatan dan build berikutnya mulai dingin sekali. Cache kecil yang sesekali dingin lebih baik daripada cache yang tumbuh sampai disk runner penuh.

Yang sering rusak

Kesalahan yang benar-benar membuang waktu

  • mode=min pada multi-stage build

    cache-to dengan mode default hanya menyimpan layer yang masuk ke image akhir. Pada multi-stage build, layer yang mahal, instalasi dan kompilasi, berada di stage yang tidak pernah dikirim, sehingga dibangun ulang di setiap run sementara log menunjukkan cache sedang ditulis. Setel mode=max.

  • Cache mount yang Anda kira akan bertahan

    BuildKit tidak menyimpan cache mount di cache GitHub Actions secara default, jadi mount yang cepat di laptop mulai kosong di runner baru. Pulihkan folder yang Anda butuhkan dengan actions/cache, seperti yang dilakukan situs ini untuk .next/cache, atau pakai action cache-dance yang disebut dokumentasi Docker.

  • Key actions/cache tetap yang tidak pernah diperbarui

    Dengan key: next-cache-v1, run pertama menyimpan, dan setiap run berikutnya mengenai key yang persis, yang tidak membuat entri baru. Cache diam-diam membeku pada isi pertamanya. Tambahkan SHA commit ke key dan tulis prefix di restore-keys agar setiap run menyimpan entri baru.

  • Ekspor cache opsional yang menahan deploy

    Pada 2026-10-03 step ekstraksi cache situs ini kehilangan layernya, menjalankan ulang npm ci dan seluruh build, lalu mati dengan EOF. Job gagal dan deploy image yang sudah ter-push dengan benar dilewati. Step yang hanya mempercepat dan berjalan setelah push diberi continue-on-error.

  • Menulis cache dari event yang tidak boleh menulis

    Beberapa event hanya mendapat akses cache read-only, dan ekspor di sana gagal dengan failed to reserve cache. Jangan memperluas akses tulis untuk memperbaikinya, karena workflow tidak tepercaya dengan akses tulis bisa meracuni cache. Pertahankan cache-from, hapus cache-to untuk event itu, dan isi cache dari push ke branch default.

Tiga cache dalam satu build, dan kegunaan masing-masing
Yang disimpanLayer, dipilih lewat modeFolder yang dipakai satu RUNFolder apa pun yang Anda tentukan
Bertahan di runner baruYa, lewat cache-from dan cache-toTidak secara defaultYa, lewat key dan restore-keys
Dideklarasikan diStep buildDockerfileWorkflow
Paling cocok untukMelewati seluruh langkah yang tidak berubahUnduhan ulang ketika langkah harus berjalan lagiCache tool di luar layer, seperti .next/cache
Coba sendiri

Lakukan di komputer Anda sendiri

Anda butuh repository GitHub berisi proyek Node kecil yang menghasilkan folder dist (proyek dari lesson multi-stage bisa dipakai). Anda akan menambah Dockerfile dan workflow, lalu membaca waktu run untuk melihat cache bekerja.

  1. Tambahkan Dockerfile dengan stage deps, stage builder, dan stage akhir yang hanya menyalin hasil build.

    # syntax=docker/dockerfile:1
    FROM node:22-alpine AS deps
    WORKDIR /app
    COPY package.json package-lock.json ./
    RUN --mount=type=cache,target=/root/.npm npm ci
    
    FROM node:22-alpine AS builder
    WORKDIR /app
    COPY --from=deps /app/node_modules ./node_modules
    COPY . .
    RUN npm run build
    
    FROM node:22-alpine
    WORKDIR /app
    COPY --from=builder /app/dist ./dist
    CMD ["node", "dist/server.js"]
  2. Tambahkan .github/workflows/build.yml. Workflow ini membangun di setiap push dan memakai cache gha dengan mode=max. push bernilai false, jadi tidak ada yang dipublikasikan.

    name: build
    on: push
    jobs:
      build:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - uses: docker/setup-buildx-action@v3
          - uses: docker/build-push-action@v6
            with:
              context: .
              push: false
              cache-from: type=gha
              cache-to: type=gha,mode=max
  3. Push sekali dan catat berapa lama step build. Lalu ubah satu baris source, push lagi, dan bandingkan. Layer instalasi seharusnya dipakai ulang, jadi run kedua lebih singkat.

  4. Ubah cache-to menjadi type=gha tanpa mode, push perubahan source, dan lihat langkah instalasi dan kompilasi berjalan lagi. Itulah jebakan mode min dari lesson ini. Kembalikan mode=max sesudahnya.

              cache-to: type=gha
Kuis singkat

Tiga pertanyaan sebelum lanjut

Pilih jawaban untuk melihat kenapa benar atau salah. Tidak ada yang disimpan atau dinilai, jadi tebak saja dengan bebas.

  1. 1.Sebuah multi-stage build mengekspor cache dengan cache-to type=gha tanpa mode. Layer npm ci dan next build tetap dibangun ulang di setiap run. Kenapa?
  2. 2.Sebuah workflow memulihkan .next/cache dengan key tetap next-cache-v1. Setelah run pertama, cache tidak pernah membaik. Apa yang terjadi?
  3. 3.Sebuah event dengan akses cache read-only gagal saat ekspor dengan failed to reserve cache. Apa perbaikan paling aman?
Pikirkan dulu

Jawab dalam hati, lalu cek

  1. 1.Kenapa build Docker yang cepat di laptop terasa lambat di runner hosted?Lihat jawabannya

    Jawaban:Setiap runner hosted adalah virtual machine baru, jadi layer dari build terakhir sudah hilang. Cache harus diekspor ke tempat yang bertahan setelah runner hilang, misalnya backend gha.

  2. 2.Kenapa workflow ini memakai actions/cache untuk .next/cache, bukan cache mount?Lihat jawabannya

    Jawaban:BuildKit tidak menyimpan cache mount di cache GitHub Actions secara default, dan cache-to mengekspor layer, bukan isi mount. actions/cache memulihkan folder itu ke runner, dan build context membawanya masuk.

  3. 3.Apakah job yang gagal menyimpan entri actions/cache-nya?Lihat jawabannya

    Jawaban:Tidak. Cache baru dibuat asalkan job selesai dengan sukses. Itu menjaga hasil setengah jadi tetap di luar cache.

Pelajari lebih dalam

Bahasan mendalam terkait di situs ini