Crash loop adalah kegagalan yang paling berisik di luar namun paling sunyi di dalam: container mati, daemon membangkitkannya lewat restart policy, container mati lagi — berulang tanpa satu pun pesan muncul di layar kita. Yang terlihat hanyalah status Restarting (1) yang terus berganti umur di kolom STATUS, sementara jejak lengkapnya tersimpan rapi di tempat lain: log terakhir aplikasi, exit code, jumlah kebangkitan di docker inspect, sampai catatan kernel bila prosesnya dibunuh kelaparan memori. Korban sore ini adalah worker-gambar — service baru di stack toko yang memproses unggahan gambar produk, berjalan dari image worker:1.0 yang sore itu berhasil dibangun setelah chapter sebelumnya. Chapter ini menyusun jejak-jejak tersebut menjadi urutan diagnosis yang tetap, memetakan lima pola penyebab yang paling sering terjadi, lalu menutupnya dengan dua skenario nyata dari laporan sampai pemulihan — termasuk satu keterampilan yang sering dilupakan: menjinakkan restart policy agar akar masalah berhenti bersembunyi.

Mengenali Crash Loop dari Luar #

Pemeriksaan pertama selalu docker ps, dan inilah yang dilihat Budi beberapa menit setelah worker-gambar dideploy:

bash
$ docker ps --filter name=worker-gambar

Output yang Dihasilkan:

text
CONTAINER ID   IMAGE        COMMAND         CREATED          STATUS                         PORTS     NAMES
7c9e2a1b3d4f   worker:1.0   "python work…"  25 minutes ago   Restarting (1) 3 seconds ago             worker-gambar

Status Restarting (1) 3 seconds ago terbaca sebagai: container terakhir mati dengan exit code 1 (angka dalam kurung) dan menghidupkan diri kembali tiga detik yang lalu. Angka dalam kurung itu langsung menghubungkan kasus ini dengan tabel chapter Membaca Container Exit. Restart policy aktif karena service dideklarasikan restart: always; daemon memberi jeda backoff yang membesar antar kebangkitan, namun kebijakan always tidak pernah menyerah. Jumlah kebangkitannya sendiri dicatat terpisah:

bash
$ docker inspect worker-gambar --format 'RestartCount={{.RestartCount}} ExitCode={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}}'

Output yang Dihasilkan:

text
RestartCount=23 ExitCode=1 OOMKilled=false

Dua puluh tiga kebangkitan dalam dua puluh lima menit menegaskan loop yang rapat. Exit code 1 yang konsisten di setiap kebangkitan menandakan kegagalan deterministik — bukan gangguan acak — dan OOMKilled=false membebaskan memori dari daftar tersangka sejak awal.

Empat Jejak dalam Urutan Tetap #

Diagnosis crash loop bergerak pada empat jejak berurutan, dari yang termurah menuju yang paling dalam:

  1. docker ps — siapa yang loop, dan dengan exit code berapa: langkah di atas.
  2. docker logs — kata-kata terakhir aplikasi sebelum mati.
  3. docker inspect — angka-angka (RestartCount, ExitCode, OOMKilled) untuk mengonfirmasi pola.
  4. dmesg — catatan kernel, dibuka hanya bila exit code mengarah ke 137.

Jejak kedua pada kasus kita berbunyi:

bash
$ docker logs --tail 6 worker-gambar

Output yang Dihasilkan:

text
[2026-10-14 15:34:02] worker-gambar 1.0 dimulai
[2026-10-14 15:34:02] membaca konfigurasi dari /app/config/config.yml
Traceback (most recent call last):
  File "/app/worker.py", line 28, in <module>
    with open('/app/config/config.yml') as f:
FileNotFoundError: [Errno 2] No such file or directory: '/app/config/config.yml'

