Lima chapter sebelumnya mengurus satu keluarga pertanyaan: bagaimana caranya membentuk perilaku container sejak lahir — menimpa perintah bawaan lewat Command Override, menyuntik konfigurasi lewat Environment Variable, memilih identitas lewat User dan Permissions, membuka pintu ke luar lewat Port Publishing, dan mematok titik berpijak lewat Working Directory. Semua flag itu bersifat statis: nilainya dipilih sekali pada docker run dan berlaku selama container hidup. Pertanyaan yang diwariskan penutup chapter sebelumnya kini menanti jawaban: siapa yang membangkitkan container setelah kejatuhan — crash, sinyal 9 dari OOM killer, atau host yang restart di tengah malam? Sebuah server sungguhan tidak hanya hidup; server reboot untuk pemeliharaan kernel, aplikasi mati karena bug, dan pada pukul tiga pagi tidak ada manusia yang bangun untuk mengetik docker start. Docker menjawab kebutuhan itu dengan satu flag kecil bernama --restart, yang di baliknya berdiri empat kebijakan dengan perilaku yang sangat berbeda satu sama lain — dan salah satunya menyimpan perbedaan halus yang hanya terlihat ketika daemon sendiri mati lalu hidup kembali.

Daemon sebagai Pengawas Container #

Mari kita luruskan dulu pelakunya, karena jawaban atas “siapa yang membangkitkan container” tidak berada di dalam container itu sendiri. Container adalah proses biasa di host — sebagaimana kita telusuri di Chapter Arsitektur Docker — dan proses yang mati tidak bisa membangkitkan dirinya sendiri. Yang mengawasi adalah dockerd: lewat containerd, daemon menerima kabar setiap kali PID 1 sebuah container berakhir, lengkap dengan exit code-nya. Pada momen itu daemon membuka catatan konfigurasi container, membaca field HostConfig.RestartPolicy, dan memutuskan: bangkitkan ulang, atau biarkan tergeletak.

Kebijakan restart karenanya hidup di sisi daemon, bukan di sisi image atau aplikasi. Konsekuensinya ada dua. Pertama, kebijakan bisa kita baca dan bahkan ubah setelah container diciptakan — docker update akan membuktikannya sebentar lagi. Kedua, keputusan daemon ikut berperan ketika daemon itu sendiri mati dan menyala kembali: daemon yang bangun akan menoleh ke daftar container yang ditinggalkannya, lalu memperlakukan masing-masing sesuai kebijakannya — dan di sinilah perbedaan paling halus antar kebijakan akan tampak.

Empat Kebijakan Restart #

Dokumentasi resmi Docker menyediakan tepat empat kebijakan, dan ketiganya yang aktif bekerja dengan cara yang berbeda tergantung dua hal: exit code yang ditinggalkan proses utama, dan apakah container berhenti karena mati sendiri atau karena dihentikan tangan manusia. Tabel berikut merangkum keempatnya:

KebijakanExit ≠ 0Exit 0Di-stop manual, lalu daemon start ulang
no (default)tetap berhentitetap berhentitetap berhenti
on-failure[:maks]dibangkitkan ulang, maksimal maks kalitetap berhentitetap berhenti
alwaysdibangkitkan ulangdibangkitkan ulangdibangkitkan kembali oleh daemon
unless-stoppeddibangkitkan ulangdibangkitkan ulangtetap berhenti

Mari kita telaah baris demi baris tabel di atas:

  1. no adalah kebijakan bawaan — tanpa flag --restart, container yang mati tetap mati; seluruh perilaku yang kita kenal sepanjang course sejauh ini memang berasal dari kebijakan ini.
  2. on-failure menimbang exit code — container dibangkitkan hanya bila PID 1 keluar dengan kode gagal; keluar dengan kode 0 berarti tugas selesai dan tidak ada alasan untuk mengulang. Angka setelah titik dua membatasi jumlah percobaan.
  3. always paling setia — container dibangkitkan apa pun exit code-nya, termasuk keluar rapi dengan 0; bahkan container yang kita hentikan sendiri secara manual akan dihidupkan kembali oleh daemon ketika daemon menyala ulang.
  4. unless-stopped menghormati keputusan manusia — perilakunya serupa always selama container dibiarkan hidup, tetapi keputusan kita untuk menghentikannya dicatat permanen: daemon yang start ulang pun tidak akan membangkitkannya.
  5. Seluruh kebijakan menghormati penghentian manual saat itu terjadi — docker stop dan docker kill tidak diikuti kebangkitan seketika; kebijakan baru “menagih” kewajibannya saat daemon restart, dan hanya always yang membayar tagihan itu.

