HEALTHCHECK menutup instruksi runtime yang paling terasa dampaknya: image myapp kini melaporkan kesehatannya sendiri dan kolom STATUS berhenti menyampaikan kabar baik yang palsu. Tiga instruksi terakhir di peta tujuh belas jarang muncul di Dockerfile sehari-hari, tetapi masing-masing menyimpan pelajaran yang pantas dipahami: STOPSIGNAL mengubah sinyal yang dikirim docker stop, SHELL mengganti program perantara untuk bentuk shell form, dan ONBUILD menyimpan instruksi yang baru menyala ketika image kita dipakai orang lain sebagai basis. Setelah ketiganya kita bedah, kita rangkai seluruh section dengan satu Dockerfile final untuk myapp — gabungan seluruh pola yang telah kita kumpulkan sepanjang sepuluh chapter.

STOPSIGNAL Mengubah Sinyal Berhenti #

Mekanisme docker stop sudah kita bedah pada Chapter Menghentikan dan Menjalankan Container: daemon mengirim SIGTERM ke PID 1, menunggu dengan masa tenggang 10 detik, lalu menurunkan SIGKILL bila proses tetap ngotot. Catatan pada chapter itu juga sempat menyinggung bahwa sinyal pertama bisa ditimpa oleh image — dan instruksinya adalah STOPSIGNAL. Contoh paling terkenal ada pada image nginx, yang sudah lama tinggal di mesin latihan kita:

bash
$ docker image inspect nginx:alpine --format '{{.Config.StopSignal}}'

Output yang Dihasilkan:

text
SIGQUIT

Mari kita telaah makna satu kata di atas:

  1. SIGQUIT, bukan SIGTERM — image nginx (varian alpine yang kita pakai menunjuk rilis 1.29) menetapkan sinyal berhenti sendiri; setiap docker stop pada container nginx mengirim SIGQUIT kepada master prosesnya.
  2. Jalur shutdown yang lebih rapi milik nginx — master nginx menanggapi SIGQUIT dengan mematikan para worker secara berurutan sambil menyelesaikan permintaan yang sedang dilayani — inilah shutdown graceful versi nginx; SIGTERM justru memotong lebih cepat.
  3. Masa tenggang tetap berlaku — STOPSIGNAL hanya mengganti sinyal yang dikirim, bukan mekanisme tunggunya; lewat 10 detik tanpa hasil, SIGKILL tetap turun.

Kapan aplikasi kita membutuhkan instruksinya? Node menangani SIGTERM dengan baik sehingga myapp nyaman dengan nilai bawaan. Pola pemakaiannya umumnya dua arah: aplikasi yang jalur bersihnya berada pada sinyal lain — misalnya rutinitas pembersihan SIGTERM yang lambat sementara SIGINT membawa aplikasi ke jalur cepat yang tetap menutup koneksi dengan rapi — atau image yang ingin menuliskan kontrak secara eksplisit. Bentuk sintaksnya STOPSIGNAL SIGQUIT dengan nilai berupa nama sinyal atau angka; SIGTERM misalnya adalah angka 15.

SHELL untuk Bentuk Shell Form #

Setiap instruksi yang ditulis dalam bentuk shell form — RUN apk add curl, CMD node server.js — tidak dieksekusi langsung oleh builder. Builder memanggil program perantara: di image berbasis Linux, perantaranya adalah ["/bin/sh", "-c"]. SHELL mengganti perantara itu untuk semua instruksi shell form setelah barisnya. Kebutuhan paling klasiknya ada di image dasar Windows yang perantaranya ["cmd", "/S", "/C"] — pola lama menggantinya dengan PowerShell:

dockerfile
SHELL ["powershell", "-Command"]
RUN Get-Date

Mari kita telaah blok kecil di atas:

  1. Bentuk wajib exec form — nilai SHELL ditulis sebagai array JSON; instruksi ini tidak menerima bentuk shell.
  2. RUN Get-Date kini dieksekusi PowerShell — tanpa SHELL, baris yang sama akan dijalankan cmd; hasilnya baris perintah PowerShell menjadi sah dipakai pada instruksi berikutnya.
  3. Pemakaian di Linux lebih jarang — kebutuhan khasnya misalnya mengaktifkan pipefail pada image Debian dengan SHELL ["/bin/bash", "-o", "pipefail", "-c"], supaya kegagalan di tengah pipa perintah panjang tidak tertelan oleh sukses perintah terakhir.
  4. Exec form tidak tersentuh — CMD ["node", "server.js"] tetap dieksekusi langsung tanpa perantara; SHELL hanya mengurus bentuk shell form.