Jejak ketiga sudah kita jalankan di atas dan angkanya konsisten dengan traceback. Jejak keempat tidak diperlukan di kasus ini — dmesg adalah hakim untuk exit 137, dan kita akan memakainya pada varian kedua di bawah.

Pola Penyebab yang Paling Sering #

Lima pola berikut menutup hampir seluruh crash loop yang terjadi di dunia nyata:

Pola penyebabJejak khasPerbaikan
Environment variable wajib kosongKeyError: 'API_TOKEN' atau galat “required setting” lalu exit 1 — kasus laporan-harian kemarinIsi environment/.env; buat aplikasi gagal cepat dengan pesan jelas
Berkas konfigurasi tidak adaFileNotFoundError pada path yang seharusnya di-mountPerbaiki pemetaan volume — keluarga masalah chapter Debugging Storage
Database belum siap saat startECONNREFUSED di awal log lalu mati; sehat saat dites manualdepends_on + service_healthy atau retry di kode — chapter Startup dan Dependency
OOM: memory limit kekecilanexit 137, OOMKilled=true, baris Memory cgroup out of memory di dmesgNaikkan mem_limit; cari kebocoran dengan docker stats
Konflik port di dalam containeraddress already in use saat startupPeriksa entrypoint yang menyalakan dua proses pada port sama

Dua pola pertama bersifat deterministik: exit code selalu sama pada setiap kebangkitan. Pola ketiga justru khas intermiten — aplikasi hanya kalah di detik-detik awal, dan begitu database selesai inisialisasi, container yang dibangkitkan ulang berhasil hidup. Pola OOM bisa keduanya: deterministik bila limit memang lebih kecil dari kebutuhan start, atau berkala bila kebocoran perlahan menggembungkan pemakaian memori — kasus lengkapnya sudah kita bedah di chapter Debugging Container pada section Docker dalam Pengembangan.

Skenario Nyata dari Laporan sampai Pemulihan #

Melanjutkan kasus worker-gambar: traceback menunjuk berkas /app/config/config.yml yang tidak ada, padahal service seharusnya menerima berkas itu lewat mount. Karena container sibuk mati-nyala dan exec hanya bisa masuk ke container berstatus Up, konfigurasi mount dibaca dari docker inspect dengan template yang kita kenal dari chapter Docker Inspect:

bash
$ docker inspect worker-gambar --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{println}}{{end}}'

Output yang Dihasilkan:

text
/home/siti/toko/konfig/config.YAML -> /app/config/config.YAML

Tersangka tertangkap pada kolom kanan: berkas host bernama config.YAML (Siti menyimpannya dengan ekstensi huruf besar, kebiasaan lama editornya) dipasang ke /app/config/config.YAML — destination ikut menyalin nama berkas host — sedangkan aplikasi membaca /app/config/config.yml. Selisih satu huruf besar-kecil pada ekstensi; bagi Linux yang case-sensitive, keduanya dua nama berkas yang berbeda. FileNotFoundError turun, container mati, daemon membangkitkan, dan siklus berulang dua puluh tiga kali. Perbaikannya satu baris pada compose.yaml — normalisasi destination ke nama yang dibaca aplikasi:

yaml
services:
  worker-gambar:
    image: worker:1.0
    volumes:
      - ./konfig/config.YAML:/app/config/config.yml
    restart: always

Setelah docker compose up -d worker-gambar membuat ulang container dengan mount yang benar, verifikasi berjalan dua arah — ke atas dan ke bawah:

bash
$ docker inspect worker-gambar --format 'RestartCount={{.RestartCount}}'

Output yang Dihasilkan:

text
RestartCount=0
bash
$ docker logs --tail 3 worker-gambar

Output yang Dihasilkan:

text
[2026-10-14 15:47:11] worker-gambar 1.0 dimulai
[2026-10-14 15:47:11] konfigurasi /app/config/config.yml berhasil dibaca
[2026-10-14 15:47:12] worker siap — mendengarkan antrean gambar

