Chapter Development Dependencies menutup dengan satu kemajuan yang mengubah wajah Dockerfile katalog-web: target dev kini berdiri sebagai stage tersendiri, membawa devDependencies lengkap sementara volume node_modules menjaga dependensi container tetap utuh dari hari ke hari. Pertanyaan yang menyertainya belum selesai dijawab: image yang manakah yang kita bangun, kapan membangunnya, dan dengan kebutuhan seperti apa? Development dan produksi ternyata menuntut hal-hal yang saling berlawanan — image yang nyaman untuk mengedit kode jarang sekali layak berdiri di server. Chapter ini merakit jawaban finalnya: satu Dockerfile dengan empat target yang melayani kedua dunia, dua image dengan tag yang tegas berbeda, dan satu disiplin tentang kapan masing-masing image dibangun ulang.
Kebutuhan yang Saling Berlawanan #
Biaya dari seluruh kemudahan section ini baru terasa ketika kita membayangkan image yang sama berdiri di server produksi. Tool pemantau berkas, dependensi pengembangan, dan pintu debugger adalah muatan yang tidak pernah dipakai server; sebaliknya, healthcheck tertanam dan ukuran yang ramping adalah tuntutan yang hanya muncul di sana. Tabel berikut memetakan enam perbedaan yang paling menentukan:
| Aspek | Image development | Image produksi |
|---|---|---|
| Dependensi | seluruhnya, termasuk devDependencies seperti nodemon | hanya dependencies aplikasi lewat --omit=dev |
| Kode sumber | ditimpa bind mount dari host selama sesi development | tertanam permanen lewat COPY |
| Perintah bawaan | nodemon dengan watch mode | node src/server.js dalam exec form |
| Ukuran image | bebas gemuk demi kenyamanan | seramping mungkin demi tarik-unduh cepat |
| Port debugger | 9229 dibuka untuk node --inspect | tidak ada pintu tambahan sama sekali |
| HEALTHCHECK | tidak dibutuhkan — log selalu kita awasi sendiri | tertanam agar kolom STATUS jujur di server |
Mari kita telaah keenam baris di atas sebelum merakit solusinya:
- Setiap baris adalah tarik-ulur antara dua pemakai image yang berbeda. nodemon menghabiskan ukuran untuk membeli siklus edit yang instan; healthcheck memakan ruang metadata untuk membeli kejujuran status bagi operator yang tidak mengawasi log.
- Satu image tidak bisa menjadi yang terbaik di keduanya sekaligus. Image yang membawa devDependencies dan test suite akan gemuk di server; image yang telanjang akan menyulitkan setiap sesi development yang mengandalkan tool.
- Solusinya bukan dua Dockerfile. Dua resep yang menduplikasi manifest dependensi akan berbeda diam-diam dalam hitungan minggu — satu berganti dependensi, satunya tertinggal. Satu Dockerfile multi-target, sebagaimana pola yang kita bangun pada Chapter Multi-Stage Build dan dilanjutkan Chapter Development Dependencies, menjaga kedua wajah tetap lahir dari satu sumber kebenaran.
- Rantai target yang kita tuju berbentuk
base → dev → test → runtime— fondasi bersama, wajah development, gerbang uji, lalu wajah produksi yang hanya mewarisi apa yang benar-benar dibutuhkannya.
Dockerfile Empat Target #
Sebelum merakit, Budi merapikan dua berkas kecil agar seluruh kebutuhan di atas punya tempat yang jelas. Struktur folder yang relevan bagi build kini berbentuk:
katalog/
├── Dockerfile
├── package.json
├── src/
│ ├── rupiah.js
│ └── server.js
└── test/
└── unit/
└── harga.test.jsManifest package.json memisahkan dua kelas dependensi sejak awal, dan skrip test menunjuk folder unit test yang akan kita pakai penuh pada Chapter Menjalankan Test di Container:
{
"name": "katalog-web",
"version": "2.0.0",
"scripts": {
"dev": "nodemon src/server.js",
"test": "node --test test/unit/"
},
"dependencies": {
"express": "^4.19.2",
"pg": "^8.12.0"
},
"devDependencies": {
"nodemon": "^3.1.0"
}
}Inilah Dockerfile final yang menggantikan seluruh versi sebelumnya — empat target, dua puluh dua baris:
# syntax=docker/dockerfile:1
FROM node:22-alpine AS base
WORKDIR /app
COPY package.json ./
FROM base AS dev
RUN npm install
COPY src/ ./src/
COPY test/ ./test/
USER node
EXPOSE 3000 9229
CMD ["npx", "nodemon", "src/server.js"]
FROM dev AS test
RUN npm test
FROM base AS runtime
ENV NODE_ENV=production
RUN npm install --omit=dev
COPY --from=test /app/src ./src
USER node
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -qO- localhost:3000/healthz || exit 1
CMD ["node", "src/server.js"]Diagram berikut memetakan hubungan keempat target — perhatikan bahwa runtime berakar pada base, bukan pada dev:
Mari kita telaah diagram di atas: satu akar bernama base menurunkan dua cabang; cabang dev berlanjut ke test, dan runtime tidak mewarisi apa pun dari dev melainkan hanya menerima hasil uji lewat satu jembatan COPY --from. Seluruh keputusan yang kita ambil sepanjang section berpendar pada struktur ini. Bedah baris demi baris:
FROM node:22-alpine AS base— fondasi bersama ketiga target lainnya. Isinya sengaja minimal: direktori kerja dan manifest dependensi. Selamapackage.jsontidak berubah, layerCOPYdi sini ter-cache untuk semua build — aturan emas Chapter Build Cache kini bekerja untuk dua image sekaligus.COPY package.json ./diletakkan sebelum kode apa pun — perubahan padasrc/server.jstidak pernah membatalkan layernpm install, baik pada targetdevmaupunruntime.FROM base AS dev— wajah development mewarisi fondasi lalu menjalankannpm installpenuh: seluruhdependenciesdandevDependenciesterpasang, nodemon tersedia, pohontest/ikut tersalin. Bind mount source code dan volumenode_modulesdari dua chapter sebelumnya tetap bekerja di atas image ini tanpa satu perubahan pun.USER nodepada target dev — proses pengembangan tetap turun dari root, kebiasaan yang kita paku sejak Chapter WORKDIR dan USER; usernodebernilai UID 1000, cocok dengan kepemilikan berkas di mesin Budi sehingga bind mount tetap bisa ditulis.EXPOSE 3000 9229— dua port dalam satu instruksi: 3000 untuk aplikasi, 9229 untuk debugger Node yang akan kita nyalakan lewatnode --inspectpada Chapter Debugging Container.EXPOSEhanya metadata — penerbitan port tetap urusan compose.CMD ["npx", "nodemon", "src/server.js"]— perintah bawaan target dev adalah watcher, sehingga container development otomatis memulai ulang aplikasi saat kode berubah; kebiasaancommand: node --watchyang selama ini menempel di compose.override.yaml Budi kini boleh dibuang karena image-nya sendiri yang membawanya.FROM dev AS test+RUN npm test— stage uji mewarisi seluruh isidevlalu menjalankan unit test saat build berlangsang. Satu test gagal berarti build gagal — image produksi tidak pernah lahir dari kode yang tidak lulus uji.FROM base AS runtime— bukanFROM dev— inilah baris terpenting di seluruh berkas. Image produksi kembali ke fondasi bersama dan memasang dependensi sendiri lewatnpm install --omit=dev: nodemon, pohon devDependencies, dan berkas test tidak pernah menyusup ke runtime.COPY --from=test /app/src ./src— satu jembatan dengan dua tugas. Pertama, memaksa stagetestbenar-benar dieksekusi: BuildKit hanya membangun stage yang menjadi dependensi image final, jebakan yang kita buktikan pada NOTE Chapter Multi-Stage Build — tanpa baris ini,RUN npm testakan diam-diam dilewati. Kedua, kode yang berlayar ke runtime adalah salinan yang persis sama dengan yang barusan lulus uji.HEALTHCHECKtertanam di image — probe mengetuk endpoint/healthzyang kita bangun pada Chapter Healthcheck dan Service Readiness, endpoint yang ikut memeriksa koneksi database. Produksi tidak selalu punya compose.yaml di sampingnya; dengan probe tertanam, setiap container dari image ini melaporkan kesehatannya di mana pun ia berjalan.CMD ["node", "src/server.js"]dalam exec form — tanpa shell perantara, tanpa watcher; sinyalSIGTERMdaridocker stopsampai langsung ke proses Node, persis perilaku yang kita andalkan pada pembahasan Graceful Shutdown.
Membangun Kedua Image #
Dua perintah berikut membangun kedua wajah secara eksplisit — kebiasaan yang kelak juga dipakai mesin CI. Perintah pertama berhenti pada target dev:
$ docker build --target dev -t katalog-web:dev .Output yang Dihasilkan (dipotong):
[+] Building 7.2s (8/8) FINISHED
=> [internal] load build definition from Dockerfile 0.1s
=> [internal] load metadata for docker.io/library/node:22-alpine 0.7s
=> [1/8] FROM docker.io/library/node:22-alpine 0.0s
=> [2/8] WORKDIR /app 0.0s
=> [3/8] COPY package.json ./ 0.1s
=> [4/8] RUN npm install 5.1s
=> [5/8] COPY src/ ./src/ 0.1s
=> [6/8] COPY test/ ./test/ 0.1s
=> [7/8] USER node 0.0s
=> [8/8] EXPOSE 3000 9229 0.0s
=> exporting to image 0.3s
=> => exporting layers 0.2s
=> => writing image sha256:8f3a2c9e5b1d4f6a8c0e2b4d6f8a1c3e5b7d9f2a4c6e8b0d2f4a6c8e0b2d4f6a 0.1s
=> => naming to docker.io/library/katalog-web:dev 0.0sPerintah kedua tidak menulis --target sama sekali — build berhenti pada stage terakhir, dan stage terakhir kita adalah runtime:
$ docker build -t katalog-web:2.0 .Output yang Dihasilkan (dipotong):
[+] Building 13.8s (15/15) FINISHED
=> [1/8] FROM docker.io/library/node:22-alpine 0.0s
=> CACHED [2/8] WORKDIR /app 0.0s
=> CACHED [3/8] COPY package.json ./ 0.0s
=> CACHED [4/8] RUN npm install 0.0s
=> CACHED [5/8] COPY src/ ./src/ 0.0s
=> CACHED [6/8] COPY test/ ./test/ 0.0s
=> CACHED [7/8] USER node 0.0s
=> CACHED [8/8] EXPOSE 3000 9229 0.0s
=> [9/15] RUN npm test 3.4s
=> [10/15] ENV NODE_ENV=production 0.0s
=> [11/15] RUN npm install --omit=dev 5.3s
=> [12/15] COPY --from=test /app/src ./src 0.1s
=> [13/15] USER node 0.0s
=> [14/15] EXPOSE 3000 0.0s
=> [15/15] HEALTHCHECK &{["CMD-SHELL" "wget -qO- localhost:3000/healthz || exit 1"] 30s 3s 5s 0s 3 false} 0.0s
=> exporting to image 0.4s
=> => exporting layers 0.2s
=> => writing image sha256:4d8b2e7f1a6c9e3b5d7f1a3c5e7b9d1f3a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b 0.1s
=> => naming to docker.io/library/katalog-web:2.0 0.0sKedua image kini berdiri berdampingan di penyimpanan lokal:
$ docker images katalog-webOutput yang Dihasilkan:
REPOSITORY TAG IMAGE ID CREATED SIZE
katalog-web 2.0 4d8b2e7f1a6c 40 seconds ago 142MB
katalog-web dev 8f3a2c9e5b1d 4 minutes ago 189MBMari kita telaah seluruh rangkaian build di atas:
- Stage terakhir adalah target bawaan. Tanpa
--target, BuildKit membangun hinggaruntime— itulah alasan perintah produksi lebih pendek daripada perintah development. - Kedelapan langkah pertama dilayani
CACHEDpada build kedua. Fondasi dan wajah dev sudah dibangunkan oleh perintah pertama; build produksi hanya mengerjakan lima langkah baru — uji,ENV, instalasi dependensi ramping, penyalinan kode, dan metadata. [9/15] RUN npm testbenar-benar berjalan. Ini bukti jembatanCOPY --from=testbekerja: tiga detik itu adalah harga gerbang yang memastikan tidak ada image produksi dari kode yang gagal uji.npm install --omit=devterjadi di cabang yang berbeda. Langkah[11/15]tidak meng-cacheRUN npm installmilik dev — keduanya adalah vertex berbeda pada DAG BuildKit, masing-masing dengan cache sendiri.- Selisih 47MB adalah seluruh isi world development. Pohon devDependencies nodemon, berkas test, dan dua layer
COPYtambahan hanya hidup dikatalog-web:dev. Angka 189MB sendiri adalah kelanjutan yang wajar: image dev katalog-web masih 158MB pada Chapter Image, Build, dan Pull — sebelum devDependencies hadir. - 142MB adalah aritmetika yang jujur. Basis
node:22-alpinesekitar 130MB ditambah dependensi produksi express danpg; dibandingkan standar Section Image Build dan Optimisasi, image ini memang bukan yang teramping — Node.js butuh runtime-nya — tetapi setiap megabait yang tidak dibawa adalah megabait yang tidak ditarik setiap server dan tidak menjadi permukaan serangan, argumen yang kita bangun tuntas pada Chapter Mengurangi Ukuran Image.
Dua Wajah di Dua Berkas Compose #
Disiplin dua image menuntut kejelasan di sisi compose juga: berkas mana yang memakai image yang mana. Service web pada compose.yaml kini menulis bentuk panjang build dengan sub-kunci target — bentuk yang kita kenal dari Chapter Image, Build, dan Pull:
web:
build:
context: .
target: dev
image: katalog-web:devKunci lainnya — ports, environment, env_file, depends_on, healthcheck service, bind mount source code, volume node_modules — tidak berubah sedikit pun. Yang berubah hanya satu hal: setiap docker compose up -d --build kini membangun target dev, sehingga stack development tidak pernah menjalankan image runtime secara tidak sengaja. Aturan develop.watch di compose.override.yaml Budi tetap bekerja di atasnya: suntingan package.json dan Dockerfile memicu rebuild, dan karena watcher nodemon kini dibawa CMD image, override tersebut cukup menyimpan aturan pemantauan.
Varian produksi sudah kita miliki sejak Chapter Compose Override dan Environment: berkas compose.prod.yaml yang membuang kunci build dan menggantinya dengan image dari registry:
services:
web:
build: !reset null
image: budi/katalog-web:${IMAGE_TAG:-latest}
ports: !override
- "${BIND_IP:-0.0.0.0}:80:3000"
restart: unless-stoppedTag !reset null menghapus kunci build dari hasil penggabungan sehingga server produksi murni bergantung pada image yang ditarik dari registry, dan !override menjadikan pemetaan port 80 sebagai satu-satunya yang diterbitkan. Image budi/katalog-web:2.0 yang baru saja kita bangun adalah kandidat tepat untuk ditempelkan pada variabel IMAGE_TAG — perjalanan naiknya ke registry adalah urusan Section Registry dan Distribusi, dan seluruh upacara deployment-nya dibedah pada Section Deployment dan Production.
Kapan Rebuild Image Mana #
Ritual dua image menjadi lengkap dengan tabel keputusan berikut — kapan sebuah perubahan menuntut image mana dibangun ulang:
| Perubahan yang terjadi | Selama sesi development | Saat menyiapkan rilis |
|---|---|---|
Suntingan berkas di src/ | tanpa rebuild — bind mount dan nodemon yang bekerja | docker build -t katalog-web:2.1 . |
Berkas di test/ | docker build --target dev -t katalog-web:dev . agar test baru masuk image dev | rebuild runtime — stage test membaca salinan test terbaru |
package.json berubah | aturan rebuild pada develop.watch mengerjakannya | rebuild runtime — dependensi produksi dihitung ulang |
Dockerfile berubah | aturan rebuild pada develop.watch mengerjakannya | rebuild runtime |
compose.yaml dan kawan-kawannya | tanpa rebuild — cukup docker compose up -d | tanpa rebuild — konfigurasi bukan isi image |
Mari kita telaah dua kebiasaan yang dituntut tabel di atas. Pertama, kolom development hampir seluruhnya bertuliskan “tanpa rebuild” — seluruh perangkat section ini memang dirancang agar hari-hari pengeditan tidak menyentuh builder; hanya kelas berkas yang menuntut resep baru yang bergerak. Kedua, angka versi pada rilis sebaiknya naik setiap kali image produksi dibangun — 2.0, 2.1, 2.2 — sehingga rollback selalu punya alamat pasti untuk kembali; strategi penamaannya kita bedah tuntas pada Chapter Strategi Versioning Image.
Dua image kini tertata: yang gemuk dan bersenjata lengkap untuk meja kerja, yang ramping dan berhealthcheck untuk server. Namun disiplin image tidak pernah menjamin aplikasi bebas galat — container tetap akan mati, berkas tetap akan hilang, memori tetap akan habis, dan bug logika tetap akan bersembunyi di jalur yang jarang dilalui. Saat-saat itu tiba, kita butuh kemampuan yang berbeda: membuka kotak dari luar tanpa membongkarnya. docker compose logs, exec, inspect, dan debugger yang menempel dari jauh adalah jendela-jendela itu — dan cara memakainya pada empat kecelakaan klasik adalah materi Chapter Debugging Container.
Referensi #
- Multi-Stage Builds
— pola stage bertingkat, klausa
AS, dan pemilihan target lewat--target. - Dockerfile Reference
— sintaks
FROM ... AS,COPY --from,HEALTHCHECK, dan semantik exec form. - build — Compose file reference
— sub-kunci
targetuntuk memilih stage saat Compose membangun image. - Dockerfile Best Practices — panduan memilih basis dan menata stage untuk image produksi yang ramping.