Stack toko kini menyala dengan urutan yang terbaca baris demi baris — tetapi siklus hidup tidak berhenti di momen menyala. Suatu saat stack harus dimatikan: untuk deploy versi baru, mengganti konfigurasi, atau sekadar menutup laptop di penghujung hari. Setiap kali kita menjalankan docker compose stop, Compose mengirim sinyal SIGTERM ke proses PID 1 di dalam setiap container, lalu memasang pengatur waktu sepuluh detik sebelum mengirim SIGKILL yang tak bisa ditolak. Aplikasi yang tidak pernah belajar membaca sinyal itu akan mati dengan koneksi pool yang terpotong dan permintaan yang masih setengah diproses — curl yang sedang menunggu jawaban bisa menggantung atau menerima kegagalan di tengah jalan. Chapter ini membedah alur penghentian container, mempraktikkan dua nasib aplikasi yang berbeda di hadapan SIGTERM, lalu melengkapi compose.yaml dengan tenggang waktu yang manusiawi.
Alur Penghentian: SIGTERM, Tenggang Waktu, SIGKILL #
Mekanisme di balik docker compose stop sama dengan docker stop yang kita kenal dari section dasar CLI, hanya diterapkan ke banyak service sekaligus. Alurnya tiga babak: sinyal SIGTERM dikirim ke PID 1 container; daemon menunggu dengan masa tenggang bawaan 10 detik; bila proses belum keluar saat tenggang habis, SIGKILL menyusul dan container berhenti seketika. Bukan kebetulan bahwa proses yang dihentikan paksa meninggalkan jejak berupa exit code 137 — konvensi 128 + 9 yang kita hafal dari chapter Memahami Exit Code. Tiga angka berikut adalah kosakata lengkap penghentian:
| Exit code | Asal-usul | Arti bagi kita |
|---|---|---|
| 0 | Aplikasi keluar atas keputusannya sendiri setelah drain | Shutdown yang sopan — inilah target kita |
| 143 | 128 + 15 — proses berhenti karena SIGTERM | Wajar dan bukan galat: sinyal sampai dan diberlakukan tanpa penanganan |
| 137 | 128 + 9 — proses dibunuh SIGKILL | Tenggang habis: ada pekerjaan yang terpotong di tengah |
Baris 143 layak digarisbawahi. Melihat Exited (143) setelah docker compose stop bukan berarti aplikasi gagal — sinyal memang terkirim dan proses memang berhenti karenanya; docker stop pun menganggap hasil itu sukses. Yang hilang hanyalah kesopanan di sisi aplikasi: ia tidak diberi kesempatan menutup koneksi database, menuntaskan permintaan yang sedang digarap, dan memerah log terakhirnya. Selama masa tenggang itulah aplikasi yang sehat seharusnya menunaikan urutan pembersihan berikut: berhenti menerima permintaan baru, menyelesaikan permintaan yang sedang berjalan, menutup koneksi ke dependency dengan rapi, lalu keluar dengan kode 0. Urutan inilah yang disebut graceful shutdown — berhenti dengan pamit, bukan dilerengkan dari jurang.
Aplikasi yang Mengabaikan Sinyal #
Mari kita saksikan dulu nasib aplikasi yang belum memasang penangan sinyal. Tambahkan satu endpoint yang sengaja lambat di server.js — laporan tiga detik yang meniru pekerjaan nyata:
if (req.url === "/laporan") {
await new Promise((selesai) => setTimeout(selesai, 3000));
const { rows } = await pool.query("SELECT COUNT(*) AS jumlah FROM produk");
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ produk: rows[0].jumlah }));
return;
}Endpoint di atas diakses lewat proxy sebagai /api/laporan — awalan /api/ diganti menjadi / oleh proxy_pass, persis aturan yang kita bedah di chapter Reverse Proxy. Bangun ulang image lalu nyalakan stack, dan buka dua terminal. Terminal pertama mengalirkan lalu lintas tanpa henti:
while sleep 1; do curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/api/laporan; doneOutput yang Dihasilkan:
200
200
200Sementara lalu lintas mengalir, hentikan backend dari terminal kedua:
docker compose stop backendOutput yang Dihasilkan:
[+] Stopping 1/1
✔ Container toko-backend-1 Stopped 0.9sContainer berhenti dalam sekejap — dan tepat di situlah masalahnya. Terminal pertama melanjutkan ceritanya:
200
200
200
502
502Mari kita telaah rangkaian kejadian di atas:
Stopped 0.9sadalah alarm, bukan prestasi — proses Node tidak memasang penanganSIGTERM, sehingga sinyal berlaku dengan disposition bawaan: proses dihentikan saat sinyal diterima. Permintaan laporan yang sedang digarap mati di tengah jalan.502yang seketika — permintaan yang terputus membuat nginx melihat upstream menutup koneksi sebelum respons selesai, lalu menjawab502 Bad Gatewaykepada klien. Tanpa perantara proxy, klien bahkan bisa menggantung menunggu jawaban yang tak pernah tiba.- Pool terpotong tanpa pamit — sepuluh koneksi pool yang terbuka putus serentak; PostgreSQL mencatatnya sebagai klien yang menghilang:
docker compose logs dbOutput yang Dihasilkan (dipotong):
toko-db-1 | 2026-10-14 10:02:41.512 UTC [38] LOG: unexpected EOF on client connectionBaris unexpected EOF on client connection adalah sidang jari kematian yang kasar: server database mendapati lawan bicaranya menghilang tanpa mengirim pesan penutupan koneksi. Keadaan akhirnya terbaca jelas di docker compose ps -a: backend tercatat Exited (143) — bukan galat, tetapi juga bukan perpisahan yang layak.
Penangkap Sinyal di Kode Aplikasi #
Sekarang kita lengkapi server.js dengan upacara penutupan yang layak. Penangkap sinyal dipasang setelah server mulai mendengarkan port 3000:
process.on("SIGTERM", () => {
console.log("SIGTERM diterima — menutup pintu");
server.closeIdleConnections();
server.close(async () => {
console.log("permintaan tersisa selesai — menutup pool");
await pool.end();
console.log("pool db tertutup — keluar");
process.exit(0);
});
});Mari kita telaah langkah demi langkah upacara di atas:
process.on("SIGTERM", ...)— begitu penangan terpasang, Node tidak lagi membiarkan sinyal mematikan proses begitu diterima; kendali penuh berpindah ke fungsi kita, dan jam sepuluh detik daemon mulai berdetak di latar.server.closeIdleConnections()— koneksi HTTP yang menganggur ditutup segera; tanpa baris ini, satu koneksi idle yang lupa pergi cukup membuatclose()menunggu selamanya.server.close(callback)— listener berhenti menerima koneksi baru, sedangkan permintaan yang sedang digarap dibiarkan selesai; callback hanya terpanggil setelah semua koneksi aktif benar-benar kosong.pool.end()— seluruh anggota pool ditutup dengan jabat tangan PostgreSQL yang benar: driver mengirim pesan termination, server menutup sesinya dengan rapi, dan tidak ada lagiunexpected EOFdi log db.process.exit(0)— proses keluar atas keputusannya sendiri dengan kode 0; inilah yang membuatdocker compose ps -akelak menampilkanExited (0), bukan143.
Nyalakan ulang backend, ulangi lalu lintas di terminal pertama, lalu hentikan sekali lagi:
docker compose stop backendOutput yang Dihasilkan:
[+] Stopping 1/1
✔ Container toko-backend-1 Stopped 4.2sDurasi berubah dari 0,9 detik menjadi 4,2 detik — dan perubahan itu justru kabar baik. Mari kita telaah bukti-buktinya:
4.2sadalah durasi drain yang sesungguhnya — laporan tiga detik yang sedang digarap dibiarkan selesai, ditambah waktu menutup pool; aplikasi keluar atas kemauannya jauh sebelum tenggang habis.200terakhir tiga detik setelah stop — terminal pertama kini mencatat200 200 200 200 502: baris200paling akhir adalah permintaan yang sedang diproses ketikaSIGTERMtiba, dituntaskan sampai respons terkirim; hanya permintaan baru setelah pintu tertutup yang dijawab502oleh proxy.- Log backend mencatat tiga babak upacara —
SIGTERM diterima,permintaan tersisa selesai,pool db tertutup; sementara logdbtidak lagi mencatat satu pununexpected EOF. Exited (0)menutup verifikasi — penghentian yang sopan tuntas dengan angka paling sopan yang dimiliki Unix.
stop_grace_period: Tenggang yang Diatur per Service #
Masa tenggang sepuluh detik adalah pilihan bawaan, bukan hukum alam. Drain pada toko kita memakan sekitar empat detik, tetapi worker yang harus menguras antrean panjang atau aplikasi dengan ratusan koneksi bisa membutuhkan lebih. Compose menyediakan kunci stop_grace_period per service:
backend:
build: ./api
image: toko-api:dev
stop_grace_period: 20sMari kita telaah perilaku kunci tersebut:
- Penghitungan mulai saat
SIGTERMterkirim — daemon memberi backend jendela 20 detik penuh untuk menuntaskan drain;SIGKILLbaru menyusul bila jendela itu terlampaui. - Angkanya harus melampaui anggaran drain di kode — tenggang 10 detik dengan drain 15 detik berarti
SIGKILLselalu datang lebih dulu; seluruh penangan sinyal menjadi sia-sia. Naikkan tenggang bila lalu lintas puncakmu membuat drain kerap panjang — 20 detik adalah pilihan yang wajar untuk shutdown yang berat. - Nilainya berupa durasi bebas —
30s,1m, hingga1m30ssemuanya sah; kuncinya padanan dari--timeoutpadadocker stop. - Tenggang bisa diatur ulang per pemanggilan —
docker compose stop -t 30 backendmemberi tenggang 30 detik khusus untuk penghentian itu, tanpa menyentuh compose.yaml; berguna untuk maintenance yang memang butuh ruang lebih.
Sinyal Harus Sampai: Exec Form Wajib #
Seluruh bangunan di atas bertumpu pada satu asumsi: SIGTERM benar-benar tiba di tangan aplikasi. Sinyal dikirim ke PID 1 — dan di sinilah bentuk penulisan CMD di Dockerfile menentukan nasibnya. Dockerfile api kita sejak chapter Application dan Database memakai bentuk exec:
CMD ["node", "server.js"]Bentuk itu membuat Node langsung menjadi PID 1, sehingga sinyal berhenti mendarat tepat di tangan proses yang memasang penangan. Bandingkan dengan bentuk shell yang kerap kita jumpai di image sepele:
CMD node server.jsPosisi PID 1 bisa diverifikasi langsung di dalam container yang sedang berjalan:
docker compose exec backend psOutput yang Dihasilkan:
PID USER TIME COMMAND
1 node 0:00 node server.js
7 node 0:00 psBaris pertama adalah kabar yang kita cari: aplikasi — bukan shell — menduduki kursi PID 1. Sebagai catatan penutup bagian ini: untuk aplikasi warisan yang sama sekali tidak bisa diajar menangkap sinyal, opsi --init pada docker run memasang proses init ringkas yang meneruskan sinyal dan memungut anak yatim — pola yang kita bahas pada chapter Container Init Process.
Stop Serentak dan Urutan yang Kita Atur Sendiri #
Start dibimbing rantai depends_on yang bertingkat; penghentian tidak mendapat perlakuan setimpal. docker compose stop mengirim SIGTERM kepada seluruh service dalam satu gelombang — tidak ada penunggal “backend selesai drain dulu” sebelum daemon menyentuh db. Hentikan seluruh stack yang sedang berjalan untuk menyaksikannya:
docker compose stopOutput yang Dihasilkan:
[+] Stopping 5/5
✔ Container toko-proxy-1 Stopped 1.1s
✔ Container toko-frontend-1 Stopped 0.8s
✔ Container toko-backend-1 Stopped 4.2s
✔ Container toko-cache-1 Stopped 0.6s
✔ Container toko-db-1 Stopped 2.3sMari kita telaah apa yang sebenarnya terjadi pada keluaran tersebut:
- Lima sinyal dilepas serentak — urutan baris hanyalah urutan selesai, bukan urutan penyentuhan; proxy dan db menerima
SIGTERMpada saat yang sama. - Tiap durasi menceritakan drain masing-masing — backend 4,2 detik menuntaskan laporan yang sedang digarap; db 2,3 detik menutup sesi-sesi koneksinya; cache 0,6 detik karena Redis memang sigap berpisah.
- Konsekuensi desainnya eksplisit — backend yang sedang drain bisa kehilangan db di tengah
pool.end()bila db kebetulan selesai lebih dulu; aplikasi harus tetap tangguh terhadap tetangga yang menghilang, persis semangat retry dari chapter Service Discovery yang bekerja dua arah.
Bila kita memang butuh urutan penghentian yang pasti — trafik berhenti masuk dulu, database mati paling akhir — urutan itu kita susun sendiri lewat pemanggilan eksplisit beruntun: docker compose stop proxy, lalu docker compose stop backend frontend, lalu cache, dan db terakhir. Sedangkan untuk sekadar “semua berhenti dan bersih”, docker compose down menuntaskannya dalam satu perintah: menghentikan seluruh service, menghapus container dan network, sambil membiarkan volume tetap utuh kecuali kita menuliskan -v. Perintah-perintah lengkap siklus hidup stack ini sudah kita rangkum di chapter Menjalankan dan Mengelola Stack.
Healthcheck Saat Stack Ditutup #
Satu perhatian terakhir: probe tidak berhenti hanya karena stack sedang ditutup. Selama container masih hidup, healthcheck terus berjalan sesuai intervalnya — dan selama masa drain, pintu /healthz memang sudah ditutup oleh server.close(). Akibatnya docker compose ps -a bisa sempat menampilkan backend dengan status (unhealthy) di detik-detik menjelang kematiannya. Status itu bukan diagnosis penyakit, melainkan konsekuensi wajar dari drain: aplikasi sedang menolak permintaan baru dengan sengaja. Baca konteksnya sebelum panik — unhealthy di tengah operasi stop yang sukses berbeda maknanya dari unhealthy di tengah siang yang sepi; pembedaan konteks semacam ini adalah keahlian diagnosa yang kita latih sepanjang section Observability dan Troubleshooting nanti.
Tabel Elemen Graceful Shutdown #
Seluruh perlengkapan berhenti yang sopan kini terkumpul dalam satu daftar periksa:
| Elemen | Peran | Wujud pada backend toko |
|---|---|---|
| Penangkap sinyal | Aplikasi mengambil kendali atas kematiannya sendiri | process.on("SIGTERM", ...) |
| Exec form | Sinyal tiba di aplikasi, bukan berhenti di shell | CMD ["node", "server.js"] |
| Drain permintaan | Permintaan yang sedang digarap dituntaskan | server.close() setelah closeIdleConnections() |
| Penutupan koneksi | Dependency diberi tahu dengan sopan, bukan ditinggalkan | pool.end() lalu process.exit(0) |
| Tenggang memadai | Ruang waktu untuk seluruh upacara | stop_grace_period: 20s |
Menyala dengan urutan dan berhenti dengan sopan — dua ujung siklus hidup toko kini sama-sama kita pegang. Seluruh pola sepanjang section ini, dari reverse proxy sampai penghentian yang rapi, tinggal dirangkai menjadi satu kesatuan yang bisa dipakai sehari-hari: satu repositori berisi stack development lengkap dengan hot reload, data contoh, dan alat bantu debug. Chapter penutup section, Local Development Stack, merakit semuanya.
Referensi #
- docker container stop — CLI Reference
— mekanisme
SIGTERM, masa tenggang, danSIGKILLpada penghentian container. - docker compose stop — CLI Reference
— cakupan perintah
stopbeserta flag tenggang-t. - Compose File Reference — Services
— definisi
stop_grace_perioddanstop_signalper service. - Dockerfile Reference
— bentuk exec dan shell pada
CMDdanENTRYPOINTyang menentukan nasib sinyal.