Satu catatan lagi sebelum praktik: container yang sedang berjalan ketika daemon dimatikan — misalnya karena server reboot — akan dihidupkan kembali oleh daemon yang baru menyala, untuk kebijakan always maupun unless-stopped. Dengan begitu, skenario “server reboot dini hari” tertangani tanpa satu pun perintah manusia, selama image tersimpan lokal di mesin itu.

Menjalankan web dengan unless-stopped #

Sebagai bahan latihan, kita hidupkan kembali container web yang setia menemani kita — kali ini dengan kebijakan yang membuatnya tahan banting. Meja kita mesti dibersihkan lebih dulu: tiga container masih menyala dari lima chapter pertama section ini — web pembawa nginx dari Chapter Working Directory, serta myapp-prod dan db-katalog bekas demo environment variable. Ketiganya kita pamitkan sekaligus supaya setiap tabel pada chapter ini hanya menampilkan aktor yang sedang dibicarakan:

bash
$ docker rm -f web myapp-prod db-katalog

Output yang Dihasilkan:

text
web
myapp-prod
db-katalog

Kini web pengganti lahir dengan kebijakan bangkit otomatis. Kita sengaja tidak menerbitkan port apa pun — pemetaan port adalah urusan yang sudah kita kuasai pada Chapter Port Publishing — karena fokus kita murni pada hidup-matinya proses, bukan pada lalu lintas jaringan:

bash
$ docker run -d --restart unless-stopped --name web nginx

Output yang Dihasilkan:

text
f7c2a91b3e55913c4a5f1c6c0a4d8e2b3f4a5b6c7d8e9f0a1b2c3d4e5f60718a
bash
$ docker ps

Output yang Dihasilkan:

text
CONTAINER ID   IMAGE   COMMAND                  CREATED              STATUS          PORTS   NAMES
f7c2a91b3e55   nginx   "/docker-entrypoint.…"   About a minute ago   Up 54 seconds           web

Mari kita telaah rangkaian di atas:

  1. --restart unless-stopped — kebijakan dibaca sekali oleh daemon saat container diciptakan, lalu disimpan pada konfigurasi container; flag ini tidak mengubah apa pun yang terlihat pada docker ps.
  2. ID yang akrab itu kembali — f7c2a91b3e55 adalah container web baru dari image nginx yang sudah menetap lama di mesin kita, sehingga tidak ada satu baris unduhan pun.
  3. Kolom PORTS sengaja kosong — tanpa flag -p, nginx mendengarkan di dalam jaringan container saja; kebijakan restart dan penerbitan port adalah dua urusan yang saling bebas.

Kebijakan yang barusan kita pasang bisa dibaca kembali dari konfigurasinya, memakai template docker inspect yang kita kuasai sejak Section Docker Image:

bash
$ docker inspect --format '{{.HostConfig.RestartPolicy.Name}}' web

Output yang Dihasilkan:

text
unless-stopped

Mensimulasikan Crash #

Kebijakan pasif terlihat seperti hiasan sampai container benar-benar mati — maka kita jadikan mati. Cara paling jujur mensimulasikan crash adalah membunuh PID 1 dari dalam container itu sendiri, bukan lewat docker kill yang dihitung sebagai penghentian manual. docker exec membuka pintu ke dalam, dan kill -9 1 menjatuhkan sinyal SIGKILL tepat ke kepala proses utama:

bash
$ docker exec web sh -c 'kill -9 1'
$ docker ps

Output yang Dihasilkan:

text
CONTAINER ID   IMAGE   COMMAND                  CREATED         STATUS         PORTS   NAMES
f7c2a91b3e55   nginx   "/docker-entrypoint.…"   3 minutes ago   Up 5 seconds           web

