VOLUME dan LABEL melengkapi peta hingga nyaris penuh: image kini tahu di mana datanya layak dipisahkan dan membawa identitas yang terbaca tool. Satu kebutuhan terakhir bersifat sama pentingnya tetapi beda wilayah: menjawab pertanyaan apakah aplikasi di dalam container masih sehat. Kolom STATUS pada docker ps hanya melaporkan satu hal — apakah proses utama masih hidup — sementara aplikasi bisa sakit jauh sebelum prosesnya mati: server menggantung tanpa menjawab, koneksi database terputus diam-diam, atau setiap permintaan dijawab galat 500. Budi mengalaminya sendiri ketika myapp yang ditinggal berjalan semalaman tampil Up 9 hours padahal permintaan ke aplikasinya menggantung tanpa jawaban. HEALTHCHECK adalah instruksi yang menanam cara pemeriksaan ke dalam image — sehingga status container melaporkan kesehatan aplikasi, bukan sekadar napas prosesnya.

Container Up Bukan Berarti Sehat #

Status Up punya definisi yang sempit dan jujur: proses PID 1 di dalam container belum berakhir. Daemon tidak tahu apa pun tentang isi proses itu — apakah server HTTP masih menerima koneksi, apakah worker masih mengurus antrian, apakah memori bocor perlahan. Aplikasi yang deadlock, kehabisan koneksi database, atau terjebak pada keadaan galat tetap menjaga PID 1 tetap hidup, dan docker ps dengan senyum tetap menulis Up. Operator baru sadar saat pengguna mulai menelepon.

Solusinya adalah probe: perintah kecil yang dijalankan berkala di dalam container untuk menanyakan langsung ke aplikasi, “masih sanggup melayani?”. Tanpa HEALTHCHECK, probe semacam itu harus dijalankan manual dari luar — misalnya lewat curl yang dijadwalkan di server monitoring. HEALTHCHECK memindahkan probe itu ke dalam image sehingga setiap container dari image tersebut otomatis diperiksa dengan cara yang sama, di mana pun container itu berjalan.

Sintaks HEALTHCHECK #

Bentuk lengkap instruksi ini pada Dockerfile terlihat seperti berikut:

dockerfile
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD wget -qO- localhost:8000/ || exit 1

Mari kita telaah setiap bagian di atas:

  1. CMD di akhir — menandai awal perintah probe; perintah inilah yang dijalankan daemon pada setiap interval.
  2. wget -qO- localhost:8000/ — probe yang meminta halaman utama myapp di port 8000 dari dalam container sendiri; wget menuliskan hasilnya ke stdout dan diam di bawah flag -q.
  3. || exit 1 — bila wget gagal, hasil probe diterjemahkan eksplisit menjadi exit code 1; pola ini penting saat probe berupa rangkaian perintah yang nilai akhirnya tidak selalu mencerminkan kegagalan.

Empat opsi di depan mengatur ritme pemeriksaan, dan semuanya punya nilai bawaan yang perlu dihafal:

OpsiArtiNilai bawaan
–intervalJeda antara dua pemeriksaan30s
–timeoutBatas waktu tiap pemeriksaan sebelum dianggap gagal30s
–start-periodMasa hangat; kegagalan di periode ini tidak dihitung0s
–retriesJumlah kegagalan beruntun sebelum status unhealthy3

Perhatikan nilai bawaan --timeout: 30 detik, bukan 3 detik. Probe yang menggantung akan menahan status pemeriksaan selama setengah menit sebelum dinyatakan gagal, sehingga kebanyakan image menuliskan nilai yang jauh lebih ketat seperti 3s supaya pemeriksaan lambat cepat terbaca sebagai kegagalan. Opsi --start-period adalah masa hangat mesin: aplikasi yang baru menyala kerap butuh waktu membuka koneksi database, memuat konfigurasi, atau menghangatkan cache. Selama periode ini status container tetap health: starting dan kegagalan probe tidak menambah penghitung kegagalan — aplikasi diberi kesempatan bangun sebelum dinilai.

Exit Code adalah Protokol Status #

