Setiap proses yang kita jalankan sepanjang section ini — docker run maupun docker exec — pada akhirnya berakhir, dan setiap akhir meninggalkan satu angka kecil antara 0 sampai 255 sebagai warisan terakhirnya. Kita sudah dua kali berpapasan dengan angka itu: quirky_einstein tutup usia dengan Exited (0), sedangkan web sempat tercatat Exited (137) ketika kita mengujinya dengan docker kill. Angka-angka semacam itu bukan hiasan kolom STATUS — angka tersebut adalah bahasa diagnosis paling murah yang dimiliki container: tanpa membuka log, tanpa memasuki container, kita bisa mempersempit dugaan penyebab kematian sebuah proses. Chapter penutup section ini membedah konvensi di balik exit code, mempraktikkan cara menangkapnya dari shell, menungguinya secara langsung, dan membacanya dari status container — bekal yang membuat kita tidak pernah lagi menganggap Exited (137) sebagai teka-teki.

Exit Code dari Sudut Pandang Shell #

Exit code lahir dari konvensi Unix yang setua sistem operasi itu sendiri: saat sebuah proses berakhir, proses tersebut menyerahkan satu angka status kepada induknya. Nilai 0 berarti sukses; nilai selain 0 berarti gagal dengan sebab yang tersirat di angkanya. Shell menyimpan angka status perintah terakhir pada variabel $?, dan docker run yang berjalan dalam mode terpasang meneruskan exit code milik PID 1 container langsung ke sana. Karena image alpine:3.20 sudah tersimpan di mesin kita sejak Chapter Menghapus Container, percobaan berikut berlangsung seketika:

bash
$ docker run --rm alpine:3.20 sh -c 'exit 7'
$ echo $?

Output yang Dihasilkan:

text
7

Mari kita telaah apa yang barusan terjadi:

  1. sh -c 'exit 7' sebagai PID 1 — shell di dalam container menjalankan satu instruksi exit 7 lalu berakhir dengan kode 7; proses utama container berumur hanya beberapa milidetik dan tidak mencetak apa pun.
  2. docker run meneruskan kode tersebut — CLI menunggu container selesai, mengambil kode keluar PID 1, lalu menutup dirinya dengan kode yang sama; $? milik shell kita menangkap angka 7 itu utuh.
  3. --rm membuat percobaan steril — container bekas langsung dibersihkan sehingga docker ps -a tidak menampilkan jejak apa pun; satu-satunya bukti bahwa percobaan pernah terjadi adalah angka di $?.
  4. Kode 3 sampai 255 adalah wilayah aplikasi — angka 7 barusan kami pilih sendiri; aplikasi bebas mendefinisikan arti tiap kode, dan kebiasaan membaca $? berurutan dengan docker run adalah cara tercepat menguji kode-kode tersebut.
  5. Mode detached tidak meneruskan kode — docker run -d mencetak ID container dan segera mengembalikan terminal; exit code container tersebut tidak bisa ditangkap lewat $? dan harus dibaca belakangan lewat status container.

Status container memang menjadi tempat kediaman resmi angka tersebut: kolom STATUS pada docker ps -a menampilkan Exited (N) untuk setiap container yang sudah berhenti — Exited (0) yang kita lihat pada quirky_einstein, Exited (137) yang pernah tercatat pada web, dan sebentar lagi Exited (143) pada percobaan kita.

Tiga Konvensi Exit Code #

Tiga keluarga konvensi mengisi rentang 0 sampai 255, dan ketiganya perlu dibedakan karena berasal dari pelaku yang berbeda: aplikasi, Docker, dan kernel.

Konvensi Unix #

Keluarga pertama adalah kesepakatan universal dunia Unix. Kode 0 menyatakan sukses penuh — program menuntaskan tugasnya tanpa keluhan. Kode 1 adalah kode kegagalan aplikasi yang paling umum: program mencoba, gagal, lalu melaporkan dirinya — error pembacaan berkas, koneksi ditolak, atau exception yang tidak tertangani. Kode 2 secara tradisi menandai kesalahan pemakaian: argumen perintah salah tulis atau flag tidak dikenal; banyak program CLI menjatuhkan kode ini sambil mencetak ringkasan cara pakainya. Percobaan exit 7 di atas menunjukkan bahwa sisanya — 3 hingga 255 — adalah wilayah bebas yang artinya ditentukan masing-masing aplikasi.

Konvensi Docker #

Keluarga kedua berasal dari Docker CLI sendiri, untuk keadaan ketika container gagal menjalankan sesuatu. Kode 125 menandakan daemon menolak atau gagal menjalankan perintah docker run sebelum container sempat menyala — penyebabnya berupa flag yang salah atau nilainya tidak valid, misalnya konflik nama container yang sudah dipakai. Kode 126 berarti perintah yang dimaksud ada di dalam image tetapi tidak bisa dieksekusi — khasnya berkas tanpa bit eksekusi. Kode 127 adalah yang paling sering menjumpai pemula: perintah yang diminta tidak ditemukan sama sekali di dalam image. Kita reproduksi kejadiannya secara sengaja:

