Dua instruksinya kini lengkap: ENV membawa konfigurasi yang hidup sampai container menyala, dan ARG membawa parameter yang menyempurnakan resep saat build. Namun dari seluruh instruksi Dockerfile, dua yang paling menentukan hidup container justru belum kita sentuh secara sadar: selama ini myapp menyala lewat CMD ["node", "server.js"] tanpa kita pernah bertanya bagaimana Docker memperlakukan baris itu — apa yang terjadi bila argumen lain menyusul di belakang nama image saat docker run, dan mengapa bentuk penulisan dengan tanda kurung siku begitu digalakkan. Chapter ini membedah CMD dan ENTRYPOINT sampai ke mekanismenya: siapa yang menimpa siapa, bagaimana bentuk shell form dan exec form menentukan proses mana yang menjadi PID 1, dan bagaimana nasib sinyal docker stop bergantung pada pilihan bentuk itu.

CMD: Nilai Bawaan yang Mudah Ditimpa #

Instruksi CMD menyatakan perintah bawaan container: apa yang dijalankan ketika docker run tidak menyebut perintah apa pun. Kata kuncinya adalah bawaan — dan bawaan memang dibuat untuk ditimpa. Argumen yang kita ketik setelah nama image menggantikan CMD secara total:

bash
$ docker run --rm myapp:1.0 node --version

Output yang Dihasilkan:

text
v22.20.0

Mari kita telaah satu perintah pendek yang padat makna itu:

  1. myapp:1.0 membawa CMD ["node", "server.js"] — image buatan Chapter Membangun Image dengan Dockerfile yang perintah bawaannya menjalankan server.
  2. node --version menggantikan CMD seutuhnya — Docker tidak menggabungkan keduanya; server.js tidak pernah tersentuh, dan yang dieksekusi adalah node --version saja. Timpaan bersifat total, bukan tempelan di depan atau di belakang.
  3. --rm membuang bekasnya seketika — container sekali jalan seperti ini tidak meninggalkan baris Exited di docker ps -a.
  4. Pola ini adalah pintu utama image serbaguna — image basis seperti node:22-alpine menimbang CMD ["node"]; setiap docker run node:22-alpine --eval ... pada Chapter Menjalankan Perintah dalam Container bekerja lewat mekanisme timpa yang sama.

ENTRYPOINT: Perintah Utama yang Menambat #

ENTRYPOINT adalah kebalikan CMD dalam hal posisi: perintah utama yang menambat container, sedangkan CMD menyediakan parameter bawaan untuk perintah utama itu. Argumen docker run tidak lagi menggantikan apa pun — argumen menjadi parameter yang ditempelkan di belakang ENTRYPOINT. Kombinasi keduanya paling jelas terbaca pada image yang paling lama menemani kita:

bash
$ docker image inspect nginx --format '{{.Config.Entrypoint}} {{.Config.Cmd}}'

Output yang Dihasilkan:

text
[/docker-entrypoint.sh] [nginx -g daemon off;]

Pasangan itu bermakna: container nginx menjalankan /docker-entrypoint.sh sebagai proses utama, dengan nginx -g daemon off; sebagai parameter bawaannya. Ketika docker run nginx dieksekusi tanpa argumen, ENTRYPOINT menerima CMD sebagai parameter; ketika argumen disertakan, CMD tergeser dan argumen itulah yang menjadi parameter:

PerintahYang dieksekusidari mana susunannya
docker run nginx/docker-entrypoint.sh nginx -g daemon off;ENTRYPOINT + CMD bawaan
docker run --rm nginx nginx -t/docker-entrypoint.sh nginx -tENTRYPOINT + argumen kita menimpa CMD
docker run -it --entrypoint sh nginxshflag --entrypoint menimpa ENTRYPOINT

Baris kedua dari tabel itu layak dieksekusi langsung — nginx membawa nginx -t, pemeriksa sintaks konfigurasinya:

bash
$ docker run --rm nginx nginx -t

Output yang Dihasilkan:

text
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Mari kita telaah rantai peristiwa pada perintah tersebut:

  1. Argumen nginx -t menimpa CMD — nginx -g daemon off; tidak dipakai, persis perilaku CMD yang kita buktikan pada myapp.
  2. ENTRYPOINT tetap berdiri di depan — /docker-entrypoint.sh tetap proses pertama yang menyala; skrip milik image nginx ini kemudian memeriksa argumennya dan menjalankan nginx -t apa adanya.
  3. --rm menjadikan image ini alat sekali pakai — tanpa server menyala, tanpa port diterbitkan, container hidup beberapa detik untuk memvalidasi konfigurasi lalu lenyap.
  4. Ganti argumennya dan wajahnya ikut berubah — docker run --rm nginx nginx -v membalas nginx version: nginx/1.29.4, nilai yang sama dengan NGINX_VERSION pada .Config.Env dari Chapter Image Inspect dan History.

Pola ENTRYPOINT penyalur argumen inilah yang melahirkan keluarga image alat — image yang terasa seperti perintah lokal terpasang padahal tidak:

ImagePerintah khasManfaatnya
redis:7-alpinedocker run --rm redis:7-alpine redis-cli --versionklien redis-cli tanpa instalasi di host
postgres:18-alpinedocker run --rm postgres:18-alpine psql --versionklien PostgreSQL siap pakai dengan versi terpasang
nginxdocker run --rm nginx nginx -tvalidator konfigurasi satu perintah

Shell Form dan Exec Form #