Nilai kembali perintah probe dipakai daemon sebagai protokol status: exit code 0 berarti sehat, exit code 1 berarti tidak sehat. Nilai lain dipesan untuk keperluan internal dan sebaiknya tidak dihasilkan probe buatan kita. Perhatikan bahwa semantik ini lebih ketat daripada kebiasaan exit code pada skrip shell: skrip biasa memakai 0 untuk sukses dan angka apa pun untuk gagal, tetapi HEALTHCHECK hanya mengenal dua makna sah, dan probe yang keliru mengembalikan 2 tetap dibaca sebagai kegagalan tanpa pesan yang membantu.

Siklus satu pemeriksaan lengkap tergambar pada diagram berikut:

Diagram di atas memetakan keputusan yang diambil daemon pada setiap detakan interval. Mari kita telaah alurnya:

  1. Probe dieksekusi layaknya docker exec — daemon menjalankan perintah sebagai proses tambahan di dalam container, lalu menunggu nilai keluarnya sampai batas --timeout.
  2. Exit 0 — aplikasi dianggap sehat, penghitung kegagalan beruntun (FailingStreak) dikembalikan ke nol.
  3. Exit 1 berulang — setiap kegagalan menambah FailingStreak; begitu angka itu menyamai --retries, status resmi berubah menjadi unhealthy.
  4. Pemeriksaan tidak berhenti — setelah unhealthy, probe tetap berjalan pada setiap interval; bila kelak probe berhasil lagi, status kembali healthy karena FailingStreak direset.

Alat Probe Harus Ada di Dalam Image #

Perintah probe dieksekusi di dalam container, sehingga alat yang dipanggil harus tersedia di dalam image — daemon tidak membawa curl atau wget dari host. Kabar baik untuk kita: node:22-alpine berdiri di atas Alpine yang membawa busybox, dan busybox menyertakan wget, sehingga myapp bisa memeriksa dirinya tanpa memasang satu paket pun. Image berbasis Debian slim sering tidak membawa curl maupun wget; pilihannya hanya dua — memasang alat probe saat build, atau memakai runtime bahasa itu sendiri untuk mengeksekusi skrip pemeriksaan singkat lewat node. Probe yang memanggil alat yang tidak ada akan gagal berulang dan menarik status ke unhealthy; container yang sakit karena “perintah tidak ditemukan” adalah jebakan klasik yang hanya terlihat dari log pemeriksaan.

Demo myapp dengan Healthcheck Bawaan #

Budi menanamkan HEALTHCHECK pada Dockerfile myapp dan menaikkan versi menjadi 1.6. Ia membuang container latihan lama lebih dulu supaya port 8000 di host kosong:

dockerfile
# syntax=docker/dockerfile:1

# Basis: 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="1.6" \
      org.opencontainers.image.authors="Budi <[email protected]>" \
      org.opencontainers.image.source="git.example.com/budi/myapp" \
      org.opencontainers.image.licenses="MIT"

# Direktori kerja aplikasi
WORKDIR /app

# Buat user dan grup non-root selagi build masih berjalan sebagai root
RUN addgroup -g 1000 app && adduser -u 1000 -G app -H -D app

# Berkas aplikasi mendarat di /app
COPY package.json server.js ./

# Mulai baris ini semua instruksi 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

# Perintah bawaan container
CMD ["node", "server.js"]
bash
$ docker build -t myapp:1.6 .

Output yang Dihasilkan:

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

Dua kejanggalan tampilan pada output di atas perlu dijelaskan sebelum lanjut:

  1. Baris LABEL terpotong ... — BuildKit mencetak seluruh pasangan label dalam satu baris panjang; tampilan lengkapnya sama seperti yang sudah kita baca lewat inspect pada Chapter VOLUME dan LABEL.
  2. HEALTHCHECK &{...} — BuildKit merender instruksi ini sebagai struct internal: daftar perintah probe, lalu deretan nilai opsi kita (interval 30s, timeout 3s, start-period 5s, start-interval 0s yang tidak kita atur, retries 3). Format tampilan ini memang kurang ramah manusia; konfigurasi final yang mudah dibaca tetap berada di inspect, bukan di output build.

Budi menjalankan container lalu buru-buru melihat statusnya:

bash
$ docker run -d --name myapp-sehat -p 8000:8000 myapp:1.6

Output yang Dihasilkan:

text
6c1f8a2e9b4d7c3a5e0b2d9f4a6c8e1b3d5f7a9c2e4b6d8f0a2c4e6b8d1f3a5c
bash
$ docker ps