bash
$ docker run --rm alpine:3.20 tidakada

Output yang Dihasilkan:

text
docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: "tidakada": executable file not found in $PATH: unknown.
bash
$ echo $?

Output yang Dihasilkan:

text
127

Mari kita telaah pasangan output di atas:

  1. Pesan galat yang panjang adalah jejak perjalanannya — permintaan kita berjalan dari CLI ke daemon, ke containerd, sampai ke runc yang hendak melahirkan proses; rantai ini persis arsitektur berlapis yang kita bahas pada Chapter Arsitektur Docker.
  2. Intinya ada di ujung pesan — exec: "tidakada": executable file not found in $PATH berarti runc tidak menemukan biner bernama tidakada di direktori pencarian image alpine; nama perintah kita memang karangan.
  3. CLI menutup dirinya dengan kode 127 — sesuai konvensi shell yang sudah ada sebelum Docker lahir: perintah tidak ditemukan berarti 127; container gagal menjalankan apapun sehingga tidak ada exit code aplikasi yang bisa dilaporkan.
  4. Kejadiannya sering tidak disengaja — versi paling umum dari galat ini adalah image yang keliru disebut atau aplikasi dalam image yang berpindah jalur biner; kode 127 pada docker ps -a adalah petunjuk kuat untuk memeriksa nama perintah dan image yang dipakai.

Konvensi Sinyal 128+N #

Keluarga ketiga diisi oleh kernel. Bila sebuah proses berakhir karena dibunuh sinyal nomor N, kernel melaporkan status keluarnya sebagai 128 + N — konvensi yang diwarisi Docker utuh. Dua angka paling penting: 137 berarti 128 + 9, yaitu SIGKILL, dan 143 berarti 128 + 15, yaitu SIGTERM. Makanya docker kill selalu meninggalkan Exited (137), dan docker stop pada container yang proses utamanya tidak menangani SIGTERM juga berakhir di sana… sebenarnya tidak — container seperti itu justru berakhir di 143, karena prosesnya tumbang oleh SIGTERM yang sopan itu sendiri, persis seperti yang akan kita saksikan sebentar lagi pada percobaan docker wait.

Exit Code Umum dan Langkah Diagnosis #

Tujuh kode berikut menutupi hampir semua kejadian yang akan kita jumpai sehari-hari. Tabel berikut merangkum artinya beserta langkah diagnosis yang paling produktif untuk masing-masing:

KodeArtiLangkah diagnosis
0Berhasil — PID 1 menuntaskan tugasnya dengan rapiTidak perlu diagnosis; baca docker logs bila ingin kronologinya
1Kegagalan aplikasi — program gagal dan melaporkan dirinyaBaca docker logs dan cari pesan error atau exception paling akhir
125Perintah ditolak daemon — flag atau nilainya tidak validPeriksa penulisan perintah; bandingkan dengan --help
126Perintah ditemukan tetapi tidak bisa dieksekusi — izin salahPeriksa bit eksekusi berkas dan arsitektur biner image
127Perintah tidak ditemukan di dalam imagePeriksa ejaan nama biner dan variabel PATH; verifikasi image yang dipakai
137Dihentikan SIGKILL — 128+9, termasuk OOM killPeriksa riwayat docker kill dan masa tenggang stop; periksa OOMKilled
143Dihentikan SIGTERM — 128+15Umumnya wajar akibat docker stop; pastikan aplikasi menangani sinyal

Urutan kerja yang sehat ketika sebuah container mati adalah: baca kode keluarnya lebih dulu lewat docker ps -a, duga penyebabnya lewat tabel di atas, lalu gali buktinya di docker logs — exit code mempersempit pencarian, log yang menengahi.

docker wait dan docker inspect #

Dua perintah pelengkap membuat kita bisa bekerja dengan exit code secara langsung. Yang pertama, docker wait, memblokir terminal sampai container yang disebut berhenti, lalu mencetak kode keluarnya. Kita siapkan container korban berumur lima menit — image alpine yang sama, kali ini tanpa --rm supaya bekasnya bisa kita periksa:

bash
$ docker run -d --name uji-tunggu alpine:3.20 sleep 300

Output yang Dihasilkan:

text
143371a70729ccdd11c5d92a68fd5364fd4ef0d23d06ca538f6ad8b776cc386a

Buka terminal kedua, lalu jalankan docker wait uji-tunggu di terminal pertama — layar akan diam, menunggu. Dari terminal kedua, kita hentikan uji-tunggu dengan jalur sopan yang sudah kita kenal:

bash
$ docker stop uji-tunggu

Output yang Dihasilkan:

text
uji-tunggu

Beberapa saat setelah perintah kedua, terminal pertama yang tadi diam akhirnya menjawab:

text
143

Mari kita telaah rangkaian kejadian di atas:

  1. docker wait memblokir sampai ada yang mati — perintah di terminal pertama tidak mencetak apa pun selama uji-tunggu hidup; sesi menunggu berakhir begitu container berhenti, dan satu-satunya keluarannya adalah exit code.
  2. 143 berasal dari sleep yang telanjang — program sleep tidak memasang penangan SIGTERM; ketika docker stop mengirim sinyal tersebut, kernel menghentikan proses begitu saja dan melaporkannya sebagai 128 + 15 = 143.
  3. Kontras dengan pengalaman kita pada web — nginx menangani sinyal berhenti dengan rapi sehingga docker stop web selalu berakhir Exited (0); dua container yang dimatikan dengan perintah yang sama meninggalkan dua angka berbeda, dan bedanya murni soal siapa yang mengurus sinyal.

Status container kini memuat korban baru, lengkap dengan kodenya:

bash
$ docker ps -a

Output yang Dihasilkan:

text
CONTAINER ID   IMAGE         COMMAND                  CREATED          STATUS                            PORTS                                     NAMES
f7c2a91b3e55   nginx         "/docker-entrypoint.…"   56 minutes ago   Up 34 minutes                     0.0.0.0:8080->80/tcp, [::]:8080->80/tcp   web
3a1c8f2e9b7d   redis:7       "docker-entrypoint.s…"   50 minutes ago   Up 50 minutes                     6379/tcp                                  cache
143371a70729   alpine:3.20   "sleep 300"              2 minutes ago    Exited (143) About a minute ago                                             uji-tunggu

Perintah pelengkap kedua membaca kode yang sama secara retroaktif — berguna untuk container detached yang sudah lama berlalu atau saat $? tidak pernah sempat kita tangkap. Template Go yang kita kenal dari Chapter docker help dan docker version bekerja di sini:

bash
$ docker inspect --format '{{.State.ExitCode}}' uji-tunggu

Output yang Dihasilkan:

text
143

Mari kita telaah pemakaian kedua perintah pelengkap tersebut:

  1. {{.State.ExitCode}} — template membaca field ExitCode di dalam bagian State pada metadata container; docker inspect tanpa --format akan mencetak seluruh metadata JSON itu dan kolom State menyimpan status, waktu mulai, waktu berhenti, hingga penanda OOM.
  2. docker wait untuk masa kini, docker inspect untuk masa lalu — wait menghadang kita di depan peristiwa yang sedang berjalan, sementara inspect menggali arsip yang sudah tersimpan; keduanya melengkapi, bukan bersaing.

Bekas percobaan kita bereskan sesuai kebiasaan section ini:

bash
$ docker rm uji-tunggu

Output yang Dihasilkan:

text
uji-tunggu

Sepuluh chapter silam kita memulai dari mesin yang kosong — tabel docker ps yang hanya menampilkan kepala kolom. Kini, setelah menjalankan seluruh siklus hidup container, sudah sepantasnya kita menutup section dengan mengembalikan mesin ke keadaan semula, dan kita lakukan persis dengan urutan yang dianjurkan galat docker rm pada Chapter Menghapus Container: hentikan secara sopan lebih dulu, baru hapus:

bash
$ docker stop web cache

Output yang Dihasilkan:

text
web
cache
bash
$ docker rm web cache

Output yang Dihasilkan:

text
web
cache
bash
$ docker ps -a

Output yang Dihasilkan:

text
CONTAINER ID   IMAGE   COMMAND   CREATED   STATUS   PORTS   NAMES

Tabel kosong — persis tampilan pertama yang kita saksikan pada Chapter Struktur Perintah Docker. Yang tidak ikut kosong adalah penyimpanan image: nginx, redis:7, alpine:3.20, dan hello-world tetap menunggu di mesin, siap melahirkan container baru kapan pun tanpa satu baris unduhan. Dan di sinilah section ini menutup dirinya dengan satu pertanyaan yang belum pernah kita jawab: selama sepuluh chapter, kita memperlakukan image sebagai kotak hitam yang tinggal kita sebut namanya — padahal struktur internal image menjelaskan mengapa container menyala seketika, mengapa docker rm tidak pernah merusak image asalnya, dan mengapa layer yang sama bisa dipakai bergiliran tanpa disalin. Section berikutnya, Docker Image, membuka kotak itu lapisan demi lapisan — dimulai dari Chapter Konsep Image Layer.

Referensi #