ONBUILD Menyimpan Instruksi untuk Image Anak #

ONBUILD adalah satu-satunya instruksi yang tidak berbuat apa pun ketika image miliknya dibangun. ONBUILD COPY . . hanya menyimpan pemicu (trigger) pada metadata image. Pemicu itu baru menyala ketika image lain dibangun dengan FROM menunjuk image ini — instruksi yang tersimpan dieksekusi seolah-olah ditulis di Dockerfile si anak:

dockerfile
FROM node:22-alpine
WORKDIR /app
ONBUILD COPY package.json server.js ./
ONBUILD CMD ["node", "server.js"]

Dockerfile anak cukup satu baris:

dockerfile
FROM myapp:induk

Build image anak akan menjalankan COPY package.json server.js ./ dan CMD ["node", "server.js"] yang tersimpan di metadata induk — Dockerfile anak tidak menampilkan instruksi itu sama sekali. Pola semacam ini dahulu populer untuk image dasar framework: pembuat framework menyalakan salinan kode otomatis di setiap proyek turunan.

Rekap Tujuh Belas Instruksi #

Sepuluh chapter section ini telah menyusuri hampir seluruh instruksi Dockerfile. Tabel berikut merangkum semuanya — kapan dipakai dan jebakan yang paling sering menjebak — sebagai peta penutup sebelum kita merakit Dockerfile final:

InstruksiKapan dipakaiJebakan utama
FROMSetiap Dockerfile, awal tiap stageTag mengambang latest bisa diam-diam berpindah isi
RUNMemasang paket dan mengubah filesystem saat buildSatu instruksi satu layer; cache paket dibersihkan di instruksi yang sama
COPYMenyalin berkas dari build contextCOPY . . membawa berkas asing tanpa .dockerignore
ADDEkstraksi arsip tar lokal otomatisPerilaku implisit; jangan dipakai mengunduh URL
ENVVariabel yang menempel hingga runtimeTerbaca docker inspect; jangan menyimpan rahasia
ARGParameter build yang berubah per buildNilai terekam di history; bisa bocor ke siapa saja
CMDPerintah bawaan yang mudah ditimpaShell form tidak meneruskan sinyal ke aplikasi
ENTRYPOINTPerintah utama container yang stabilLupa exec form membuat sinyal berhenti di shell
WORKDIRMenetapkan direktori kerja instruksi berikutnyaTanpa WORKDIR, berkas mendarat di /
USERIdentitas non-root untuk build dan runtimeUser harus dibuat dulu; urutan setelah instalasi paket
EXPOSEDokumentasi port aplikasiTidak menerbitkan port — miskonsepsi klasik
VOLUMEMenandai titik data layak-pisahVolume anonim menumpuk diam-diam
LABELMetadata key-value, standar OCIMAINTAINER deprecated; kunci bebas tak dibaca tool
HEALTHCHECKProbe kesehatan bawaan imageAlat probe harus ada di dalam image
STOPSIGNALMengubah sinyal docker stopAplikasi harus benar-benar menangani sinyal itu
SHELLMengganti perantara shell formHanya menyentuh bentuk shell form
ONBUILDInstruksi yang menyala saat jadi basisMembingungkan; multi-stage lebih baik

Dockerfile Final myapp 2.0 #

Saatnya merakit semuanya. Budi menyusun Dockerfile final yang menggabungkan seluruh pola section ini — parser directive, basis terpin, label OCI, WORKDIR, environment, user non-root, salinan manifest-dulu, healthcheck, sinyal berhenti eksplisit, dan CMD exec form — lalu menaikkan versi menjadi 2.0:

dockerfile
# syntax=docker/dockerfile:1

# Basis dipin eksplisit: Node.js 22 di atas Alpine
FROM node:22-alpine

# Metadata standar OCI
LABEL org.opencontainers.image.title="myapp" \
      org.opencontainers.image.description="API kecil milik Budi" \
      org.opencontainers.image.version="2.0" \
      org.opencontainers.image.authors="Budi <[email protected]>" \
      org.opencontainers.image.source="git.example.com/budi/myapp" \
      org.opencontainers.image.licenses="MIT"

# Direktori kerja semua instruksi berikutnya
WORKDIR /app