Output yang Dihasilkan:

text
CONTAINER ID   IMAGE       COMMAND          CREATED         STATUS                          PORTS                                       NAMES
6c1f8a2e9b4d   myapp:1.6   "node server.js" 18 seconds ago  Up 17 seconds (health: starting)   0.0.0.0:8000->8000/tcp, [::]:8000->8000/tcp   myapp-sehat

Status health: starting muncul karena pemeriksaan pertama baru dijadwalkan setelah --interval berlalu. Budi menunggu sebentar lalu memeriksa ulang:

bash
$ docker ps

Output yang Dihasilkan:

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

Kata healthy pada kolom STATUS adalah bukti probe pertama berhasil: wget menembak port 8000 dari dalam container dan aplikasi menjawab. Aplikasi itu sendiri tetap terbuka dari luar seperti biasa:

bash
$ curl localhost:8000

Output yang Dihasilkan:

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

Membaca Laporan State Health #

Status satu kata di docker ps hanyalah puncak gunung es; laporan lengkapnya hidup di field .State.Health pada inspect:

bash
$ docker inspect --format '{{json .State.Health}}' myapp-sehat

Output yang Dihasilkan:

json
{
  "Status": "healthy",
  "FailingStreak": 0,
  "Log": [
    {
      "Start": "2026-10-13T10:02:31.104512312Z",
      "End": "2026-10-13T10:02:31.118044293Z",
      "ExitCode": 0,
      "Output": "{\"app\":\"myapp\",\"pesan\":\"Halo dari container\"}"
    }
  ]
}

Mari kita telaah isi laporan di atas:

  1. Status — kesimpulan saat ini: healthy, unhealthy, atau starting; nilai inilah yang diringkas docker ps menjadi kata dalam tanda kurung.
  2. FailingStreak — jumlah kegagalan beruntun terakhir; nilainya direset ke nol setiap kali probe berhasil, dan inilah angka yang dibandingkan dengan --retries.
  3. Log — riwayat pemeriksaan terakhir, disimpan maksimal lima entri; setiap entri mencatat waktu mulai (Start), waktu selesai (End), exit code, dan keluaran stdout/stderr probe.
  4. Output berisi JSON myapp — probe kita memakai wget -qO- yang menuliskan isi respons ke stdout, sehingga laporan kesehatan ikut menyimpan jawaban aplikasi. Ini bukti terkuat bahwa probe benar-benar berbicara dengan aplikasi, bukan sekadar mengetuk pintunya.

Membuat Container Unhealthy #

Status unhealthy layak dilihat sekali supaya kita mengenal wajahnya — dan supaya melihat jebakan probe yang salah sasaran. Budi membangun image eksperimen yang probenya menembak port 9999, padahal aplikasinya tetap mendengarkan 8000:

dockerfile
FROM node:22-alpine
WORKDIR /app
COPY package.json server.js ./
EXPOSE 8000
HEALTHCHECK --interval=5s --timeout=3s --retries=3 \
  CMD wget -qO- localhost:9999/ || exit 1
CMD ["node", "server.js"]
bash
$ docker build -t myapp:sakit .

Output yang Dihasilkan:

text
[+] Building 1.2s (12/12) FINISHED
 => [internal] load build definition from Dockerfile                    0.1s
 => => transferring dockerfile: 197B                                    0.0s
 => [internal] load metadata for docker.io/library/node:22-alpine       0.7s
 => [1/5] FROM docker.io/library/node:22-alpine                         0.0s
 => => resolve docker.io/library/node:22-alpine                         0.7s
 => CACHED [2/5] WORKDIR /app                                           0.0s
 => CACHED [3/5] COPY package.json server.js ./                         0.0s
 => CACHED [4/5] EXPOSE 8000                                            0.0s
 => [5/5] HEALTHCHECK &{["CMD-SHELL" "wget -qO- localhost:9999/ || exit 1"] 5s 3s 0s 0s 3 false} 0.0s
 => exporting to image                                                  0.1s
 => => exporting layers                                                 0.0s
 => => writing image sha256:8a1e4c7b2d9f3a6c5b0d7f2e4c9a1b3d5f7c9e1a3b5d7f9c1e3a5b7d9f1c3e5f 0.1s
 => => naming to docker.io/library/myapp:sakit                          0.0s
