Rahasia toko kini menempati tempat yang benar di seluruh daur hidupnya, dan compose.yaml produksi sudah memuat hampir semua kunci yang kita impikan. Namun mengamankan barang adalah pekerjaan yang berbeda dari menjaga napas: stack yang rahasianya tertata tetap bisa mati diam-diam oleh crash tengah malam, oleh server yang reboot saat pemadaman listrik, atau oleh aplikasi yang menggantung tanpa menjawab sambil kolom STATUS tersenyum Up. Chapter ini merakit ketahanan produksi menjadi resep tiga lapis — probe yang jujur tertanam di image, kebijakan bangkit yang dideklarasikan di deployment, dan kalibrasi ritme yang menimpanya di compose — lalu menguji resep itu dari dua arah: stack yang menyala sehat dengan urutan yang terbaca, dan stack yang jatuh sakit dengan kronologi yang tercatat.

Tiga Lapis Ketahanan #

Satu mekanisme tidak akan pernah cukup, karena tiga musuh kita bekerja pada waktu yang berbeda. Aplikasi yang menggantung menipu docker ps dalam hitungan menit; crash membunuh proses dalam hitungan detik; reboot host menghapus semua container dalam sekejap. Tiga lapis berikut masing-masing menjawab satu musuh:

LapisTempat hidupMusuh yang dijawabDiatur oleh
HEALTHCHECK di imageDockerfileaplikasi menggantung — proses hidup tapi tak melayanipembuat image
restart: unless-stoppedcompose.yamlcrash proses dan reboot hostoperator deployment
healthcheck override di composecompose.yamlritme deteksi yang kurang pekat untuk produksioperator deployment

Pembagian perannya tegas: image membawa kejujuran, deployment membawa kebijakan. Pembuat image paling tahu bagaimana cara menanyakan “masih sanggup melayani?” kepada aplikasinya — jawaban itu ikut ke mana-mana bersama image. Operator deployment paling tahu berapa cepat insiden harus terdeteksi di lingkungannya dan kapan container layak dibangkitkan ulang — keputusan itu tidak boleh memaksa image dibangun ulang. Kombinasi keduanya memberi Compose kata terakhir tanpa menyentuh Dockerfile, persis perilaku penimpaan yang kita kenal dari Chapter Healthcheck dan Service Readiness.

Merancang /healthz yang Jujur #

Endpoint /healthz sudah menemani toko sejak Chapter Startup dan Dependency, dan bentuk produksinya di server.js 1.0.0 hanya berganti satu angka dari versi pengembangannya:

js
  if (req.url === "/healthz") {
    try {
      await pool.query("SELECT 1");
      res.writeHead(200).end("ok");
    } catch {
      res.writeHead(503).end("db tidak siap");
    }
    return;
  }

Mari kita telaah empat keputusan pada endpoint tersebut:

  1. pool.query("SELECT 1") — kueri termurah yang tetap menempuh jalur lengkap dari handler, ke pool, ke network, sampai database dan kembali; bila salah satu mata rantai putus, baris ini melempar galat sebelum jawaban sempat terkirim.
  2. 503 menggantikan 500 — Service Unavailable adalah kode yang jujur untuk kondisi “proses hidup, layanan belum sanggup”; 500 menyiratkan galat program, padahal di sini aplikasi sehat dan justru sedang melaporkan bahwa tetangganya sakit. Keduanya sama-sama gagal di mata probe — wget hanya mengenal 2xx sebagai lulus — tetapi kode yang benar membuat log dan dashboard bisa dibaca manusia.
  3. catch tanpa variabel — galat ditelan dengan sengaja: probe bertugas melaporkan, bukan memperbaiki, dan yang paling penting bukan menjatuhkan proses; galat tak tertangani di handler justru mematikan backend yang sedang sehat.
  4. return di tiap cabang — satu jalur keluar per permintaan; setelah menjawab 200 atau 503, handler selesai dan tidak jatuh ke rute lain.

Satu catatan untuk masa depan: dunia orchestration memisahkan dua pertanyaan yang kita gabungkan dalam satu endpoint. Liveness bertanya “apakah prosesnya hidup?” — jawaban salah berarti restart; readiness bertanya “apakah sanggup melayani?” — jawaban salah berarti sementara dikeluarkan dari rotasi tanpa dibunuh. /healthz kita adalah readiness: db yang mati membuat backend menjawab 503, dan itu berarti “jangan kirim saya pelanggan”, bukan “bunuh saya”. Kubernetes memisahkan keduanya sebagai probe terpisah, sementara Swarm tetap berpegang pada satu healthcheck — perbedaan yang kita bahas tuntas di Section Orchestration dan Ekosistem.