# Environment runtime aplikasi
ENV NODE_ENV=production

# User non-root dibuat selagi build masih berjalan sebagai root
RUN addgroup -g 1000 app && adduser -u 1000 -G app -H -D app

# Manifest dulu supaya cache instalasi bertahan saat kode berubah
COPY --chown=1000:1000 package.json ./

# Kode aplikasi paling belakang karena paling sering berubah
COPY --chown=1000:1000 server.js ./

# Semua instruksi selanjutnya berjalan sebagai user app
USER app

# Port yang didengarkan aplikasi
EXPOSE 8000

# Probe kesehatan bawaan image
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD wget -qO- localhost:8000/ || exit 1

# Sinyal berhenti yang diharapkan aplikasi
STOPSIGNAL SIGTERM

# Perintah bawaan container, exec form
CMD ["node", "server.js"]

Mari kita telaah Dockerfile final tersebut blok demi blok:

  1. # syntax=docker/dockerfile:1 — parser directive yang memastikan frontend Dockerfile modern membaca berkas ini; posisinya wajib di baris paling atas, persis aturan dari Chapter Struktur Dockerfile.
  2. FROM node:22-alpine — basis dipin ke versi mayor Node 22 di atas Alpine, bukan tag mengambang; bila ingin kaku total, tag bisa dikunci lebih jauh dengan digest — materi Chapter Image Digest.
  3. LABEL org.opencontainers.image.* — enam pasangan metadata standar OCI menggantikan label bebas; title, version, dan source terbaca registry serta tool audit.
  4. WORKDIR /app dan ENV NODE_ENV=production — alamat rumah dan environment produksi untuk seluruh instruksi setelahnya.
  5. RUN addgroup ... && adduser ... — user app UID 1000 dibuat selagi build masih root; seluruh pekerjaan ber-akses-root berakhir di baris ini.
  6. COPY --chown=1000:1000 package.json ./ lalu server.js ./ — salinan manifest dulu dan kode belakang, pola dari Chapter COPY dan ADD; flag --chown memindahkan kepemilikan ke UID 1000 tanpa layer tambahan, menyambung konsistensi UID dari Chapter WORKDIR dan USER.
  7. USER app — sejak baris ini, instruksi apa pun — termasuk node server.js saat container menyala — berjalan tanpa wewenang root.
  8. EXPOSE 8000 — dokumentasi port yang kini kita tulis dengan kesadaran penuh bahwa penerbitan tetap urusan -p dan -P saat runtime.
  9. HEALTHCHECK ... wget -qO- localhost:8000/ || exit 1 — probe bawaan image dengan interval 30 detik, timeout 3 detik, masa hangat 5 detik, dan 3 percobaan; exit code 0 melaporkan sehat, 1 melaporkan sakit.
  10. STOPSIGNAL SIGTERM — kontrak eksplisit bahwa aplikasi ini menanti SIGTERM; nilainya sama dengan bawaan, tetapi penulisan eksplisitnya memberi tahu pembaca image bahwa sinyal ini memang disengaja.
  11. CMD ["node", "server.js"] — perintah bawaan dalam exec form, bentuk yang meneruskan sinyal langsung ke proses node.

Budi membangunnya:

bash
$ docker build -t myapp:2.0 .

Output yang Dihasilkan:

text
[+] Building 4.8s (18/18) FINISHED
 => [internal] load build definition from Dockerfile                    0.1s
 => => transferring dockerfile: 1301B                                   0.0s
 => [internal] load metadata for docker.io/library/node:22-alpine       0.7s
 => [1/11] FROM docker.io/library/node:22-alpine                        0.0s
 => => resolve docker.io/library/node:22-alpine                         0.7s
 => [2/11] LABEL org.opencontainers.image.title=myapp ...               0.0s
 => [3/11] WORKDIR /app                                                 0.0s
 => [4/11] ENV NODE_ENV=production                                      0.0s
 => [5/11] RUN addgroup -g 1000 app && adduser -u 1000 -G app -H -D app 0.5s
 => [6/11] COPY --chown=1000:1000 package.json ./                       0.1s
 => => [7/11] COPY --chown=1000:1000 server.js ./                        0.1s
 => => transferring context: 1958B                                      0.0s
 => [8/11] USER app                                                     0.0s
 => [9/11] EXPOSE 8000                                                  0.0s
 => [10/11] HEALTHCHECK &{["CMD-SHELL" "wget -qO- localhost:8000/ || exit 1"] 30s 3s 5s 0s 3 false} 0.0s
 => [11/11] STOPSIGNAL SIGTERM                                          0.0s
 => exporting to image                                                  0.2s
 => => exporting layers                                                 0.1s
 => => writing image sha256:d94c1f8b2a6e7b0d4c9f2a5e8b1d3f6c0a7e9b2d4f8a1c5e3b7d9f0a2c4e6b8d 0.1s
 => => naming to docker.io/library/myapp:2.0                            0.0s

