NestJS Health Checks with Terminus for Kubernetes & Docker

Foto oleh kewl via Openverse (CC BY 2.0)
Liveness probe menjawab apakah proses itu sendiri masih berjalan dan responsif, dan seharusnya hanya memeriksa sinyal di dalam proses seperti penggunaan heap memory — bukan dependency eksternal. Readiness probe menjawab apakah instance tersebut saat ini bisa melayani traffic, sehingga perlu memeriksa hal seperti konektivitas database. Mencampur keduanya bisa membuat gangguan database memicu Kubernetes me-restart semua pod, mengubah gangguan kecil yang bisa pulih sendiri menjadi outage penuh.
Jalankan npm install --save @nestjs/terminus, lalu impor TerminusModule ke dalam HealthModule khusus. Setelah itu kamu bisa menyuntikkan HealthCheckService beserta health indicator bawaan apa pun (TypeOrmHealthIndicator, MemoryHealthIndicator, DiskHealthIndicator, dan lainnya) ke HealthController lalu memanggil health.check() dengan array fungsi indicator.
Bisa — Terminus menyediakan MongooseHealthIndicator untuk koneksi MongoDB berbasis Mongoose, yang bekerja dengan cara yang sama seperti pingCheck di TypeOrmHealthIndicator: menjalankan pemeriksaan ringan terhadap koneksi aktif dan melaporkannya sebagai up atau down di dalam response health gabungan.
Kamu mengekspos dua endpoint HTTP, misalnya /health/live dan /health/ready, lalu mengarahkan livenessProbe.httpGet Kubernetes ke endpoint liveness dan readinessProbe.httpGet ke endpoint readiness. livenessProbe mengontrol apakah Kubernetes me-restart pod, sedangkan readinessProbe mengontrol apakah pod menerima traffic dari Service — keduanya dikonfigurasi secara terpisah dengan ambang waktu masing-masing.
Readiness check sebaiknya memverifikasi segala hal yang dibutuhkan agar request benar-benar bisa dilayani dengan sukses — ping database, ruang disk, keterjangkauan service downstream. Liveness check sebaiknya tetap terbatas pada sinyal tentang proses itu sendiri, seperti penggunaan heap atau kesehatan event loop, supaya gangguan eksternal tidak pernah membuat Kubernetes me-restart pod yang sebenarnya sehat.