bash
$ docker run -d --name myapp-sakit myapp:sakit

Output yang Dihasilkan:

text
b3e7d1a9c5f20a6d8f2b4c7e1a5d9b3f6c0a2e8d4b6f1c3a7e5b9d2f0a4c6e8a

Dengan interval 5 detik dan 3 percobaan, container ini resmi unhealthy dalam waktu sekitar 15 detik:

bash
$ docker ps

Output yang Dihasilkan:

text
CONTAINER ID   IMAGE         COMMAND          CREATED         STATUS                       PORTS      NAMES
b3e7d1a9c5f2   myapp:sakit   "node server.js" 41 seconds ago  Up 41 seconds (unhealthy)   8000/tcp   myapp-sakit

Anehnya, aplikasi di dalamnya hidup dan sehat — tidak ada flag -p, jadi Budi memeriksanya dari dalam:

bash
$ docker exec myapp-sakit wget -qO- localhost:8000/

Output yang Dihasilkan:

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

Laporan kesehatannya menceritakan kegagalan yang berulang:

bash
$ docker inspect --format '{{json .State.Health}}' myapp-sakit

Output yang Dihasilkan:

json
{
  "Status": "unhealthy",
  "FailingStreak": 8,
  "Log": [
    {
      "Start": "2026-10-13T10:04:50.003122841Z",
      "End": "2026-10-13T10:04:50.005874210Z",
      "ExitCode": 1,
      "Output": "wget: can't connect to remote host (127.0.0.1:9999): Connection refused"
    },
    {
      "Start": "2026-10-13T10:04:55.012401936Z",
      "End": "2026-10-13T10:04:55.015022478Z",
      "ExitCode": 1,
      "Output": "wget: can't connect to remote host (127.0.0.1:9999): Connection refused"
    },
    {
      "Start": "2026-10-13T10:05:00.021530082Z",
      "End": "2026-10-13T10:05:00.024018753Z",
      "ExitCode": 1,
      "Output": "wget: can't connect to remote host (127.0.0.1:9999): Connection refused"
    },
    {
      "Start": "2026-10-13T10:05:05.030618541Z",
      "End": "2026-10-13T10:05:05.033200914Z",
      "ExitCode": 1,
      "Output": "wget: can't connect to remote host (127.0.0.1:9999): Connection refused"
    },
    {
      "Start": "2026-10-13T10:05:10.039744120Z",
      "End": "2026-10-13T10:05:10.042311365Z",
      "ExitCode": 1,
      "Output": "wget: can't connect to remote host (127.0.0.1:9999): Connection refused"
    }
  ]
}

Mari kita telaah laporan kegagalan di atas:

  1. FailingStreak: 8 — delapan kegagalan beruntun terhitung sejak pemeriksaan pertama; angka terus bertambah selama probe gagal dan akan direset begitu probe berhasil.
  2. Log hanya berisi lima entri — daemon menyimpan riwayat pemeriksaan terakhir saja; entri ke enam dan seterusnya menggeser yang paling tua.
  3. Output berisi pesan wget — probe yang gagal meninggalkan jejak penyebab: koneksi ke 9999 ditolak; pada kasus nyata, jejak inilah yang membedakan “aplikasi mati” dari “probe salah sasaran”.
  4. Aplikasi tetap sehat — exec ke port 8000 dijawab sempurna; status unhealthy melaporkan hasil probe, bukan isi hati container. Alarm palsu semacam ini hanya bisa dipercaya bila probe memang menguji jalur yang benar.

Image myapp kini mampu melaporkan kesehatannya sendiri, dan kolom STATUS akhirnya bisa dipercaya. Tiga instruksi terakhir di peta tujuh belas jarang muncul di Dockerfile sehari-hari tetapi masing-masing menyimpan pelajaran: STOPSIGNAL mengubah sinyal yang dikirim docker stop, SHELL mengganti perantara bentuk shell form, dan ONBUILD menyimpan instruksi yang baru menyala ketika image kita menjadi basis image orang lain. Setelah ketiganya kita bedah, kita rangkai seluruh section ini dengan satu Dockerfile final untuk myapp — itulah agenda Chapter Instruksi Lanjutan.

Referensi #