Mari kita telaah beberapa kejadian penting pada build di atas:

  1. Seluruh rantai dieksekusi ulang sejak LABEL — isi label berubah karena versi naik 1.6 menjadi 2.0, dan perubahan satu instruksi membatalkan cache semua instruksi setelahnya dalam stage yang sama, persis pola yang kita saksikan pada Chapter COPY dan ADD; cara menyusun Dockerfile agar dampaknya minimal adalah materi Chapter Build Cache.
  2. transferring context: 1958B — berkas catatan.txt peninggalan eksperimen lama masih menetap di folder Budi dan ikut terkirim ke builder; nilainya tidak masuk image karena COPY menyebut berkas secara eksplisit, dan cara menyaring context lebih ketat menanti Chapter .dockerignore.
  3. COPY dipecah dua langkah — manifest di [6/11], kode di [7/11]; begitu server.js berubah, hanya langkah itu dan sesudahnya yang dibangun ulang.
  4. HEALTHCHECK dan STOPSIGNAL tampil sebagai langkah — keduanya instruksi metadata yang tetap dihitung sebagai langkah build, sementara CMD seperti biasa dicatat saat ekspor tanpa nomor langkah.

Waktu pengujian tiba. Budi membuang container lama supaya port 8000 kosong, lalu menjalankan image final:

bash
$ docker run -d --name myapp-final -p 8000:8000 myapp:2.0

Output yang Dihasilkan:

text
8e2b7d4a1c9f0a5b3d6e8c1a4f7b2d9e5c0a3b6d8f2a4c7e1b5d9f0a3c6e8b1a
bash
$ curl localhost:8000

Output yang Dihasilkan:

text
{"app":"myapp","pesan":"Halo dari container"}

Setelah interval pertama berlalu, seluruh janji Dockerfile ini terbaca pada satu baris status:

bash
$ docker ps

Output yang Dihasilkan:

text
CONTAINER ID   IMAGE       COMMAND          CREATED         STATUS                    PORTS                                       NAMES
8e2b7d4a1c9f   myapp:2.0   "node server.js" 41 seconds ago  Up 40 seconds (healthy)   0.0.0.0:8000->8000/tcp, [::]:8000->8000/tcp   myapp-final

Dua pemeriksaan kecil menutup verifikasi — identitas proses dan kontrak sinyal:

bash
$ docker exec myapp-final whoami

Output yang Dihasilkan:

text
app
bash
$ docker image inspect myapp:2.0 --format '{{.Config.StopSignal}}'

Output yang Dihasilkan:

text
SIGTERM

Perjalanan panjang section ini bermula dari satu Dockerfile lima baris yang kita tulis begitu saja; kini setiap baris Dockerfile final memiliki alasan yang bisa kita jelaskan — dari directive hingga exec form. Sebagai penutup yang manis, koleksi image myapp di mesin Budi kini membentuk garis waktu perjalanan itu:

bash
$ docker images myapp

Output yang Dihasilkan:

text
REPOSITORY   TAG     IMAGE ID       CREATED              SIZE
myapp        2.0     d94c1f8b2a6e   30 seconds ago       130MB
myapp        1.6     a2f74c9e3b51   25 minutes ago       130MB
myapp        1.5     4e8d1a6c92f7   About an hour ago    130MB
myapp        1.4     7b3e9f2c81d4   2 hours ago          130MB
...

Output di atas terpotong — tag-tag myapp lain dari perjalanan section ini tidak ditampilkan. Namun menulis Dockerfile hanyalah separuh keterampilan; separuh lainnya adalah memahami mesin yang mengeksekusinya. Mengapa seluruh isi context dikirim ke builder padahal hanya sebagian kecil disalin, bagaimana cache dihitung dan kapan ia batal, bagaimana multi-stage memangkas image produksi menjadi sepersekian ukurannya — semuanya itu adalah urusan Section Image Build dan Optimisasi, yang membuka dengan Chapter docker build.

Referensi #