Mari kita telaah apa yang barusan terjadi:

  1. docker exec tidak mencetak apa pun — perintah di dalam container berjalan senyap; satu-satunya jejaknya ada pada kolom STATUS.
  2. PID 1 tumbang oleh SIGKILL — nginx master mati seketika tanpa kesempatan beres-beres, dan container berhenti dengan exit code 137 alias 128 + 9, persis konvensi yang kita bedah pada Chapter Memahami Exit Code.
  3. Daemon membangkitkannya dalam hitungan detik — kolom STATUS kini membaca Up 5 seconds; container yang sama, ID yang sama, proses PID 1 yang baru — dan seluruh proses ini berlangsung tanpa satu tombol pun kita sentuh.
  4. Kebangkitan itu tercatat — daemon menghitung setiap restart pada field RestartCount, yang bisa kita baca dengan template inspect:
bash
$ docker inspect --format '{{.RestartCount}}' web

Output yang Dihasilkan:

text
1

Membatasi Percobaan dengan on-failure #

Kebijakan unless-stopped membangkitkan apa pun sebab kematiannya — termasuk keluar dengan kode sukses. Bagi pekerjaan batch, perilaku itu justru salah: skrip laporan mingguan yang selesai dengan exit 0 seharusnya dibiarkan pulang, bukan dijadwalkan ulang. Kebijakan on-failure membedakan kedua keadaan itu dengan menimbang exit code, dan angka setelah titik dua membatasi percobaannya agar container yang gagal permanen tidak berputar selamanya. Kita uji keduanya dengan alpine yang sudah lama menjadi korban sukarela kita.

Uji on-failure:3 dengan exit 1 #

Container berikut sengaja lahir untuk gagal: PID 1-nya hanya menjalankan satu instruksi exit 1 lalu tumbang, dan daemon diperintahkan mencoba maksimal tiga kali:

bash
$ docker run -d --name gagal --restart on-failure:3 alpine:3.20 sh -c 'exit 1'

Output yang Dihasilkan:

text
00aa78d878205b7e8490175c3a5d3511752de544825530b6430b1f1bca01105f

Bila kita buru-buru membuka docker ps -a, kita sempat menangkap kebijakan itu sedang bekerja — status Restarting dengan exit code kegagalan di dalam kurung:

bash
$ docker ps -a

Output yang Dihasilkan:

text
CONTAINER ID   IMAGE         COMMAND                  CREATED         STATUS                         PORTS   NAMES
9e1c4a7b2d58   alpine:3.20   "sh -c exit 1"           9 seconds ago   Restarting (1) 2 seconds ago           gagal
f7c2a91b3e55   nginx         "/docker-entrypoint.…"   4 minutes ago   Up 3 minutes                           web

Beberapa saat kemudian, seluruh jatah percobaan habis dan container menyerah. Kita baca catatan akhirnya:

bash
$ docker inspect --format '{{.RestartCount}} {{.State.Status}}' gagal

Output yang Dihasilkan:

text
3 exited
bash
$ docker inspect --format '{{.HostConfig.RestartPolicy.MaximumRetryCount}}' gagal

Output yang Dihasilkan:

text
3

Mari kita telaah rangkaian pembuktian di atas:

  1. Restarting (1) adalah status transisi — container terlihat pada docker ps -a dalam keadaan menunggu jeda ulang, dengan angka 1 di dalam kurung menandai exit code terakhir; status ini hanya muncul sesaat dan akan kita petakan formal pada Chapter Lifecycle Container.
  2. 3 exited membuktikan batas percobaan bekerja — daemon mencoba menghidupkan ulang tepat tiga kali, gagal tiga kali, lalu berhenti mencoba; container kini berstatus exited dan akan tetap demikian.
  3. MaximumRetryCount menyimpan angka dari flag — nilai on-failure:3 terurai menjadi nama kebijakan on-failure dan batas percobaan 3; tanpa angka, daemon mencoba tanpa batas selama exit code tetap gagal.

Exit 0 tidak dibangkitkan #

Sekarang kembarannya yang sukses — perintah yang sama, kebijakan yang sama, hanya exit code yang berbeda:

bash
$ docker run -d --name sukses --restart on-failure:3 alpine:3.20 sh -c 'exit 0'