Foto oleh kewl via Openverse (CC BY 2.0)
Ketika service NestJS berjalan di balik Kubernetes atau Docker, orchestrator perlu cara yang andal untuk menjawab dua pertanyaan yang sangat berbeda: apakah prosesnya masih hidup, dan apakah service ini benar-benar bisa melayani traffic sekarang. Menjawab keduanya dengan benar adalah kunci agar rolling deploy, auto-healing, dan load balancing berjalan tanpa menjatuhkan request. Package resmi @nestjs/terminus menyediakan toolkit kecil yang teruji untuk kebutuhan ini, dan artikel ini membahas cara membangun health module yang sesungguhnya dari nol, termasuk perbedaan yang sering disalahpahami banyak tim: mencampur liveness dan readiness menjadi satu endpoint yang sama.
Terminus dirilis sebagai package terpisah, bukan bagian dari core NestJS, jadi kamu menginstalnya sendiri dan mendaftarkan TerminusModule di dalam HealthModule khusus. Pendekatan ini menjaga urusan health check tetap terpisah dari module bisnis, dan memudahkan pengecualian health module dari guard atau interceptor global yang seharusnya tidak berjalan setiap kali ada ping liveness. Setelah TerminusModule diimpor, kamu mendapat akses ke HealthCheckService dan sejumlah provider health indicator bawaan yang bisa disuntikkan ke controller mana pun.
npm install --save @nestjs/terminus
# health.module.ts
import { Module } from "@nestjs/common"
import { TerminusModule } from "@nestjs/terminus"
import { HealthController } from "./health.controller"
@Module({
imports: [TerminusModule],
controllers: [HealthController],
})
export class HealthModule {}Blok inti dari semuanya adalah HealthCheckService.check(), yang menerima array fungsi indicator dan menggabungkan hasilnya menjadi satu response dengan status keseluruhan ok atau error, lengkap dengan rincian per indicator. Setiap fungsi indicator disediakan oleh kelas health indicator yang bisa disuntikkan — Terminus menyediakan beberapa di antaranya secara bawaan untuk dependency infrastruktur paling umum yang biasa disentuh backend service.
Jika service kamu memakai TypeORM, TypeOrmHealthIndicator.pingCheck() menjalankan query ringan ke koneksi yang dikonfigurasi dan melaporkannya sebagai up atau down. Tim yang memakai Mongoose punya padanan MongooseHealthIndicator — pola yang sama, gaya API pingCheck yang sama, hanya menyasar koneksi ODM yang berbeda. Ini adalah check paling penting untuk readiness probe, karena service yang tidak bisa menjangkau database-nya tidak seharusnya menerima traffic meski proses Node.js itu sendiri sepenuhnya sehat.
MemoryHealthIndicator.checkHeap() dan checkRSS() menjaga agar kebocoran memory yang lambat tidak diam-diam menurunkan performa pod sebelum akhirnya kena OOM-kill, sementara DiskHealthIndicator.checkStorage() melaporkan status unhealthy begitu volume yang di-mount melewati persentase penggunaan yang dikonfigurasi. Keduanya adalah sinyal level proses, bukan sinyal dependency eksternal, dan itulah sebabnya keduanya cocok berada di jalur liveness — mereka menjawab apakah container itu sendiri dalam kondisi layak, terlepas dari apa pun yang terjadi di luar.
// health.controller.ts
import { Controller, Get } from "@nestjs/common"
import {
HealthCheckService,
HealthCheck,
TypeOrmHealthIndicator,
MemoryHealthIndicator,
DiskHealthIndicator,
} from "@nestjs/terminus"
@Controller("health")
export class HealthController {
constructor(
private health: HealthCheckService,
private db: TypeOrmHealthIndicator,
private memory: MemoryHealthIndicator,
private disk: DiskHealthIndicator,
) {}
// Liveness: is the Node.js process itself still running and
// responsive? No dependency calls — Kubernetes uses this to decide
// whether to KILL and restart the pod.
@Get("live")
@HealthCheck()
checkLiveness() {
return this.health.check([
() => this.memory.checkHeap("memory_heap", 300 * 1024 * 1024),
])
}
// Readiness: can this instance actually serve traffic right now?
// Checks the database connection and disk headroom — Kubernetes
// uses this to decide whether to ROUTE traffic to the pod.
@Get("ready")
@HealthCheck()
checkReadiness() {
return this.health.check([
() => this.db.pingCheck("database"),
() =>
this.disk.checkStorage("storage", {
path: "/",
thresholdPercent: 0.9,
}),
])
}
}Jaga agar liveness check hanya bergantung pada sinyal di dalam proses — ukuran heap, responsivitas event loop, mungkin check disk yang sederhana. Begitu liveness probe memanggil database atau API downstream, gangguan yang tidak berhubungan bisa berubah menjadi badai restart yang menimpa seluruh deployment.
Liveness probe hanya menjawab satu pertanyaan: apakah proses ini macet atau crash sehingga perlu di-kill dan di-restart. Ia tidak boleh bergantung pada sistem eksternal — jika database mengalami gangguan lima menit, Kubernetes tidak seharusnya mulai me-restart semua pod di deployment, karena itu hanya menambah badai restart di atas situasi yang sudah buruk. Readiness probe menjawab pertanyaan yang berbeda: apakah instance ini secara spesifik bisa melayani traffic sekarang. Wajar — bahkan diharapkan — jika readiness probe gagal sementara liveness tetap hijau: itulah sinyal yang dibutuhkan Kubernetes untuk menarik pod keluar dari load balancer tanpa membunuhnya, lalu mengembalikannya begitu dependency pulih.
Kesalahan umum adalah menaruh ping database di endpoint liveness, bukan readiness. Jika database restart atau mengalami gangguan jaringan sebentar, Kubernetes akan menganggap semua pod mati dan me-restart seluruh deployment — mengubah gangguan dependency yang sebenarnya bisa pulih sendiri menjadi outage penuh, ditambah cold-start latency.
Setelah /health/live dan /health/ready ada sebagai endpoint HTTP yang nyata, menghubungkannya ke infrastruktur sebagian besar tinggal konfigurasi, bukan kode lagi. Instruksi HEALTHCHECK di Docker melakukan polling ke satu endpoint secara berkala dan menandai container unhealthy setelah sejumlah kegagalan berturut-turut — kebanyakan tim mengarahkannya ke endpoint readiness supaya `docker ps` mencerminkan apakah container benar-benar bisa mengerjakan tugasnya. Kubernetes memisahkan konsep ini secara native: livenessProbe mengontrol restart, readinessProbe mengontrol apakah pod menerima traffic dari Service, dan keduanya dikonfigurasi terpisah dengan initialDelaySeconds, periodSeconds, dan failureThreshold masing-masing.
# Dockerfile
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health/ready || exit 1
# kubernetes deployment.yaml
livenessProbe:
httpGet:
path: /health/live
port: 3000
initialDelaySeconds: 10
periodSeconds: 15
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 3Service yang butuh 30 detik untuk memanaskan connection pool atau memuat cache besar di memory menciptakan konflik tuning yang nyata: liveness probe yang cukup toleran untuk jendela startup itu juga jadi terlalu longgar untuk mendeteksi hang yang sungguhan di kemudian hari. startupProbe di Kubernetes menyelesaikan ini dengan bersih — menonaktifkan check liveness dan readiness sepenuhnya sampai startup probe itu sendiri berhasil sekali, setelah itu probe normal mengambil alih dengan timing yang lebih ketat. Ini layak dipakai kapan pun aplikasi NestJS punya logika bootstrap yang cukup lambat — inisialisasi module, pemanasan cache, atau pengecekan migrasi awal.
Jika kamu berada di balik API gateway atau service mesh yang juga melakukan polling health sendiri, pastikan itu diarahkan ke /health/ready, bukan /health/live — kalau tidak, gangguan kecil pada database bisa salah dilaporkan seolah seluruh node sedang down.
Sebelum mempercayai endpoint ini di cluster, coba langsung: curl localhost:3000/health/live seharusnya mengembalikan 200 dengan status ok dalam kondisi normal, dan kamu bisa memaksa readiness gagal dengan mematikan container database lokal lalu mengamati /health/ready berubah menjadi 503 dengan indicator database melaporkan down. Pengecekan manual singkat ini menangkap kesalahan konfigurasi — seperti tidak sengaja menghubungkan ping database ke jalur liveness — sebelum itu menyebabkan insiden di production.
Terminus adalah package yang kecil, tapi memastikan pembagian liveness dan readiness benar adalah salah satu detail yang membedakan service yang menurun secara graceful saat terjadi gangguan sebagian, dari service yang malah berujung pada badai restart penuh begitu satu dependency tersendat.