Dua service katalog kini punya cetakbiru yang semakin kaya, tetapi service web masih memakai pengganti sementara: image nginx yang tidak berisi aplikasi apa pun milik proyek kita. Bab ini melunasi janji pembuka section — mengganti pengganti itu dengan aplikasi katalog yang sungguhan, dibangun dari Dockerfile di dalam folder proyek lalu diberi nama stabil lewat kombinasi kunci build dan image. Perjalanan menuntut tiga pengetahuan sekaligus: dari mana image diperoleh (pull otomatis dari registry), bagaimana image sendiri dibangun (build dari Dockerfile), dan kapan Compose memutuskan untuk menarik atau membangun ulang (pull policy) — termasuk satu kebiasaan yang paling sering menjebak pemula: up tidak membangun ulang image yang sudah ada.
image: dan Pull Otomatis #
Kunci image menyatakan referensi image siap pakai — persis referensi yang kita kuasai di Section Docker Image. Ketika Compose menemukan image: postgres:18-alpine dan image itu belum tersimpan lokal, Compose menariknya lebih dulu tanpa diminta; itulah blok [+] Pulling 2/2 yang menyertai up pertama di laptop Siti pada chapter pembuka. Perilaku bawaannya sama dengan docker run: cek lokal, tarik bila kosong, lalu lanjut — kebijakan yang bernama missing dan yang kolom tabelnya menanti di bagian akhir chapter.
Referensi itu sendiri punya dua bentuk, dan pilihan di antara keduanya adalah keputusan arsitektur yang sudah pernah kita bawa pulang dari Chapter Image Digest:
| Aspek | Tag postgres:18-alpine | Digest postgres@sha256:… |
|---|---|---|
| Penunjuknya | label yang bisa dipindahkan penerbitnya | isi image yang terkunci permanen |
| Mutabilitas | bisa bergeser diam-diam antar build penerbit | tidak pernah berubah selama-lamanya |
| Keterbacaan | versi dan varian terbaca jelas | deretan 64 karakter heksadesimal |
| Kapan dipakai | pengembangan dan latihan | produksi, audit, dan reproduksibilitas |
Bentuk digest ditulis langsung pada nilai image bila kita ingin mengunci mutlak:
db:
image: postgres@sha256:1b50fd69a38322b7f1e2f3f2c9a2f370703d5718b327738152775c8a89df45a2Nilai digest itu bukan karangan — cara mengambilnya sama persis dengan yang kita praktikkan pada Chapter Image Digest:
$ docker image inspect --format '{{index .RepoDigests 0}}' postgres:18-alpineOutput yang Dihasilkan:
postgres@sha256:1b50fd69a38322b7f1e2f3f2c9a2f370703d5718b327738152775c8a89df45a2Berkas katalog kita tetap memakai tag postgres:18-alpine demi keterbacaan latihan; begitu stack ini pindah ke produksi, mengganti nilai itu menjadi referensi digest adalah cara termurah mengunci seluruh lingkungan.
build: Aplikasi katalog yang Sungguhan #
Sekarang bagian besarnya: service web berhenti meminjam image orang lain dan mulai membangun imagenya sendiri. Budi menumbuhkan folder proyek katalog dengan tiga berkas baru di samping compose.yaml:
katalog/
├── compose.yaml
├── Dockerfile
├── package.json
└── server.jsTiga pendatang baru itu saling menerangkan. package.json memuat satu dependensi — web framework express — beserta metadata proyek; server.js adalah aplikasi kecil berbasis Node.js yang mendengarkan port 3000 dan menjawab setiap permintaan dengan JSON berisi nama aplikasi dan nomor versi; sedangkan Dockerfile adalah resep mengemas keduanya menjadi image:
FROM node:22-alpine
WORKDIR /app
COPY package.json ./
RUN npm install
COPY server.js ./
USER node
EXPOSE 3000
CMD ["node", "server.js"]Delapan baris itu meliputi seluruh kebiasaan baik yang kita bangun di Section Dockerfile. Mari kita telaah baris demi baris:
FROM node:22-alpine— basis Node.js 22 varian Alpine yang sama dengan seluruh build Course ini; mengunci lebih jauh dengan digest tetap opsional lewat polaFROM image@sha256:…dari Chapter Reproducible Build.WORKDIR /app— direktori kerja untuk seluruh instruksi berikutnya, sekaligus direktori tempat aplikasi hidup di dalam container.COPY package.json ./— manifest dependensi disalin lebih dulu, terpisah dari kode; urutan ini disengaja agar layernpm installtetap ter-cache ketika hanyaserver.jsyang berubah — strategi cache yang kita bedah pada Chapter Build Cache.RUN npm install— memasang dependensi berdasarkan manifest; untuk latihan ininpm installsudah memadai, sementara pipeline yang menuntut kepastian memakainpm cipersis seperti contoh-contoh Section Image Build dan Optimisasi.COPY server.js ./— kode aplikasi masuk paling akhir karena ia adalah berkas yang paling sering berubah.USER node— proses turun dari root ke usernodebawaan image, UID 1000, melunasi prinsip identitas minimal yang kita bedah pada Chapter WORKDIR dan USER dan User dan Permissions.EXPOSE 3000— catatan metadata bahwa aplikasi mendengarkan port 3000; ia bukan penerbitan port, dan tugas menerbitkannya tetap padaportsdi compose.yaml.CMD ["node", "server.js"]— perintah bawaan dalam exec form sehingga sinyalSIGTERMdaridocker stopsampai langsung ke proses Node — perilaku yang kita buktikan pada Chapter CMD dan ENTRYPOINT.
Di compose.yaml, service web berganti rupa:
web:
build: .
image: katalog-web:dev
ports:
- "8080:3000"
restart: unless-stoppedTiga perubahannya: build: . menyatakan image web dibangun dari Dockerfile di folder yang sama dengan compose.yaml; image: katalog-web:dev memberi nama pada hasil build; dan pemetaan port bergeser menjadi "8080:3000" karena aplikasi mendengarkan 3000, bukan 80 milik nginx.
Bentuk Pendek dan Panjang build #
Nilai build: . adalah bentuk pendek dari sebuah pemetaan. Kedua bentuk ini sah, dan bentuk panjangnya membuka tiga kunci tambahan:
| Kunci | Bentuk pendek | Bentuk panjang | Peran |
|---|---|---|---|
context | build: . | build: {context: .} | folder yang dikirim ke builder sebagai context |
dockerfile | mengikuti nama bawaan Dockerfile | build: {dockerfile: Dockerfile.dev} | nama berkas resep dalam context |
args | tidak tersedia | build: {args: …} | build-time variable seperti flag --build-arg |
target | tidak tersedia | build: {target: …} | stage yang dihentikan pada multi-stage build |
Bentuk panjang dipakai saat satu proyek menyimpan lebih dari satu Dockerfile — misalnya resep development yang berbeda dari produksi:
web:
build:
context: .
dockerfile: Dockerfile.dev
args:
VERSI_APP: 1.2.0
target: dev
image: katalog-web:devMari kita telaah empat kunci bentuk panjang tersebut: context: . tetap menunjuk folder yang sama dengan compose.yaml; dockerfile: Dockerfile.dev meminta builder membaca resep dari berkas lain di dalam context itu; args: menyuntikkan VERSI_APP sebagai variable build-time yang terbaca instruksi ARG — mekanisme yang kita bedah pada Chapter ENV dan ARG; dan target: dev menghentikan build pada stage bernama dev bila resepnya multi-stage, senjata yang kita asah pada Chapter Multi-Stage Build. Proyek katalog kita sendiri cukup bentuk pendek build: . karena satu Dockerfile melayani seluruh kebutuhan.
Menempel Nama pada Hasil Build #
Kombinasi build dan image layak dijadikan kebiasaan tetap:
Budi membangun imagenya dengan perintah build milik Compose:
$ docker compose buildOutput yang Dihasilkan:
[+] Building 24.1s (14/14) FINISHED
=> [internal] load build definition from Dockerfile 0.1s
=> => transferring dockerfile: 238B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/node:22-alpine 1.2s
=> [auth] node:22-alpine pull token from registry-1.docker.io 0.9s
=> [internal] load build context 0.1s
=> => transferring context: 1.19MB 0.1s
=> [1/7] FROM docker.io/library/node:22-alpine 0.0s
=> => resolve docker.io/library/node:22-alpine 1.2s
=> [2/7] WORKDIR /app 0.0s
=> [3/7] COPY package.json ./ 0.1s
=> [4/7] RUN npm install 9.4s
=> [5/7] COPY server.js ./ 0.0s
=> [6/7] USER node 0.0s
=> [7/7] EXPOSE 3000 0.0s
=> exporting to image 0.4s
=> => exporting layers 0.2s
=> => writing image sha256:7e1c94b0a2f5d8e3c6a9b4d1e8f2c5a7b9d3e6f1c8a2b4d6e9f1c3a5b7d9e2f4 0.1s
=> => naming to docker.io/library/katalog-web:dev 0.0sOutput itu adalah output BuildKit yang sudah akrab dari Section Image Build dan Optimisasi — Compose hanyalah penggeraknya. Mari kita telaah bagian yang spesifik Compose:
[+] Building 24.1s (14/14) FINISHED— ringkasan build dengan status akhir FINISHED; empat belas langkah meliputi langkah internal, resolusi metadata, tujuh langkah bernomor dariFROMsampaiEXPOSE, dan ekspor image.[4/7] RUN npm installselama 9.4s — langkah termahal; berkat urutanCOPY package.jsonyang terpisah, langkah ini akan ter-cache pada build berikutnya selama manifest tidak berubah.naming to docker.io/library/katalog-web:dev— nama dari kunciimage:ditempelkan pada hasil build; tanpa kunci itu, Compose menamai image secara otomatis mengikuti pola<project>-<service>, sehingga image web kita akan bernamakatalog-webtanpa tag.- Ukuran context 1.19MB — kecil karena context hanya berisi empat berkas; kontras dengan 148MB pada chapter Build Context dulu, sebelum
.dockerignoredisiplinkan.
Hasil akhirnya menempati penyimpanan lokal seperti biasa:
$ docker images katalog-webOutput yang Dihasilkan:
REPOSITORY TAG IMAGE ID CREATED SIZE
katalog-web dev 7e1c94b0a2f5 1 minute ago 158MBUkuran 158MB adalah angka yang wajar untuk aplikasi Node.js di atas node:22-alpine: basis ±130MB ditambah layer dependensi express. Image siap — kini service web bisa dinyalakan ulang dengan wajah barunya:
$ docker compose up -dOutput yang Dihasilkan:
[+] Running 4/4
✔ Container katalog-db-1 Running 0.0s
✔ Container katalog-web-1 Recreated 0.3s
✔ Container katalog-web-1 Started 0.4sPerintah up ini sebenarnya juga sanggup membangun image bila katalog-web:dev belum ada — Compose memeriksa kunci build saat image yang dibutuhkan tidak ditemukan. Namun batas perilakunya penting dan menjadi jebakan klasik.
up Tidak Membangun Ulang Otomatis #
Budi menambah fitur kecil: nomor versi pada server.js dinaikkan menjadi 1.1.0, lalu ia menjalankan up seperti biasa dan memeriksa hasilnya:
$ docker compose up -dOutput yang Dihasilkan:
[+] Running 2/2
✔ Container katalog-db-1 Running 0.0s
✔ Container katalog-web-1 Running 0.0s$ curl localhost:8080Output yang Dihasilkan:
{"app":"katalog","versi":"1.0.0"}Aplikasi masih menjawab versi lama. up melihat image katalog-web:dev sudah ada di lokal, sehingga container dianggap sudah sesuai deklarasi — perubahan pada server.js tidak pernah dibaca, karena yang dibandingkan adalah image, bukan isi folder.
Solusinya satu flag:
$ docker compose up -d --buildOutput yang Dihasilkan:
[+] Building 8.7s (14/14) FINISHED
=> [internal] load build definition from Dockerfile 0.1s
=> [4/7] RUN npm install 0.0s (cached)
=> [5/7] COPY server.js ./ 0.1s
...
=> => writing image sha256:9a2f61c3e8d4b5a7f1c6e9d2b4a8f3c1e5d7b9a2f4c6e8d1b3a5f7c9e2d4b6a8 0.1s
=> => naming to docker.io/library/katalog-web:dev 0.0s
[+] Running 3/3
✔ Container katalog-db-1 Running 0.0s
✔ Container katalog-web-1 Recreated 0.3s
✔ Container katalog-web-1 Started 0.4sBaris-baris internal lainnya dipotong agar ringkas. Mari kita telaah alurnya: build berjalan ulang dan menghasilkan image ID baru, RUN npm install selesai seketika karena ter-cache, image baru menimpa nama katalog-web:dev, lalu container web dibuat ulang — Recreated — dan menyala dengan isi yang segar:
$ curl localhost:8080Output yang Dihasilkan:
{"app":"katalog","versi":"1.1.0"}pull_policy: Menyetel Kapan Image Ditarik #
Kapan image harus ditarik ulang adalah kebijakan, dan Compose menyerahkan keputusannya pada kunci pull_policy di samping image:
| Nilai | Perilaku Compose | Kapan dipakai |
|---|---|---|
missing | tarik hanya bila image belum ada lokal — bawaan | pengembangan sehari-hari |
always | rujuk registry setiap up, walau image sudah ada | pipeline CI yang ingin selalu mutakhir |
never | jangan menarik — berkas gagal bila image tidak tersedia | mesin offline atau image privat lokal |
build | bangun dari Dockerfile, jangan menarik dari registry | iterasi aplikasi sendiri |
Service yang membangun imagenya sendiri dan memakai pull_policy: build misalnya menolak tersentuh registry sama sekali — berguna saat registry tak terjangkau tetapi builder lokal masih hidup. Pembandingnya juga berguna dilihat dari sisi tarik eksplisit:
$ docker compose pull dbOutput yang Dihasilkan:
[+] Pulling 1/1
✔ db Pulled 1.9sPerintah ini menarik image untuk service bernama db — atau seluruh service bila nama tidak disebut. Durasinya pendek karena manifest postgres:18-alpine di registry masih cocok dengan yang tersimpan lokal, sehingga tidak ada satu layer pun yang bergerak; angka itu akan membesar ketika penerbit merilis build baru dan layer-layer baru harus diunduh.
Alur Lokal dan Registry #
Tiga perintah yang kita pakai di chapter ini hanyalah dua sisi dari satu alur yang sama — pergerakan image antara mesin lokal dan registry:
| Aktivitas | Perintah | Arah pergerakan |
|---|---|---|
| Build | docker compose build | Dockerfile dan context menjadi image lokal |
| Push | docker push | image lokal naik ke registry |
| Pull | docker compose pull | image turun dari registry ke lokal |
Mari kita telaah diagram di atas: jalur kiri adalah pekerjaan Budi — membangun image di mesinnya sendiri; jalur kanan adalah kelanjutan yang menanti di Section Registry dan Distribusi — image yang sama didorong ke registry sehingga server produksi atau mesin rekan tim cukup menariknya, tanpa Dockerfile dan tanpa toolchain Node.js di mesin tujuan.
Stack katalog kini menjalankan aplikasi yang sesungguhnya di port 8080 dan PostgreSQL di belakangnya — tetapi kedua service itu masih berdiri berdampingan tanpa satu pun benang jaringan yang kita lihat: web belum tahu alamat db, dan pemetaan port masih ditulis dalam bentuk pendek yang menyembunyikan detailnya. Chapter Port dan Network menyambungkan keduanya — dari long syntax ports, expose, sampai DNS antar service lewat network bawaan project.
Referensi #
- build — Compose file reference — kunci build beserta bentuk pendek, bentuk panjang, dan seluruh sub-kuncinya.
- image — Compose file reference — referensi image pada service termasuk bentuk digest.
- Node.js — Docker Hub — halaman image resmi Node.js beserta varian alpine yang dipakai Dockerfile katalog.
- Docker Registry — konsep registry yang menjadi tujuan push dan sumber pull pada alur distribusi image.