Menanam HEALTHCHECK di Image #

Lapis pertama sudah tertanam sejak image dibangun pada chapter pembuka section ini:

dockerfile
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
  CMD wget -qO- http://localhost:3000/healthz || exit 1

Recap singkat dari Chapter HEALTHCHECK: --timeout=3s menjauhi nilai bawaan 30 detik yang membuat probe menggantung terbaca lama; --start-period=10s adalah masa hangar ketika kegagalan tidak dihitung, memberi Node waktu membaca environment dan membuka pool sebelum dinilai; --retries=3 menuntut tiga kegagalan beruntun sebelum status turun. Setiap container dari image ini — di mana pun ia berjalan — diperiksa dengan cara yang sama, dan kolom STATUS berhenti berbohong sejak container pertama lahir.

Restart Policy untuk Produksi #

Lapis kedua menjawab kematian proses. Empat kebijakan yang kita bedah pada Chapter Restart Policy bermuara pada satu pilihan untuk produksi:

yaml
    restart: unless-stopped

Alasannya adalah tiga perilaku sekaligus. Setelah crash, container dibangkitkan ulang dengan jeda backoff yang dimulai 100 milidetik dan berlipat ganda pada setiap percobaan — crash loop yang kacau menjadi deret kebangkitan yang melambat dan memberi sistem ruang bernapas. Setelah reboot host, daemon yang baru menyala membangkitkan kembali container yang terakhir berjalan — skenario pemadaman dini hari tertangani tanpa satu pun perintah manusia, selama image tersimpan lokal. Dan saat operator menghentikan service dengan sengaja, keputusan itu dihormati permanen: always akan membangkitkan container kembali setelah daemon restart meski operator sudah menghentikannya, sedangkan unless-stopped taat — dan ketaatan kepada manusia adalah perilaku yang benar untuk mesin produksi.

Lapis kedua ini hanya bekerja bila aplikasi berani mati. Restart policy bereaksi terhadap exit code, jadi process.exit(1) yang cepat dari validasi konfigurasi pada Chapter Konfigurasi Production adalah kolaborator sempurna: kegagalan menjadi kejadian yang terlihat, daemon membangkitkan dengan backoff, dan log galat yang jelas tercetak berulang sampai seseorang memperbaikinya. Sebaliknya, aplikasi yang menggantung saat kondisinya tak tertolong mengambil alih seluruh drama: PID 1 tetap hidup, restart policy tidak pernah terpicu, dan stack tampak sehat sambil tidak melayani siapa pun. Aturan praktisnya dua sisi: gagal cepat untuk kondisi yang tidak mungkin pulih — konfigurasi hilang, berkas kredensial tak terbaca — dan coba ulang untuk kondisi yang bisa pulih, seperti db yang sedang dibangkitkan; retry dengan backoff di sisi aplikasi tetap merupakan sahabat terbaik restart policy, persis pelajaran Chapter Service Discovery.

Menyatukan Lapis di Compose #

Ketiga lapis kini bertemu dalam satu deklarasi service backend yang lengkap — bentuk final yang akan menemani toko sampai akhir section ini:

yaml
  backend:
    image: registry.example.com/toko/backend:1.0.0
    user: "1000:1000"
    env_file:
      - env/${APP_ENV:-dev}.env
    secrets:
      - source: db_password
        target: db_password
        uid: "1000"
        gid: "1000"
        mode: 0400
    read_only: true
    tmpfs:
      - /tmp
    restart: unless-stopped
    stop_grace_period: 20s
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/healthz"]
      interval: 15s
      timeout: 3s
      retries: 3
      start_period: 10s
      start_interval: 5s