Output yang Dihasilkan:

text
e8265d85490c2432d83996119c8e6c6389bd1b5943e25015d3673e07188102f6
bash
$ docker inspect --format '{{.RestartCount}} {{.State.Status}}' sukses

Output yang Dihasilkan:

text
0 exited

Mari kita telaah kontras kedua saudara itu:

  1. 0 exited tanpa perlawanan — container keluar dengan kode sukses, kebijakan on-failure tidak menemukan alasan untuk bangkit, dan RestartCount tetap nol; keluar rapi memang bukan kegagalan.
  2. Inilah pembeda kunci dari always dan unless-stopped — kedua kebijakan itu justru membangkitkan container yang keluar dengan 0, karena bagi keduanya “berhenti” sendiri sudah cukup alasan untuk bangkit kembali.

Mengubah Kebijakan Saat Berjalan #

Kebijakan restart tidak terkunci pada momen docker run. Perintah docker update menulis ulang bagian konfigurasi container kapan pun — container sedang berjalan maupun sedang tergeletak — tanpa menciptakan container baru. Kita ubah web menjadi container yang boleh mati damai:

bash
$ docker update --restart=no web

Output yang Dihasilkan:

text
web
bash
$ docker inspect --format '{{.HostConfig.RestartPolicy.Name}}' web

Output yang Dihasilkan:

text
no

Output web adalah konfirmasi baku perintah docker update: identifier yang kita sebut dicetak kembali, dan konfigurasi berubah seketika. Kini kita ulang percobaan crash yang tadi membangkitkan web:

bash
$ docker exec web sh -c 'kill -9 1'
$ docker ps -a

Output yang Dihasilkan:

text
CONTAINER ID   IMAGE         COMMAND                  CREATED          STATUS                          PORTS   NAMES
0a4b8c2d6e1f   alpine:3.20   "sh -c exit 0"           2 minutes ago    Exited (0) About a minute ago           sukses
9e1c4a7b2d58   alpine:3.20   "sh -c exit 1"           4 minutes ago    Exited (1) 3 minutes ago                gagal
f7c2a91b3e55   nginx         "/docker-entrypoint.…"   11 minutes ago   Exited (137) 8 seconds ago              web

Mari kita telaah baris web pada tabel di atas:

  1. Exited (137) dan tetap tergeletak — crash yang sama yang tadi membangkitkan web dalam lima detik kini meninggalkannya mati permanen; satu-satunya hal yang berubah adalah kebijakan yang terbaca daemon.
  2. Angka 137 tetap setia melaporkan sebab kematian — SIGKILL dari kill -9 1 kita; yang berubah bukan cara matinya, melainkan tanggapan daemon sesudahnya.

always vs unless-stopped Saat Daemon Start Ulang #

Perbedaan paling halus antara dua kebijakan setia itu baru berbicara setelah daemon mati. Kita susun panggungnya: web kita kembalikan ke unless-stopped lalu dihidupkan, kemudian dilahirkan web2 dengan always — dua kembar nginx dengan nasib yang akan berbeda. Empat perintah berikut dijalankan berurutan, dan lima baris output-nya masing-masing berasal dari perintah yang berbeda:

bash
$ docker update --restart unless-stopped web
$ docker start web
$ docker run -d --name web2 --restart always nginx
$ docker stop web web2

Output yang Dihasilkan:

text
web
web
32cbf8f11650921d2be9053b5a8a80f1fd3ba9b2a6d4d780771ded457259a17b
web
web2

Mari kita telaah asal tiap barisnya:

  1. Baris pertama dan kedua web — konfirmasi dari docker update dan docker start; kebijakan kembali menjadi unless-stopped dan proses PID 1 menyala kembali.
  2. Baris panjang heksadesimal — ID penuh container web2 yang baru diciptakan docker run dengan kebijakan always.
  3. Dua baris terakhir — docker stop menghentikan web dan web2 secara sopan; keduanya kini berstatus Exited (0) karena nginx menutup dirinya dengan rapi.

