Kurungan resource yang kita pasang pada Chapter Mengelola Resource di Produksi akhirnya lengkap: CPU dibatasi, memori dijaga, PIDs dicekik, dan pemantauan berdenyut di docker stats — tetapi semua kurungan itu mengurung satu penghuni yang belum pernah kita periksa kelengkapannya, yaitu image itu sendiri. Perjalanan course ini membawa kita dari hello-world yang 13.3kB, melewati Dockerfile dan build cache, compose dan reverse proxy, keamanan dan supply chain, sampai ke batas resource — dan di sepanjang jalan itu image yang kita bangun memang dirancang untuk belajar: cukup gemuk agar nyaman di-debug, cukup longgar agar cepat bereksperimen. Produksi menuntut kelas yang berbeda. Image yang melayani pengguna sungguhan akan ditarik oleh server produksi berkali-kali, dipindai oleh orang yang tidak kita kenal, dan diserang oleh orang yang tidak kita undang — setiap megabait dan setiap baris ENV di dalamnya adalah kewajiban yang harus dipertanggungjawabkan.

Chapter ini membuka Section Deployment dan Production dengan pekerjaan yang paling mendasar: membawa aplikasi toko naik kelas menjadi image produksi. Backend Node.js yang sejak Section Multi-Container Application mendengarkan port 3000 akan dibangun ulang sebagai registry.example.com/toko/backend:1.0.0 — dua panggung, non-root, berlabel OCI, ber-provenance — lalu diaudit tiga kali sebelum dinyatakan layak berangkat. Peta stack yang akan menemani lima chapter ke depan sudah dapat kita hafalkan sekarang:

ServicePeranImage
backendAPI Node.js :3000 dengan /healthzregistry.example.com/toko/backend:1.0.0
frontendhalaman statis tokonginx:1.29-alpine
dbdata produk dan pesananpostgres:18-alpine
cachepembacaan yang dipanaskanredis:7-alpine
proxysatu-satunya pintu, kelak ber-TLSnginx:1.29-alpine

Image Latihan dan Image Production #

Perbedaan antara image yang menemani pengembangan dan image yang berlayang ke produksi bukan soal selera, melainkan soal konsekuensi. Image development ditarik satu kali ke satu mesin oleh satu orang; image produksi ditarik berulang kali oleh server-server yang bahkan belum kita beli. Image development salah isi hanya merepotkan Budi sendiri; image produksi salah isi merepotkan pelanggan Siti yang sedang mencoba membayar. Tabel berikut merangkum enam aspek yang membedakan keduanya pada kasus toko:

AspekImage development (toko-backend:dev)Image produksi (registry.example.com/toko/backend:1.0.0)
Ukuran486MB — devDependencies, alat debug, sumber lengkap142MB — runtime dan dependensi produksi saja
Userroot demi kebebasan memasang apa sajanode (UID 1000), tanpa hak istimewa
Alat debugsh, curl, nodemon, test runner terpasangsesedikit mungkin; idealnya tanpa shell
Source codebind mount dari host, image hanya rangkaterbakal utuh dari build context + lockfile
Tagdev, berganti-ganti tanpa makna1.0.0 jelas, deployment dikunci ke digest
Healthcheckjarang ada — status Up dianggap cukuptertanam di image, docker ps jujur sejak awal

Sembilan Pemeriksaan Sebelum Image Berangkat #

Sebagian besar kebiasaan pada tabel di atas sebenarnya sudah kita latih satu per satu di section-section sebelumnya; tugas chapter ini adalah merangkainya menjadi satu ritus rilis. Sembilan pemeriksaan berikut adalah definisi “siap produksi” yang kita pakai untuk toko:

#ItemMengapaRujukan
1Multi-stage buildalat build dan dependensi dev tidak ikut terkirim ke serverMulti-Stage Build
2Pin base image by digesttag bisa digeser diam-diam; digest tidakImage Digest
3Non-rootcontainer root adalah tangga eskalasi ke hostMenjalankan Container Non-Root
4Tanpa shell bila bisapermukaan serangan mengecil drastisvarian distroless di bawah
5Tanpa rahasia di layerlayer permanen, terbaca docker history, ikut terdistribusiSecret dan SSH Mount
6HEALTHCHECK tertanamsetiap container dari image ini jujur melaporkan kesehatannyaHEALTHCHECK
7LABEL standar OCImetadata terbaca tool — registry, scanner, auditorVOLUME dan LABEL
8Tag versi yang jelasrollback dan audit menuntut penamaan yang tidak ambiguStrategi Versioning Image
9Provenance + SBOMasal-usul build dan inventaris isi bisa diverifikasi siapa punImage Provenance

Dockerfile Produksi untuk Backend Toko #

Budi membuka kembali folder toko yang lama dan masuk ke api/ — kode aplikasinya sudah matang (endpoint /produk.json, /healthz yang mengetuk database, penangan SIGTERM dari Chapter Graceful Shutdown), dan kali ini yang ditulis ulang adalah Dockerfile-nya. Satu berkas baru juga lahir: package-lock.json kini ter-commit, syarat yang diminta npm ci sejak Chapter Build Cache.

text
toko/
├── compose.yaml
├── nginx.conf
├── frontend-html/
│   └── index.html
└── api/
    ├── Dockerfile
    ├── package.json
    ├── package-lock.json
    └── server.js

Dockerfile produksinya memakai dua panggung, keduanya berdiri di atas basis yang sama yang di-pin ke digest — nilai yang persis kita pakai pada Chapter Supply Chain Security:

dockerfile
FROM node:22-alpine@sha256:22c1edb32d80d7d5460fa55063156af7fa5daa79bf932170a06e30c9d8fbb09f AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev

FROM node:22-alpine@sha256:22c1edb32d80d7d5460fa55063156af7fa5daa79bf932170a06e30c9d8fbb09f AS runtime
WORKDIR /app
COPY --from=deps --chown=node:node /app/node_modules ./node_modules
COPY --chown=node:node server.js ./
ENV NODE_ENV=production
USER node
EXPOSE 3000
LABEL org.opencontainers.image.title="toko-backend" \
      org.opencontainers.image.description="API backend Toko Kopi Nusantara" \
      org.opencontainers.image.version="1.0.0" \
      org.opencontainers.image.source="https://git.example.com/toko/backend" \
      org.opencontainers.image.licenses="MIT"
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
  CMD wget -qO- http://localhost:3000/healthz || exit 1
CMD ["node", "server.js"]

Mari kita telaah delapan keputusan pada Dockerfile di atas:

  1. Dua FROM dengan nama panggung — panggung deps mengurus dependensi, panggung runtime menjadi image final; hanya hasil COPY dari deps yang menyeberang, sisanya (npm cache, teks manifest, alat build) tertinggal.
  2. npm ci --omit=dev dengan cache mount — instalasi deterministik dari lockfile tanpa devDependencies, dan --mount=type=cache menjaga tarball tetap hangat di builder justru ketika manifest berubah — kombinasi yang kita latih pada Chapter Cache Mount.
  3. COPY --from=deps --chown=node:node — kepemilikan dipindahkan saat penyeberangan, sehingga panggung runtime tidak pernah menjalankan satu instruksi pun sebagai root.
  4. ENV NODE_ENV=production — sinyal bagi runtime dan pustaka untuk mematikan perilaku pengembangan; kita menulisnya eksplisit meski deployment juga menyuntikkan nilai lain lewat environment.
  5. USER node — image resmi node sudah menyediakan user node ber-UID 1000, jadi tidak perlu adduser seperti pada image buatan sendiri (disiplin Chapter Menjalankan Container Non-Root).
  6. LABEL lima field OCI — identitas image yang terbaca mesin: nama, deskripsi, versi, sumber, lisensi; docker image inspect dan registry menampilkannya tanpa membongkar layer.
  7. HEALTHCHECK mengetuk /healthz — probe bawaan image dengan --start-period=10s sebagai masa hangar Node membuka pool koneksi; wget hadir dari busybox Alpine tanpa satu paket pun terpasang tambahan.
  8. CMD exec form — node menjadi PID 1 dan menerima SIGTERM langsung, prasyarat upacara shutdown sopan yang sudah tertanam di server.js.