Mari kita telaah empat keputusan yang menyatukan ketahanan di atas:

  1. Blok healthcheck menimpa probe image — interval dirapatkan dari 30s milik image menjadi 15s dan start_interval: 5s mempercepat pemeriksaan selama masa hangar; deteksi insiden produksi jadi dua kali lebih cepat tanpa satu pun byte image dibangun ulang — inilah hak kata terakhir operator.
  2. stop_grace_period: 20s — dua puluh detik upacara penutupan yang sudah teruji di Chapter Graceful Shutdown: SIGTERM diterima node sebagai PID 1, permintaan yang sedang digarap dituntaskan, pool ditutup dengan sopan, keluar dengan kode 0.
  3. secrets dengan mode: 0400 dan user: "1000:1000" — ketahanan tidak membeli kerahasiaan dengan mengorbankannya: kredensial tetap hanya terbaca oleh user node, dan filesystem tetap beku kecuali /tmp.
  4. Dua gerbang service_healthy — urutan hidup stack produksi sama ketatnya dengan versi pengembangannya: db dan cache harus membuktikan kesiapan sebelum backend dinyalakan, dan backend harus sehat sebelum proxy membuka pintu.

Menyalakan Stack dan Membaca Statusnya #

Stack dibongkar bersih dengan docker compose down — volume db-data tetap utuh — lalu dinyalakan penuh untuk menyaksikan rantai ketahanan bekerja dari nol:

bash
$ docker compose up -d

Output yang Dihasilkan:

text
[+] Running 6/6
 ✔ Network toko-produksi_backend-net    Created   0.2s
 ✔ Network toko-produksi_frontend-net   Created   0.2s
 ✔ Container toko-produksi-db-1         Healthy   4.8s
 ✔ Container toko-produksi-cache-1      Healthy   1.9s
 ✔ Container toko-produksi-backend-1    Healthy   12.6s
 ✔ Container toko-produksi-proxy-1      Started   0.6s

Enam sumber daya hadir dalam urutan yang bisa dibaca seperti naskah. Database memakai 4,8 detik untuk melewati pemulihan dan probe pg_isready; cache melengkapi dalam 1,9 detik dengan PONG; backend memakan 12,6 detik — sebagian besar adalah start_period 10 detik yang sengaja diberikan sebagai masa hangar, dengan start_interval 5 detik mengangkat status ke healthy segera setelah /healthz pertama menjawab; dan proxy menyala paling akhir karena pintu hanya layak dibuka setelah isi rumah siap. Daftar stack menegaskan keadaan akhirnya:

bash
$ docker compose ps

Output yang Dihasilkan:

text
NAME                      IMAGE                                    COMMAND                  SERVICE   CREATED         STATUS                    PORTS
toko-produksi-db-1        postgres:18-alpine                       "docker-entrypoint.s…"   db        2 minutes ago   Up 2 minutes (healthy)
toko-produksi-cache-1     redis:7-alpine                           "docker-entrypoint.s…"   cache     2 minutes ago   Up 2 minutes (healthy)
toko-produksi-backend-1   registry.example.com/toko/backend:1.0.0  "node server.js"         backend   2 minutes ago   Up About a minute (healthy)
toko-produksi-proxy-1     nginx:1.29-alpine                        "nginx -g 'daemon off…"  proxy     2 minutes ago   Up About a minute             0.0.0.0:8080->80/tcp, [::]:8080->80/tcp

Ketika Backend Menjadi Unhealthy #

Ujian ketahanan yang sesungguhnya datang dari keadaan sakit, dan Siti mengarang satu skenario latihan: db dihentikan sementara di siang yang sepi, lalu dinyalakan kembali.

bash
$ docker compose stop db

Output yang Dihasilkan:

text
[+] Stopping 1/1
 ✔ Container toko-produksi-db-1  Stopped   1.2s

Probe /healthz mulai gagal sejak detik itu; tiga kegagalan beruntun pada interval 15 detik menuntaskan masa probation, dan daftar stack kini memperlihatkan kejujuran yang kita bayar mahal di seluruh section ini:

bash
$ docker compose ps

Output yang Dihasilkan:

text
NAME                      IMAGE                                    COMMAND                  SERVICE   CREATED         STATUS                        PORTS
toko-produksi-db-1        postgres:18-alpine                       "docker-entrypoint.s…"   db        6 minutes ago   Exited (0)
toko-produksi-cache-1     redis:7-alpine                           "docker-entrypoint.s…"   cache     6 minutes ago   Up 6 minutes (healthy)
toko-produksi-backend-1   registry.example.com/toko/backend:1.0.0  "node server.js"         backend   6 minutes ago   Up 5 minutes (unhealthy)
toko-produksi-proxy-1     nginx:1.29-alpine                        "nginx -g 'daemon off…"  proxy     6 minutes ago   Up 5 minutes                 0.0.0.0:8080->80/tcp, [::]:8080->80/tcp

