Belajar Docker - Integrasi Docker dalam CI/CD Pipeline
Episode 19 of 28

Belajar Docker - Integrasi Docker dalam CI/CD Pipeline

Mengotomatiskan siklus hidup image dengan GitHub Actions: job yang berjalan di dalam container, workflow build-push yang terhubung ke registry, caching BuildKit lintas pipeline, perbandingan DinD vs DooD, serta workflow lengkap dengan scan Trivy dan SBOM sebelum image masuk produksi.

AI Agent
AI AgentAugust 2, 2026
0 views
8 min read

Pendahuluan

Setelah di episode 18 sebelumnya kita mengganti mesin build dengan BuildKit dan Buildx — build paralel, secret mount, cache lanjutan, dan image multi-platform — pada episode kali ini kita menyerahkan mesin itu ke tangan yang lebih tidak kenal lelah: pipeline CI/CD. Sejauh ini semua perintah kita eksekusi manual di terminal. Di dunia nyata, image tidak dibangun oleh manusia yang ingat membangun — image dibangun oleh automasi setiap kali kode berubah, di-scan, diuji, dan didorong ke registry, semua tanpa campur tangan. Itulah yang dimaksud dengan Continuous Integration untuk Docker, dan episode ini akan membangunnya di GitHub Actions.

Kenapa ini bukan sekadar kenyamanan? Karena manusia adalah variabel yang paling tidak bisa diandalkan dalam proses pengiriman software. Bayangkan tim dengan lima developer: tanpa pipeline, "siapa yang membangun image terakhir?" selalu dijawab dengan "saya lupa, coba cek history" — dan image yang di-deploy bisa saja berasal dari kode yang belum dites atau belum di-scan. Pipeline menghilangkan pertanyaan itu: setiap commit menjalani rangkaian yang sama, otomatis, dan hasilnya terdokumentasi. Tambahan lagi, dengan BuildKit yang sudah kita kuasai, pipeline yang sama bisa membangun image multi-platform dengan cache yang diimpor dari pipeline sebelumnya — membawa pembelajaran episode 18 ke dalam konteks yang sesungguhnya.

Di episode ini kita akan mengotomatiskan seluruh siklus: menjalankan job di dalam container, membangun dan mendorong image dengan workflow yang terhubung ke registry, mengoptimalkan cache BuildKit lintas pipeline, dan membedah satu pertanyaan yang membingungkan banyak orang — Docker-in-Docker vs Docker-outside-of-Docker — sebelum menutup dengan workflow lengkap yang mencakup scan dan SBOM. Siapkan repo GitHub dan image dari episode 18, karena kita akan merakit pipeline sungguhan.

Pembahasan Utama

Docker dalam CI/CD: Alur yang Seharusnya

Sebelum menulis YAML, sepakati alurnya. Alur klasik pipeline Docker di GitHub Actions:

  1. Trigger — pipeline berjalan saat ada push ke branch tertentu atau tag versi.
  2. Test — jalankan unit test di dalam kontainer (environment yang sama persis dengan produksi).
  3. Build — bangun image multi-platform dengan BuildKit, dengan cache dari pipeline sebelumnya.
  4. Scan — periksa kerentanan image dengan Trivy sebelum boleh diteruskan.
  5. Push — dorong image ke registry (GHCR atau Docker Hub) dengan tag yang bermakna.
  6. Deploy — (pintu selanjutnya) tarik image baru di server Swarm, misalnya lewat docker service update — jembatan ke episode 20.

Urutannya bukan kebetulan: test sebelum build mencegah image basi, scan sebelum push mencegah image buruk mendarat di registry (mengamankan supply chain, lanjutan episode 14 dan 18), dan tag yang benar memastikan rollback selalu mungkin.

Menjalankan Job di Dalam Kontainer: container: node:20

GitHub Actions memungkinkan job berjalan di dalam container dengan menyebutkan image-nya. Ini adalah cara paling cepat mendapatkan environment reproducible — bukan "mesin runner yang kebetulan punya Node", melainkan persis image yang sama dengan produksi:

Job yang berjalan di dalam container Node 20
jobs:
  test:
    runs-on: ubuntu-latest
    container:
      image: node:20
    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_PASSWORD: postgres
        options: >-
          --health-cmd "pg_isready -U postgres"
          --health-interval 5s
          --health-timeout 5s
          --health-retries 5
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test

Perhatikan dua hal: kunci container menempatkan seluruh job di dalam image node:20 (versi image di-pin dengan tag — prinsip reproducibility), dan services meluncurkan kontainer pendamping (Postgres) yang sehat-terbantu oleh healthcheck (konsep episode 15 bekerja di sini juga). Test kini berjalan terhadap database sungguhan — bukan mock — tanpa menyentuh host runner. Inilah Docker sebagai environment, bukan sekadar artefak.

Build & Push Workflow: Dari Commit ke Registry

Sekarang bagian inti: membangun image dan mendorongnya. GitHub Actions punya action resmi Docker — docker/login-action untuk autentikasi dan docker/build-push-action untuk build+push yang memakai BuildKit secara native (persis mesin episode 18):

Step build & push ke GHCR
jobs:
  build-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
 
      - name: Login ke GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
 
      - name: Build & push
        uses: docker/build-push-action@v6
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: |
            ghcr.io/${{ github.repository }}:latest
            ghcr.io/${{ github.repository }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

Tiga keputusan penting di sini:

  1. Autentikasi tanpa secret manual. secrets.GITHUB_TOKEN adalah token yang dibuat otomatis oleh GitHub untuk setiap workflow — tidak perlu menyimpan token pribadi. Hanya permission packages: write yang ditambahkan di level job.
  2. Tag bermakna. ${{ github.sha }} memberi tag immutable per commit (rollback selalu punya target), latest untuk kenyamanan. Pola lengkapnya — termasuk tag semver dari git tag — akan kita lihat di workflow final.
  3. Cache lintas pipeline. cache-from/cache-to: type=gha menghubungkan BuildKit ke backend cache GitHub Actions. Pipeline berikutnya mengimpor semua layer yang tidak berubah — build kedua dan seterusnya jauh lebih cepat. Ini mengubah "pipeline yang selalu build dari nol" menjadi "pipeline yang lompat ke tahap yang berubah".

Tip

Untuk project yang mengadopsi semantic versioning (ingat release.config.cjs di repo ini), pola tag terbaik adalah menggabungkan ref git: saat push ke main gunakan tag semver (refs/tags/v1.2.31.2.3), selain itu gunakan sha. Sederhananya: image yang bisa di-deploy harus bisa diidentifikasi ulang — tag yang tidak stabil (latest) tidak pernah cukup sendirian untuk produksi.

Strategi Caching: Menghilangkan Build dari Nol

Kita sudah melihat type=gha. Mari letakkan dalam peta lengkap pilihan cache yang tersedia:

StrategiCocok untukKelebihanKekurangan
type=ghaGitHub ActionsCache tersimpan di backend GH, otomatis dibersihkanHanya untuk GH Actions
type=registrySemua CI, multi-mesinCache jadi bagian image di registryButuh push/pull ekstra
Docker Layer Caching (DLC)Self-hosted runnerLayer di-reuse dari mesin yang samaHanya berlaku per mesin
--cache-from lokalBuild berurutan di satu hostGratis, tanpa konfigurasiTidak lintas mesin

Poin penting untuk dipahami: Docker Layer Caching (DLC) adalah istilah yang sering disalahartikan. Secara teknis, layer caching selalu terjadi saat docker build dijalankan di mesin yang sama — BuildKit menyimpan hasil build sebelumnya. Di CI, DLC hanya menguntungkan jika runner self-hosted dan persisten (mesin yang sama dipakai berulang). Di GitHub-hosted runner, setiap job mendapat mesin baru yang bersih — di situlah type=gha atau type=registry menjadi wajib, karena merekalah yang membawa cache lintas mesin. Ini keputusan yang menentukan: workflow yang "selalu build dari nol" akan 3-5x lebih lambat daripada yang mengimpor cache.

DinD vs DooD: Dua Pendekatan, Satu Pertanyaan

Ketika sebuah job perlu menjalankan perintah docker sendiri — bukan sekadar memanggil action build-push — muncul pertanyaan klasik: di mana daemon Docker-nya? Dua jawaban yang dominan:

Docker-outside-of-Docker (DooD). Runner memakai daemon Docker host (socket /var/run/docker.sock di-mount ke container job). Keuntungannya sederhana dan cache daemon host bisa dipakai. Risikonya: semua container yang dijalankan job adalah tetangga container lain di host — tidak ada isolasi antara build yang berbahaya dengan mesin runner. Mem-mount Docker socket adalah memberikan akses root penuh ke host (ingat risiko grup docker di episode 0).

Docker-in-Docker (DinD). Job menjalankan daemon Docker di dalam container — lewat image docker:*-dind sebagai service. Setiap job punya daemon sendiri yang terisolasi. Keuntungannya isolasi penuh. Risikonya: daemon berjalan privileged (dibutuhkan untuk dind), dan setiap job memulai dari daemon kosong — tidak ada cache Docker (cache BuildKit di sini harus dikelola eksplisit).

jobs:
  dind-example:
    runs-on: ubuntu-latest
    services:
      docker:
        image: docker:27-dind
        options: >-
          --privileged
    steps:
      - uses: actions/checkout@v4
      - name: Jalankan perintah docker
        run: |
          docker build -t app:test .
          docker run --rm app:test npm test

Mana yang direkomendasikan? Sebagian besar kasus, tidak perlu keduanya. docker/build-push-action sudah memakai BuildKit container tanpa DinD sama sekali — ia tidak butuh daemon Docker di dalam job; ia hanya butuh connect ke daemon host yang sudah ada di runner. DinD baru diperlukan ketika kalian benar-benar harus menjalankan CLI docker secara langsung di dalam container job (misalnya docker compose up untuk integration test), dan DooD baru masuk akal di runner self-hosted yang memang ingin memakai cache daemon. Aturan praktisnya:

  1. Build + push image → build-push-action (BuildKit, tanpa DinD).
  2. Perlu docker compose / CLI di dalam job → DinD service (isolasi), atau DooD di self-hosted bila cache host lebih penting.
  3. Hindari --privileged bila bisa — ia melemahkan isolasi runner secara signifikan.

Workflow Lengkap: Build, Scan, SBOM, Push

Sekarang kita rakit semuanya menjadi satu workflow production-grade: test → build multi-platform → scan Trivy → push → SBOM. Jika scan menemukan kerentanan di atas ambang batas, pipeline gagal dan image tidak pernah sampai ke registry:

.github/workflows/docker-build.yml
name: Docker Build & Publish
 
on:
  push:
    branches: [main, staging]
    tags: ["v*"]
 
jobs:
  build-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
 
      - name: Login ke GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
 
      - name: Build & push
        id: build
        uses: docker/build-push-action@v6
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: |
            ghcr.io/${{ github.repository }}:${{ github.sha }}
            ghcr.io/${{ github.repository }}:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max
          sbom: true
          provenance: true
 
      - name: Scan image dengan Trivy
        uses: aquasecurity/trivy-action@0.28.0
        with:
          image-ref: ghcr.io/${{ github.repository }}:${{ github.sha }}
          format: sarif
          output: trivy-results.sarif
          severity: HIGH,CRITICAL
          exit-code: 1
          ignore-unfixed: true
 
      - name: Upload hasil scan sebagai artifact
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: trivy-sarif
          path: trivy-results.sarif
 
      - name: Setup GitHub CLI & verifikasi SBOM
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          gh attestation download \
            --repo ${{ github.repository }} \
            --predicate-type https://spdx.dev/Document \
            -O sbom.spdx.json
          test -s sbom.spdx.json && echo "SBOM terverifikasi"

Bedah workflow final ini — ia merangkum tiga episode terakhir sekaligus:

  • Tag. ${{ github.sha }} (immutable per commit) + latest; untuk push tags: ["v*"], semver dari git tag akan menjadi tag image (perlu penyesuaian nama via ekspresi refs/tags/v...). Proposisi kuncinya tetap: setiap image dapat diidentifikasi kembali.
  • sbom: true + provenance: true memanfaatkan fitur BuildKit dari episode 18 — SBOM dan jejak bangunan dikirim ke registry bersama image, tanpa action tambahan.
  • Trivy dengan exit-code: 1 mengubah scan dari laporan menjadi gerbang: satu kerentanan HIGH/CRITICAL yang unfixable menghentikan pipeline. ignore-unfixed: true mencegah kegagalan pada kerentanan yang belum ada perbaikannya (strategi umum agar pipeline tidak terus-terusan merah).
  • SBOM didownload ulang via gh attestation download — verifikasi bahwa SBOM yang kita klaim benar-benar terpasang ke image di registry (menutup lingkaran supply chain dari episode 14).

Pitfalls yang Paling Sering Merusak Pipeline

  1. Secret bocor ke env build. Secret yang tersimpan di secrets.* hanya boleh dipakai sebagai build arg via --secret (episode 18), bukan diekspor ke env lalu direferensikan dalam layer image. Sekali token masuk ARG/ENV, ia mengendap di image selamanya.

  2. Tag yang tidak stabil. Menandai setiap push dengan latest saja membuat dua image berbeda menyandang nama sama — tidak ada cara membedakan, tidak ada rollback yang jelas. Selalu sertakan sha atau semver di samping latest.

  3. Mengabaikan cache. Pipeline tanpa cache-from/cache-to adalah build dari nol setiap kali — 3-5x lebih lambat dan membuang kuota runner. Gunakan type=gha (hosted) atau type=registry (multi-mesin).

  4. DooD sembarangan di runner bersama. Mounting Docker socket di runner hosted memberi akses root ke host runner — dan job lain di runner itu. Untuk workload mandiri, lebih aman DinD service atau, lebih baik lagi, build-push-action yang tidak butuh keduanya.

  5. Scan setelah push. Jangan men-scan image yang sudah terlanjur mendarat di registry dan berlabel latest — ganti urutannya: build → scan → (lolos) → push. Workflow di atas menjaga urutan ini dengan benar.

Penutup

Pada episode 19 ini kita telah mengotomatiskan siklus hidup image: menjalankan job di dalam kontainer (container: node:20) dengan service pendamping, membangun & mendorong image lewat docker/login-action dan docker/build-push-action yang memakai BuildKit secara native, mengoptimalkan pipeline dengan cache lintas mesin (type=gha/type=registry dan memahami posisi DLC di self-hosted), membedah DinD vs DooD beserta risiko --privileged dan cache yang hilang, serta merakit workflow lengkap yang menutup dengan scan Trivy dan verifikasi SBOM sebelum image diizinkan masuk registry.

Inti yang harus kalian bawa:

  • Pipeline menjawab siapa yang terakhir membangun? dengan tegas: setiap commit, rangkaian yang sama, otomatis.
  • Job di container + healthcheck service = environment reproducible untuk test.
  • build-push-action = BuildKit tanpa DinD; jangan --privileged bila bisa dihindari.
  • Cache lintas pipeline (type=gha) mengubah build dari nol menjadi lompat-ke-berubah.
  • Urutan yang benar: test → build → scan → push → SBOM.

Sekarang kalian punya image yang dibangun, dipindai, dan didorong secara otomatis. Persoalan terakhir yang tersisa di perjalanan ini: menghantarkan image itu ke pengguna — publikasi yang benar-benar production-grade. Di episode 20 selanjutnya kita akan menutup jembatan itu: Production Deployment & Reverse Proxy Integration — satu IP publik untuk banyak service, NGINX vs Traefik dengan TLS otomatis, dan strategi zero-downtime deployment. Semua fondasi sudah kalian bangun dari episode 0 sampai 19; tinggal kita rakit menjadi arsitektur yang utuh. Sampai jumpa di episode 20!

Belajar Docker - Integrasi Docker dalam CI/CD Pipeline | Belajar Docker