Sekarang momen yang dinanti: kita matikan daemon itu sendiri, lalu bangkitkan. Pada host Linux dengan systemd, perintahnya adalah sudo systemctl restart docker — perintah ini tidak mencetak apa-apa; daemon dimatikan dan dinyalakan kembali dalam diam beberapa detik, dan sepanjang masa transisinya CLI bisa sempat menjawab bahwa daemon belum bisa dihubungi. Pengguna Docker Desktop menggantinya dengan keluar dari aplikasi lalu membukanya kembali. Setelah daemon siap, kita lihat nasib kedua kembar itu:

bash
$ docker ps -a

Output yang Dihasilkan:

text
CONTAINER ID   IMAGE         COMMAND                  CREATED          STATUS                     PORTS   NAMES
a3f5c8e91d47   nginx         "/docker-entrypoint.…"   2 minutes ago    Up 14 seconds                      web2
0a4b8c2d6e1f   alpine:3.20   "sh -c exit 0"           5 minutes ago    Exited (0) 4 minutes ago           sukses
9e1c4a7b2d58   alpine:3.20   "sh -c exit 1"           7 minutes ago    Exited (1) 6 minutes ago           gagal
f7c2a91b3e55   nginx         "/docker-entrypoint.…"   14 minutes ago   Exited (0) 2 minutes ago           web

Mari kita telaah perbedaan nasib pada dua baris nginx:

  1. web2 bangkit sendiri — Up 14 seconds — kebijakan always memuat janji yang berlaku bahkan melintasi kematian daemon: container yang pernah dihentikan manual sekalipun akan dihidupkan kembali ketika daemon menyala ulang.
  2. web tetap tergeletak — Exited (0) 2 minutes ago — kebijakan unless-stopped mencatat keputusan kita menghentikannya secara permanen; daemon yang baru bangun membaca catatan itu dan menghormatinya.
  3. gagal dan sukses tidak ikut terlibat — keduanya berhenti karena kehabisan percobaan dan exit sukses, bukan karena dihentikan manual; keduanya tetap diam karena kebijakan on-failure memang tidak menagih apa-apa dari daemon yang bangkit.
  4. Inilah alasan unless-stopped menjadi kebijakan paling populer untuk server — container otomatis bangkit saat crash dan saat reboot, tetapi tetap tunduk saat kita dengan sengaja mematikannya untuk pemeliharaan.

Jeda Retry yang Berlipat Ganda #

Daemon tidak membangkitkan container yang gagal secepat kilat berulang-ulang. Antara tiap percobaan ada jeda tunggu yang dimulai dari 100 milidetik dan berlipat ganda pada tiap kegagalan — strategi backoff yang mencegah container bermasalah membanjiri daemon dan host dengan percobaan sia-sia:

Percobaan restart ke-Jeda tunggu sebelumnya
1100 ms
2200 ms
3400 ms
4800 ms
51600 ms
seterusnyaterus berlipat ganda

Pada uji gagal kita, tiga percobaan hanya memakan waktu sekitar tujuh ratus milidetik jeda total — terasa seketika. Tetapi bayangkan container yang gagal setiap kali menyala selama sepuluh detik: jeda yang berlipat ganda membuat percobaan kesepuluh menunggu hampir dua menit, memberi napas bagi host dan bagi kita untuk menyelidiki sambil kebijakan masih bekerja.

Seluruh aktor chapter ini kita bereskan dulu sebelum pindah topik — docker rm -f membersihkan empat bekas latihan dalam satu ayunan, baik yang masih hidup (web2 yang barusan dibangkitkan daemon) maupun yang sudah tergeletak:

bash
$ docker rm -f web web2 gagal sukses

Output yang Dihasilkan:

text
web
web2
gagal
sukses

Kini container kita bangkit sendiri saat crash dan saat reboot — tetapi kebangkitan itu punya musuh yang lebih sunyi: container yang bangkit hanya untuk menabrak langit-langit resource, atau justru tetangga yang merampas jatah sampai container kita sendiri tersedak. Masalah itu tidak diselesaikan dengan kebijakan hidup-mati, melainkan dengan batas jumlah — berapa core CPU dan berapa banyak memori yang boleh dimakan sebuah container. Chapter berikutnya, Resource Limit, memperkenalkan wilayah itu: noisy neighbor, cgroups sang penegak, dan perbedaan antara langit-langit keras dan jaminan kursi lunak.

Referensi #