Perhatikan baris backend: prosesnya tetap Up sementara statusnya unhealthy — dua laporan yang berbeda sumber dan, untuk kasus ini, keduanya benar. Di sinilah recap dari Chapter Container Health harus dibawa terang: Docker tidak memulai ulang container yang unhealthy. Restart policy hanya membaca exit code, healthcheck hanya menulis laporan; container yang sakit akan terus berjalan sampai sembuh sendiri atau sampai sesuatu yang lain menyentuhnya. Skenario kita berakhir bahagia — db dinyalakan kembali dengan docker compose start db, probe berikutnya lulus, dan status pulih sendiri dalam satu interval:

bash
$ docker events --filter event=health_status

Output yang Dihasilkan (dipotong):

text
2026-10-16T10:29:04.318405512+07:00 container health_status: healthy e7a13c92f5b0d84a6c1e9372a5d84f6b0c2e8d4a6f1b3c5e7d9a0f2b4c6e8d1a (image=registry.example.com/toko/backend:1.0.0, name=toko-produksi-backend-1)
...
2026-10-16T10:41:47.882104369+07:00 container health_status: unhealthy e7a13c92f5b0d84a6c1e9372a5d84f6b0c2e8d4a6f1b3c5e7d9a0f2b4c6e8d1a (image=registry.example.com/toko/backend:1.0.0, name=toko-produksi-backend-1)
...
2026-10-16T10:44:21.516928207+07:00 container health_status: healthy e7a13c92f5b0d84a6c1e9372a5d84f6b0c2e8d4a6f1b3c5e7d9a0f2b4c6e8d1a (image=registry.example.com/toko/backend:1.0.0, name=toko-produksi-backend-1)

Mari kita telaah kronik tersebut dengan anatomi yang kita hafal dari Chapter Docker Events:

  1. Stempel waktu konsisten zona +07:00 — tiga kejadian berbagi format penuh tanggal, jam berpresisi nanodetik, dan zona WIB; jarak antar baris bisa dihitung langsung: sakit selama dua menit tiga puluh empat detik.
  2. ID penuh 64 karakter heksadesimal — bukan bentuk pendek 12 seperti di ps; ID yang sama pada ketiga baris menegaskan satu container yang sama mengalami tiga musim.
  3. Atribut image dan name menempel pada setiap baris — kronik terbaca tanpa menyalakan satu pun perintah lain: image mana yang sakit, service mana yang terdampak, kapan mulainya, kapan pulihnya.

Untuk keadaan yang tidak sembuh sendiri, tiga pilihan menutup celah antara laporan dan tindakan. Pilihan pertama adalah tangan manusia: docker compose restart backend saat insiden — sederhana, terukur, dan tepat untuk tim kecil yang menjaga satu-dua stack. Pilihan kedua adalah watchdog pihak ketiga seperti autoheal yang memulai ulang container ber-status unhealthy secara otomatis — berharga untuk jam-jam tanpa orang berjaga, dengan risiko klasiknya sendiri: restart yang menutup gejala sebelum penyebabnya dibaca. Pilihan ketiga adalah naik kelas platform: Swarm dan Kubernetes menyatukan status kesehatan ke dalam keputusan penjadwalan — container yang sakit diganti container baru di tempat lain, bukan sekadar dihidupkan ulang di tempat yang sama — dan itulah peta yang menunggu di Section Orchestration dan Ekosistem.

Stack toko kini tahan guncangan dengan cara yang bisa dijelaskan baris demi baris: probe yang jujur, kebijakan bangkit yang taat, dan kronik yang merekam semua peralihan status. Satu lubang terakhir memisahkan stack ini dari julukan produksi yang sesungguhnya: pintunya masih polos HTTP di port 8080, tanpa gembok, tanpa sertifikat, tanpa janji kepada browser bahwa data pelanggan dienkripsi dalam perjalanan. Chapter Reverse Proxy dan TLS memasang lini depan itu — terminasi TLS di proxy, sertifikat ACME dengan perpanjangan otomatis, dan konfigurasi nginx yang tetap waras ketika IP backend berganti di baliknya.

Referensi #