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:
$ docker run --rm myapp:1.0 node --versionOutput yang Dihasilkan:
v22.20.0Mari kita telaah satu perintah pendek yang padat makna itu:
myapp:1.0membawaCMD ["node", "server.js"]— image buatan Chapter Membangun Image dengan Dockerfile yang perintah bawaannya menjalankan server.node --versionmenggantikan CMD seutuhnya — Docker tidak menggabungkan keduanya; server.js tidak pernah tersentuh, dan yang dieksekusi adalahnode --versionsaja. Timpaan bersifat total, bukan tempelan di depan atau di belakang.--rmmembuang bekasnya seketika — container sekali jalan seperti ini tidak meninggalkan barisExiteddidocker ps -a.- Pola ini adalah pintu utama image serbaguna — image basis seperti
node:22-alpinemenimbangCMD ["node"]; setiapdocker 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:
$ docker image inspect nginx --format '{{.Config.Entrypoint}} {{.Config.Cmd}}'Output yang Dihasilkan:
[/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:
| Perintah | Yang dieksekusi | dari 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 -t | ENTRYPOINT + argumen kita menimpa CMD |
docker run -it --entrypoint sh nginx | sh | flag --entrypoint menimpa ENTRYPOINT |
Baris kedua dari tabel itu layak dieksekusi langsung — nginx membawa nginx -t, pemeriksa sintaks konfigurasinya:
$ docker run --rm nginx nginx -tOutput yang Dihasilkan:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successfulMari kita telaah rantai peristiwa pada perintah tersebut:
- Argumen
nginx -tmenimpa CMD —nginx -g daemon off;tidak dipakai, persis perilaku CMD yang kita buktikan pada myapp. - ENTRYPOINT tetap berdiri di depan —
/docker-entrypoint.shtetap proses pertama yang menyala; skrip milik image nginx ini kemudian memeriksa argumennya dan menjalankannginx -tapa adanya. --rmmenjadikan image ini alat sekali pakai — tanpa server menyala, tanpa port diterbitkan, container hidup beberapa detik untuk memvalidasi konfigurasi lalu lenyap.- Ganti argumennya dan wajahnya ikut berubah —
docker run --rm nginx nginx -vmembalasnginx version: nginx/1.29.4, nilai yang sama denganNGINX_VERSIONpada.Config.Envdari Chapter Image Inspect dan History.
Pola ENTRYPOINT penyalur argumen inilah yang melahirkan keluarga image alat — image yang terasa seperti perintah lokal terpasang padahal tidak:
| Image | Perintah khas | Manfaatnya |
|---|---|---|
redis:7-alpine | docker run --rm redis:7-alpine redis-cli --version | klien redis-cli tanpa instalasi di host |
postgres:18-alpine | docker run --rm postgres:18-alpine psql --version | klien PostgreSQL siap pakai dengan versi terpasang |
nginx | docker run --rm nginx nginx -t | validator 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:
$ docker run -d --name myapp-exec myapp:1.0Output yang Dihasilkan:
16154154fa70400c61578fd02e86f1305a568540e6390067470179bc0bdb4180$ docker run -d --name myapp-shell myapp:shellOutput yang Dihasilkan:
2c67fcc5bd5d53bd62971a3d37a1105bebae146f6a66725ba7f063ca0285a0da$ docker psOutput yang Dihasilkan:
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-shellKolom COMMAND memperlihatkan perbedaan pertama, dan kedalaman sesungguhnya terlihat dari dalam container:
$ docker exec myapp-exec psOutput yang Dihasilkan:
PID USER TIME COMMAND
1 root 0:00 node server.js
8 root 0:00 ps$ docker exec myapp-shell psOutput yang Dihasilkan:
PID USER TIME COMMAND
1 root 0:00 sh -c node server.js
6 root 0:00 node server.js
12 root 0:00 psMari kita telaah dua tabel proses itu — perbedaan satu baris PID 1 yang berakibat besar:
- Exec form menempatkan aplikasi langsung di kursi PID 1 — proses
node server.jsadalah proses pertama container, tanpa perantara. - Shell form menempatkan shell di kursi PID 1 —
/bin/sh -cmenjadi proses pertama dan mengeksekusinode server.jssebagai anak; itulah mengapa kolom COMMAND padadocker psmenampilkan/bin/sh -c 'node server.js'. - Konsekuensinya menyangkut sinyal —
docker stopmengirimSIGTERMke PID 1, tunggu sepuluh detik, lalu mengirimSIGKILL— alur dari Chapter Menghentikan dan Menjalankan Container. Pada exec form, sinyal mendarat di tangannode; pada shell form, sinyal berhenti dishyang tidak meneruskannya ke anak-anaknya.
Bedah sinyal itu bisa diukur langsung. Budi menghentikan keduanya dan membaca angka terakhir mereka:
$ docker stop myapp-exec
$ docker inspect myapp-exec --format '{{.State.ExitCode}}'Output yang Dihasilkan:
myapp-exec
143$ docker stop myapp-shell
$ docker inspect myapp-shell --format '{{.State.ExitCode}}'Output yang Dihasilkan:
myapp-shell
137Perintah 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:
143pada myapp-exec adalah128 + 15—SIGTERMsampai ke proses node, kernel menghentikan proses karena sinyal itu, dan container melaporkan keluar dengan cara yang wajar. Angka ini bukan galat;docker stopmenganggapnya sukses.137pada myapp-shell adalah128 + 9—SIGTERMberhenti di shell, aplikasi tidak pernah tahu diminta berhenti, masa tenggang sepuluh detik lewat, danSIGKILLyang 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.- Kedua container kini berstatus berhenti —
docker ps -amenyimpan mereka sebagaiExited (143)danExited (137); jejak ini yang kelak kita baca ulang ketika membahas penghentian yang lebih sopan.
Seluruh perbedaan dua bentuk itu terangkum dalam satu tabel:
| Aspek | Shell form | Exec form |
|---|---|---|
| Bentuk penulisan | CMD node server.js | CMD ["node", "server.js"] |
| Yang dijalankan Docker | /bin/sh -c "node server.js" | node server.js tanpa perantara |
| Proses PID 1 | shell — aplikasi menjadi anak | aplikasi itu sendiri |
Nasib sinyal docker stop | berhenti di shell, akhirnya 137 | sampai ke aplikasi, umumnya 143 |
| Substitusi variabel shell | tersedia — ${VAR} diurai shell saat runtime | tidak — 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 #
- Dockerfile Reference
— semantik
CMD,ENTRYPOINT, interaksi keduanya, serta shell form versus exec form. - docker container run — CLI Reference
— perilaku argumen setelah nama image dan flag
--entrypoint. - docker stop — CLI Reference
— alur
SIGTERM, masa tenggang, danSIGKILL.