RestartCount yang kembali ke nol adalah bukti container benar-benar baru — penghitung tidak diwarisi antar kebangkitan, melainkan direset saat container dibuat ulang. Log yang menyapa “worker siap” dan antrean gambar yang kembali terproses menutup insiden ini.

Varian Kedua: Dibunuh Kelaparan Memori #

Varian crash loop yang tidak menampilkan traceback sama sekali justru lebih sering membingungkan. Service api-kecil — endpoint ringkas untuk aplikasi kasir dengan mem_limit: 256m — mati setiap setengah jam berstatus Restarting (137), dan log terakhirnya sama sekali normal:

text
[2026-10-14 16:40:02] permintaan #4812 selesai dalam 312ms

Tidak ada jejak galat — khas proses yang dibunuh dari luar tanpa sempat pamit, sebagaimana kita saksikan pada chapter Membaca Container Exit. Jejak ketiga langsung menjawab: ExitCode=137, OOMKilled=true. Konfirmasi terakhir turun ke catatan kernel:

bash
$ sudo dmesg | grep -i 'out of memory'

Output yang Dihasilkan (dipotong):

text
[10244.882015] Memory cgroup out of memory: Killed process 3120 (python3) total-vm:523468kB, anon-rss:247804kB

Kernel mengakui pembunuhan itu: cgroup container kehabisan 256 MiB. Perbaikan jangka pendek menaikkan langit-langitnya di compose.yaml:

yaml
services:
  api-kecil:
    image: api-kecil:1.2
    mem_limit: 512m
    restart: always

Setelah container dibuat ulang, kamera resource memverifikasi bahwa aplikasi kini bernapas:

bash
$ docker stats --no-stream api-kecil

Output yang Dihasilkan:

text
CONTAINER ID   NAME       CPU %     MEM USAGE / LIMIT   MEM %    NET I/O        BLOCK I/O     PIDS
c50d38e1a9b7   api-kecil  2.14%     298.7MiB / 512MiB  58.34%   1.8MB / 620kB  41.2MB / 0B   9

298.7MiB / 512MiB pada MEM USAGE / LIMIT menegaskan kebutuhan aslinya memang melampaui 256 MiB. Menaikkan limit tanpa pengawasan hanya menunda kematian berikutnya: pemakaian yang terus menempel di plafon baru menandakan kebocoran yang harus diperbaiki di kode — pembahasan penuh batas memori, swap, dan keputusan angkanya adalah urusan section Manajemen Resource.

Menjinakkan Restart Policy untuk Melihat Galat #

Satu keterampilan penutup yang membedakan penyelidik sabar dari yang terburu-buru: menghentikan sementara kebangkitan otomatis. Crash loop yang terus berjalan membuat log tergeser-geser dan host sibuk memutar container yang jelas-jelas akan mati lagi:

bash
$ docker update --restart=no worker-gambar

Output yang Dihasilkan:

text
worker-gambar

Perintah docker update --restart=no mengubah kebijakan tanpa membuat ulang container; kegagalan berikutnya menjadi kegagalan terakhir, sehingga galat mentahnya berhenti di tempat dan bisa dibaca dengan tenang lewat rantai empat jejak. Setelah akar masalah ditemukan dan diperbaiki, kembalikan kebijakan yang semestinya — restart: on-failure atau unless-stopped — sesuai peta chapter Restart Policy.

Dari exit code sampai crash loop, sepanjang section ini kita mengumpulkan satu resep per keluarga kegagalan: container mati, jaringan putus, storage bengkak, build gagal, aplikasi berulang mati. Pertanyaan penutupnya berbeda corak: bagaimana merangkai seluruh resep tersebut menjadi kerangka kerja yang bisa diulang pada masalah yang belum pernah kita lihat sama sekali? Chapter penutup section ini, Strategi Diagnostik, menjawabnya dengan lima langkah yang sistematis.

Referensi #