Kenapa Multi-Stage Walau Tanpa Kompilasi #

Untuk aplikasi Go, multi-stage menyelamatkan ratusan megabait karena kompiler tertinggal; untuk Node.js yang “hanya menyalin berkas”, keuntungannya tampak kecil — padahal tiga hal tetap dibayar penuh. Pertama, pemisahan dependensi dari kode: panggung deps hanya berisi manifest dan node_modules, sehingga iterasi server.js tidak pernah menyentuh layer dependensi yang besar — cache bekerja pada granularitas yang benar. Kedua, pembersihan otomatis: apa pun yang terjadi di deps (cache npm, berkas sementara, alat bantu) tidak pernah merembes ke image final, karena satu-satunya jalan masuk adalah COPY --from yang kita tulis sadar. Ketiga, disiplin mental: panggung terpisah memaksa kita menyatakan “apa yang layak dibawa ke produksi” secara eksplisit — dan pertanyaan itu, bukan trik ukuran, adalah inti dari image produksi.

Varian Distroless untuk Aplikasi Go #

Sembilan pemeriksaan tadi menyisakan satu yang belum terjawab: “tanpa shell bila bisa”. Untuk backend Node.js jawabannya “belum bisa” — runtime Node membutuhkan filesystem dan pustaka yang menyertainya. Aplikasi yang dikompilasi menjadi binary mandiri bisa melangkah lebih jauh dengan image distroless, seperti layanan pendata stok toko yang ditulis Budi dengan Go:

dockerfile
FROM golang:1.24-alpine AS build
WORKDIR /src
COPY *.go go.mod ./
RUN CGO_ENABLED=0 go build -o /out/server .

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/server /server
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/server"]

Mari kita telaah empat baris kuncinya:

  1. CGO_ENABLED=0 — binary dikompilasi statis, tidak terikat libc apa pun, sehingga bisa berjalan di image kosong tanpa pustaka sistem.
  2. gcr.io/distroless/static-debian12:nonroot — image yang hanya berisi berkas-berkas runtime esensial: tanpa package manager, tanpa shell, tanpa satu perintah interaktif pun.
  3. USER nonroot:nonroot — distroless menyediakan user non-root tetap dengan UID 65532, jauh dari semua UID sistem yang mungkin bertabrakan di host.
  4. ENTRYPOINT ["/server"] — binary menjadi PID 1 langsung; tidak ada shell perantara yang harus melewati sinyal.

Harganya jujur harus dibayar: tanpa shell, docker exec <container> sh gagal — debugging dari dalam container menjadi mustahil pada varian ini. Untuk itulah distroless menyediakan varian :debug yang menambahkan busybox shell, dipakai sementara saat insiden lalu diganti kembali ke varian bersih pada rilis berikutnya.

Build dengan Attestasi ke Registry #

Image produksi tidak berhenti di mesin build; ia lahir untuk didorong ke registry privat toko — registry.example.com, tempat Budi sudah memiliki akun dan kredensial login dari Chapter Private Registry. Buildnya memakai buildx dengan dua attestation menyala, persis resep Chapter Image Provenance:

bash
$ docker buildx build \
    --provenance=mode=max --sbom=true \
    -t registry.example.com/toko/backend:1.0.0 --push .

Output yang Dihasilkan (dipotong):

text
[+] Building 1m58s (20/20) FINISHED
 => [internal] load build definition from Dockerfile                       0.1s
 => => transferring dockerfile: 1.1kB                                      0.0s
