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:

AspekTag postgres:18-alpineDigest postgres@sha256:…
Penunjuknyalabel yang bisa dipindahkan penerbitnyaisi image yang terkunci permanen
Mutabilitasbisa bergeser diam-diam antar build penerbittidak pernah berubah selama-lamanya
Keterbacaanversi dan varian terbaca jelasderetan 64 karakter heksadesimal
Kapan dipakaipengembangan dan latihanproduksi, audit, dan reproduksibilitas

Bentuk digest ditulis langsung pada nilai image bila kita ingin mengunci mutlak:

yaml
  db:
    image: postgres@sha256:1b50fd69a38322b7f1e2f3f2c9a2f370703d5718b327738152775c8a89df45a2

Nilai digest itu bukan karangan — cara mengambilnya sama persis dengan yang kita praktikkan pada Chapter Image Digest:

bash
$ docker image inspect --format '{{index .RepoDigests 0}}' postgres:18-alpine

Output yang Dihasilkan:

text
postgres@sha256:1b50fd69a38322b7f1e2f3f2c9a2f370703d5718b327738152775c8a89df45a2

Berkas 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:

text
katalog/
├── compose.yaml
├── Dockerfile
├── package.json
└── server.js

Tiga 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:

dockerfile
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:

  1. 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 pola FROM image@sha256:… dari Chapter Reproducible Build.
  2. WORKDIR /app — direktori kerja untuk seluruh instruksi berikutnya, sekaligus direktori tempat aplikasi hidup di dalam container.
  3. COPY package.json ./ — manifest dependensi disalin lebih dulu, terpisah dari kode; urutan ini disengaja agar layer npm install tetap ter-cache ketika hanya server.js yang berubah — strategi cache yang kita bedah pada Chapter Build Cache.
  4. RUN npm install — memasang dependensi berdasarkan manifest; untuk latihan ini npm install sudah memadai, sementara pipeline yang menuntut kepastian memakai npm ci persis seperti contoh-contoh Section Image Build dan Optimisasi.
  5. COPY server.js ./ — kode aplikasi masuk paling akhir karena ia adalah berkas yang paling sering berubah.
  6. USER node — proses turun dari root ke user node bawaan image, UID 1000, melunasi prinsip identitas minimal yang kita bedah pada Chapter WORKDIR dan USER dan User dan Permissions.
  7. EXPOSE 3000 — catatan metadata bahwa aplikasi mendengarkan port 3000; ia bukan penerbitan port, dan tugas menerbitkannya tetap pada ports di compose.yaml.
  8. CMD ["node", "server.js"] — perintah bawaan dalam exec form sehingga sinyal SIGTERM dari docker stop sampai langsung ke proses Node — perilaku yang kita buktikan pada Chapter CMD dan ENTRYPOINT.

Di compose.yaml, service web berganti rupa:

yaml
  web:
    build: .
    image: katalog-web:dev
    ports:
      - "8080:3000"
    restart: unless-stopped

Tiga 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:

KunciBentuk pendekBentuk panjangPeran
contextbuild: .build: {context: .}folder yang dikirim ke builder sebagai context
dockerfilemengikuti nama bawaan Dockerfilebuild: {dockerfile: Dockerfile.dev}nama berkas resep dalam context
argstidak tersediabuild: {args: …}build-time variable seperti flag --build-arg
targettidak tersediabuild: {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:

yaml
  web:
    build:
      context: .
      dockerfile: Dockerfile.dev
      args:
        VERSI_APP: 1.2.0
      target: dev
    image: katalog-web:dev

Mari 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:

bash
$ docker compose build

Output yang Dihasilkan:

text
[+] 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.0s

Output itu adalah output BuildKit yang sudah akrab dari Section Image Build dan Optimisasi — Compose hanyalah penggeraknya. Mari kita telaah bagian yang spesifik Compose:

  1. [+] Building 24.1s (14/14) FINISHED — ringkasan build dengan status akhir FINISHED; empat belas langkah meliputi langkah internal, resolusi metadata, tujuh langkah bernomor dari FROM sampai EXPOSE, dan ekspor image.
  2. [4/7] RUN npm install selama 9.4s — langkah termahal; berkat urutan COPY package.json yang terpisah, langkah ini akan ter-cache pada build berikutnya selama manifest tidak berubah.
  3. naming to docker.io/library/katalog-web:dev — nama dari kunci image: ditempelkan pada hasil build; tanpa kunci itu, Compose menamai image secara otomatis mengikuti pola <project>-<service>, sehingga image web kita akan bernama katalog-web tanpa tag.
  4. Ukuran context 1.19MB — kecil karena context hanya berisi empat berkas; kontras dengan 148MB pada chapter Build Context dulu, sebelum .dockerignore disiplinkan.

Hasil akhirnya menempati penyimpanan lokal seperti biasa:

bash
$ docker images katalog-web

Output yang Dihasilkan:

text
REPOSITORY     TAG      IMAGE ID       CREATED         SIZE
katalog-web    dev      7e1c94b0a2f5   1 minute ago    158MB

Ukuran 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:

bash
$ docker compose up -d

Output yang Dihasilkan:

text
[+] Running 4/4
 ✔ Container katalog-db-1 Running                                     0.0s
 ✔ Container katalog-web-1 Recreated                                  0.3s
 ✔ Container katalog-web-1 Started                                    0.4s

Perintah 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:

bash
$ docker compose up -d

Output yang Dihasilkan:

text
[+] Running 2/2
 ✔ Container katalog-db-1 Running                                     0.0s
 ✔ Container katalog-web-1 Running                                    0.0s
bash
$ curl localhost:8080

Output yang Dihasilkan:

text
{"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:

bash
$ docker compose up -d --build

Output yang Dihasilkan:

text
[+] 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.4s

Baris-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:

bash
$ curl localhost:8080

Output yang Dihasilkan:

text
{"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:

NilaiPerilaku ComposeKapan dipakai
missingtarik hanya bila image belum ada lokal — bawaanpengembangan sehari-hari
alwaysrujuk registry setiap up, walau image sudah adapipeline CI yang ingin selalu mutakhir
neverjangan menarik — berkas gagal bila image tidak tersediamesin offline atau image privat lokal
buildbangun dari Dockerfile, jangan menarik dari registryiterasi 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:

bash
$ docker compose pull db

Output yang Dihasilkan:

text
[+] Pulling 1/1
 ✔ db Pulled                                                          1.9s

Perintah 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:

AktivitasPerintahArah pergerakan
Builddocker compose buildDockerfile dan context menjadi image lokal
Pushdocker pushimage lokal naik ke registry
Pulldocker compose pullimage 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 #