Kedua instruksi di atas bisa ditulis dalam dua bentuk yang perilakunya berbeda penting. Bentuk exec form menuliskan perintah sebagai daftar JSON: CMD ["node", "server.js"]. Bentuk shell form menuliskannya sebagai string biasa: CMD node server.js — dan Docker membungkusnya menjadi /bin/sh -c "node server.js". Budi membuktikan bedanya dengan satu eksperimen: ia menyalin Dockerfile myapp lalu mengubah satu barisnya ke bentuk shell, membangunnya sebagai myapp:shell, lalu menjalankan kedua versi berdampingan:

bash
$ docker run -d --name myapp-exec myapp:1.0

Output yang Dihasilkan:

text
16154154fa70400c61578fd02e86f1305a568540e6390067470179bc0bdb4180
bash
$ docker run -d --name myapp-shell myapp:shell

Output yang Dihasilkan:

text
2c67fcc5bd5d53bd62971a3d37a1105bebae146f6a66725ba7f063ca0285a0da
bash
$ docker ps

Output yang Dihasilkan:

text
CONTAINER ID   IMAGE          COMMAND                    CREATED         STATUS         PORTS                    NAMES
f7c2a91b3e55   nginx          "/docker-entrypoint.…"     4 days ago      Up 4 days      0.0.0.0:8080->80/tcp     web
16154154fa70   myapp:1.0      "node server.js"           3 seconds ago   Up 2 seconds                            myapp-exec
2c67fcc5bd5d   myapp:shell    "/bin/sh -c 'node serv…"   2 seconds ago   Up 2 seconds                            myapp-shell

Kolom COMMAND memperlihatkan perbedaan pertama, dan kedalaman sesungguhnya terlihat dari dalam container:

bash
$ docker exec myapp-exec ps

Output yang Dihasilkan:

text
PID   USER     TIME  COMMAND
    1 root      0:00 node server.js
    8 root      0:00 ps
bash
$ docker exec myapp-shell ps

Output yang Dihasilkan:

text
PID   USER     TIME  COMMAND
    1 root      0:00 sh -c node server.js
    6 root      0:00 node server.js
   12 root      0:00 ps

Mari kita telaah dua tabel proses itu — perbedaan satu baris PID 1 yang berakibat besar:

  1. Exec form menempatkan aplikasi langsung di kursi PID 1 — proses node server.js adalah proses pertama container, tanpa perantara.
  2. Shell form menempatkan shell di kursi PID 1 — /bin/sh -c menjadi proses pertama dan mengeksekusi node server.js sebagai anak; itulah mengapa kolom COMMAND pada docker ps menampilkan /bin/sh -c 'node server.js'.
  3. Konsekuensinya menyangkut sinyal — docker stop mengirim SIGTERM ke PID 1, tunggu sepuluh detik, lalu mengirim SIGKILL — alur dari Chapter Menghentikan dan Menjalankan Container. Pada exec form, sinyal mendarat di tangan node; pada shell form, sinyal berhenti di sh yang tidak meneruskannya ke anak-anaknya.

Bedah sinyal itu bisa diukur langsung. Budi menghentikan keduanya dan membaca angka terakhir mereka:

bash
$ docker stop myapp-exec
$ docker inspect myapp-exec --format '{{.State.ExitCode}}'

Output yang Dihasilkan:

text
myapp-exec
143
bash
$ docker stop myapp-shell
$ docker inspect myapp-shell --format '{{.State.ExitCode}}'

Output yang Dihasilkan:

text
myapp-shell
137

Perintah pertama selesai hampir seketika; perintah kedua berhenti tepat setelah masa tenggang sepuluh detik habis. Mari kita telaah kedua angka itu dengan kosakata Chapter Memahami Exit Code:

  1. 143 pada myapp-exec adalah 128 + 15 — SIGTERM sampai ke proses node, kernel menghentikan proses karena sinyal itu, dan container melaporkan keluar dengan cara yang wajar. Angka ini bukan galat; docker stop menganggapnya sukses.
  2. 137 pada myapp-shell adalah 128 + 9 — SIGTERM berhenti di shell, aplikasi tidak pernah tahu diminta berhenti, masa tenggang sepuluh detik lewat, dan SIGKILL yang tak bisa ditolak menuntaskan segalanya. Sepuluh detik itu terasa lama pada shutdown dan lebih buruk lagi pada deployment: proses tidak pernah diberi kesempatan menutup koneksi databasenya.
  3. Kedua container kini berstatus berhenti — docker ps -a menyimpan mereka sebagai Exited (143) dan Exited (137); jejak ini yang kelak kita baca ulang ketika membahas penghentian yang lebih sopan.

Seluruh perbedaan dua bentuk itu terangkum dalam satu tabel:

AspekShell formExec form
Bentuk penulisanCMD node server.jsCMD ["node", "server.js"]
Yang dijalankan Docker/bin/sh -c "node server.js"node server.js tanpa perantara
Proses PID 1shell — aplikasi menjadi anakaplikasi itu sendiri
Nasib sinyal docker stopberhenti di shell, akhirnya 137sampai ke aplikasi, umumnya 143
Substitusi variabel shelltersedia — ${VAR} diurai shell saat runtimetidak — daftar argv diteruskan mentah

Dua pasang instruksi kini tuntas dibedah: CMD dan ENTRYPOINT menentukan siapa yang menyala dan bagaimana ia menerima sinyal, ENV dan ARG menentukan nilai apa yang dibawanya. Dockerfile myapp dengan demikian sudah tahu cara menyala dengan benar. Namun satu baris kecil di dalamnya masih menyimpan miskonsepsi yang paling sering menjebak pemula Docker: EXPOSE 8000 — baris yang oleh banyak orang diyakini sebagai perintah membuka port ke dunia luar, padahal kenyataannya jauh lebih sederhana dan jauh lebih penting untuk dipahami dengan benar.

Referensi #