...
 => [deps 1/4] FROM docker.io/library/node:22-alpine@sha256:22c1edb32d8…  0.0s
 => => resolve docker.io/library/node:22-alpine@sha256:22c1edb32d80d7d5…  0.8s
 => [deps 2/4] WORKDIR /app                                                0.0s
 => [deps 3/4] COPY package.json package-lock.json ./                      0.1s
 => [deps 4/4] RUN --mount=type=cache,target=/root/.npm npm ci --omit=…  13.8s
...
 => [runtime 3/10] COPY /app/node_modules ./node_modules                   1.9s
 => [runtime 4/10] COPY server.js ./                                       0.1s
...
 => [runtime 8/10] LABEL org.opencontainers.image.title=toko-backend ...   0.0s
 => [runtime 9/10] HEALTHCHECK &{["CMD-SHELL" "wget -qO- http://localh…   0.0s
 => [runtime 10/10] CMD ["node" "server.js"]                               0.0s
 => exporting to image                                                     2.4s
 => => exporting layers                                                    1.8s
 => => exporting manifest sha256:9c4f27e1d3a5b8c0e2f4a6d8b0c2e4f6a8d0b2c4e6f8a0d2b4c6e8f0a2d4b6c8
 => => exporting attestation manifest sha256:4b7e2c91a3d5f8b0c2e4a6d8f0a2c4e6b8d0a2f4c6e8b0d2a4f6c8e2b0d4a6f
 => => exporting sbom manifest sha256:8a1e4c7b2d9f3a6c5b0d7f2e4c9a1b3d5f7c9e1a3b5d7f9c1e3a5b7d9f1c3e5f
 => => exporting manifest list sha256:d6b0f2a4c8e1d3b5f7a9c0e2d4b6f8a1c3e5d7f9b1a3c5e7d9f0b2a4c6e8d0f2
 => => pushing layers                                                     41.7s
 => => pushing manifest for registry.example.com/toko/backend:1.0.0        1.3s

Mari kita telaah tiga flag yang membedakan build ini dari build latihan:

  1. --provenance=mode=max — setiap detail proses build (source, flag, konteks) tercatat sebagai attestation yang menempel pada image di registry; klaim “image ini lahir dari build begini” kini bisa diverifikasi, bukan sekadar dipercaya.
  2. --sbom=true — builder menyusun inventaris paket isi image sebagai attestation kedua; scanner dan auditor membacanya tanpa mengunduh ulang layer.
  3. --push — hasil langsung dikirim ke registry, karena attestation memang dirancang hidup di sana; baris exporting manifest list di akhir adalah image index yang menjadi alamat sejati rilis ini — digest yang kelak dikunci di deployment.

Audit Ukuran, Riwayat, dan Kerentanan #

Build yang berhasil belum berarti image yang layak; tiga audit menutup ritus rilis ini. Audit pertama menegaskan bahwa image memang sampai ke registry dan bisa ditarik kembali — persis yang akan dilakukan server produksi kelak:

bash
$ docker pull registry.example.com/toko/backend:1.0.0

Output yang Dihasilkan (dipotong):

text
1.0.0: Pulling from toko/backend
a0b3c4d5e6f7: Pull complete
...
Digest: sha256:d6b0f2a4c8e1d3b5f7a9c0e2d4b6f8a1c3e5d7f9b1a3c5e7d9f0b2a4c6e8d0f2
Status: Downloaded newer image for registry.example.com/toko/backend:1.0.0

Audit kedua adalah perbandingan ukuran — image baru berdampingan dengan image development yang menemani Budi sejak stack toko dirakit:

bash
$ docker images

Output yang Dihasilkan:

text
REPOSITORY                        TAG      IMAGE ID       CREATED         SIZE
registry.example.com/toko/backend 1.0.0    5f3a91c2b7e4   5 minutes ago   142MB
toko-backend                      dev      c8e41d6a2f90   3 days ago      486MB

Selisih 344MB itu bukan angka kosmetik. Setiap tarikan image produksi memindahkan sepertiga giga lebih sedikit; setiap pemulihan insiden jam tiga pagi menunggu sepertiga giga lebih singkat; dan setiap paket yang tidak dibawa adalah paket yang tidak perlu dipindai, di-patch, dan diargumentasikan. Basis node:22-alpine sekitar 130MB ditambah dependensi produksi express dan pg menghasilkan 142MB — aritmetika yang jujur, bukan keajaiban.

Audit ketiga membaca riwayat layer — pertanyaannya spesifik: adakah baris yang membawa nilai rahasia, dan adakah baris yang tidak seharusnya ada?

bash
$ docker history registry.example.com/toko/backend:1.0.0

Output yang Dihasilkan (dipotong):

text
IMAGE          CREATED         CREATED BY                                      SIZE      COMMENT
5f3a91c2b7e4   5 minutes ago   CMD ["node" "server.js"]                        0B
<missing>      5 minutes ago   HEALTHCHECK &{["CMD-SHELL" "wget -qO- http:/…   0B
<missing>      5 minutes ago   LABEL org.opencontainers.image.title=toko-ba…   0B
<missing>      5 minutes ago   EXPOSE 3000                                     0B
<missing>      5 minutes ago   USER node                                       0B
<missing>      5 minutes ago   ENV NODE_ENV=production                         0B
<missing>      5 minutes ago   COPY server.js ./ # buildkit                    1.4kB
<missing>      5 minutes ago   COPY /app/node_modules ./node_modules # bui…   12.1MB
<missing>      5 minutes ago   WORKDIR /app                                    0B
...
<missing>      3 days ago      ENV NODE_VERSION=22.20.0                        0B

Riwayat itu bersih dalam arti yang kita latih sejak Chapter Secret dan SSH Mount: satu-satunya ENV adalah NODE_ENV dan milik basis image — tidak ada token, tidak ada password, tidak ada satu pun nilai yang membuat kita menyesal image ini tersebar. Pemeriksaan terakhir menyerahkan penilaian kepada pemindai, sebagaimana Chapter Image Scanning mengajarkan:

bash
$ docker scout cves registry.example.com/toko/backend:1.0.0

Output yang Dihasilkan (dipotong):

text
### Overview

target:
  └─ registry.example.com/toko/backend:1.0.0

    digest      sha256:9c4f27e1d3a5b8c0e2f4a6d8b0c2e4f6a8d0b2c4e6f8a0d2b4c6e8f0a2d4b6c8
    os          alpine 3.20
    size        142MB
    packages    118
...
### CVEs found: 7 (0C 0H 2M 5L)

CVE ID          SEVERITY    AFFECTED PACKAGE    FIXED IN
CVE-2026-10417  MEDIUM      musl                1.2.5-r2
CVE-2026-09382  MEDIUM      busybox             1.36.1-r8
...

Tujuh temuan tanpa satu pun CRITICAL atau HIGH adalah laporan yang sehat untuk rilis pertama: dua MEDIUM dijadwalkan naik versi basis pada jendela pemeliharaan berikutnya, dan lima LOW dicatat untuk dipantau. Angka ini juga bukan kebetulan — memotong 344MB devDependencies dan alat debug berarti memotong sebagian besar tempat CVE biasa bersembunyi, pelajaran yang sama dengan Section Keamanan Docker dalam bungkusan yang lebih ringkas.

Image 1.0.0 kini berdiri di registry dengan ukuran yang wajar, riwayat yang bersih, temuan yang terkelola, dan attestation yang bisa diaudit. Namun image ini membawa satu kebiasaan lama yang belum kita bahas: sepanjang course, konfigurasi aplikasi selalu menempel sebagai environment variable yang ditulis langsung di compose.yaml — kebiasaan yang ampuh untuk satu stack di satu mesin, tetapi rapuh begitu toko memiliki development, staging, dan produksi sekaligus. Chapter Konfigurasi Production merapikan urusan tersebut: memisahkan konfigurasi dari kode, menyusunnya per environment, dan mengajarkan aplikasi kita menolak menyala dengan konfigurasi yang timpang.

Referensi #