# Deploy NestJS ERP API ke Managed PaaS: Probe dan Migration

> Checklist yang saya jalankan sebelum NestJS ERP API mendekati managed PaaS: shutdown hooks, readiness endpoint yang tidak me-restart pod yang sehat, migration yang jalan tepat sekali di semua replica, dan hitungan connection pool.

URL: https://www.matthewswong.com/id/blog/deploy-nestjs-erp-api-paas-guide/

- Author: Matthews Wong (https://www.matthewswong.com/id/about/)
- Published: 2026-09-04
- Updated: 2026-09-04
- Category: Backend

> **Key takeaway:** Men-deploy NestJS ERP API ke managed PaaS butuh empat hal yang tidak dilakukan framework secara default: port dan host yang diambil dari platform, enableShutdownHooks supaya posting yang sedang jalan selesai sebelum pod mati, readiness endpoint yang dipisahkan dari liveness, dan database migration yang dijalankan di satu release job, bukan di bootstrap setiap replica.

Deploy-nya hijau, tapi API-nya salah. Tiga replica sebuah ERP backend baru selesai rolling, schema sudah maju, dua pod masih menjalankan build sebelumnya di atas schema itu, dan satu supplier invoice yang masuk lewat load balancer mendarat di pod mana pun yang menjawab lebih dulu. Tidak ada yang crash, tidak ada alert, dan angkanya sekadar bukan angka yang benar.

Ini checklist yang sekarang saya jalankan sebelum sebuah ERP API mendekati managed platform: apa yang dibutuhkan NestJS di production, health endpoint mana yang benar-benar dipanggil platform, cara membuat migration jalan tepat sekali di semua replica, cara menghitung connection pool terhadap batas koneksi database, dan pekerjaan mana yang sebaiknya tidak pernah tinggal di web service. Semua perilaku framework dan platform di bawah diambil dari dokumentasi NestJS, Kubernetes, Prisma, TypeORM dan PostgreSQL yang dikutip di bagian akhir.

## ERP API bukan web app yang kebetulan punya database

Beda ERP backend dengan CRUD API biasa bukan soal traffic, tetapi karena setiap write-nya adalah catatan keuangan. Render halaman yang terduplikasi tidak kelihatan. Goods receipt yang terduplikasi berarti stock adjustment, jurnal koreksi, catatan audit, dan satu percakapan dengan tim finance yang dibuka dengan kata kenapa. Jadi setiap keputusan deployment diukur dengan satu pertanyaan: apakah rollout ini bisa membuat satu write terjadi dua kali, atau mendarat di schema yang tidak diharapkan oleh code yang sedang berjalan?

Pertanyaan itu diam-diam mengeliminasi beberapa hal yang membuat managed platform terasa nyaman. Autoscaling adalah peristiwa correctness, bukan cuma soal biaya, karena ia mengalikan apa pun yang dilakukan process saat boot. Rolling deploy berarti dua build membaca dan menulis tabel yang sama selama rolling berlangsung. Pod yang restart berarti ada HTTP request yang terpotong di tengah transaction. Semua ini biasa saja untuk service yang direplikasi. Yang tidak biasa adalah konsekuensinya dihitung dalam rupiah.

## Yang dibutuhkan NestJS sebelum keluar dari laptop

File main.ts hasil generate adalah file development, dan ada tiga baris yang harus berubah sebelum deployment. Baca port dari environment platform, jangan hardcode 3000, karena runtime menyuntikkan PORT lalu melakukan probe ke port itu. Tentukan host secara eksplisit, supaya tidak ada yang menyalin contoh localhost ke dalam container lalu habis satu sore memikirkan kenapa probe tidak bisa menjangkau process yang jelas-jelas hidup. Lalu panggil enableShutdownHooks: dokumentasi NestJS menyebut dengan tegas bahwa shutdown hooks dimatikan secara default dan listener-nya harus Anda aktifkan sendiri.

Dengan hooks aktif, signal terminasi menjalankan onModuleDestroy, lalu beforeApplicationShutdown, lalu onApplicationShutdown, dalam urutan itu. Urutan tersebut adalah jendela untuk berhenti menerima pekerjaan baru dan membiarkan transaction yang sedang jalan commit atau rollback. Tanpa itu process mengikuti perilaku signal default dan langsung keluar, dan setiap request yang masih terbuka mati bersamanya, termasuk request yang sudah menulis header jurnal tetapi belum menulis baris-barisnya. Dokumentasi yang sama juga mencatat bahwa SIGTERM tidak pernah bekerja di Windows, yang baru relevan kalau Anda mengembangkan di sana dan tidak paham kenapa perilaku lokal berbeda dari platform.

## Health endpoint yang benar-benar dipanggil platform

Liveness dan readiness menjawab pertanyaan yang berbeda, dan platform bertindak berbeda atas masing-masing jawaban, jadi melayani keduanya dari satu endpoint adalah cara pod yang sehat berakhir di-restart. Kubernetes mendokumentasikan pemisahan itu dengan jelas: liveness probe menentukan kapan container harus di-restart, sedangkan readiness probe yang gagal membuat EndpointSlice controller mengeluarkan IP pod tersebut dari Service yang cocok dengannya. Karena itu liveness check yang menjalankan query mengubah database yang lambat menjadi restart loop, tepatnya cascading failure yang diperingatkan di halaman yang sama: container restart saat beban tinggi, request client gagal, dan beban tambahan menumpuk di pod yang masih hidup.

| Probe | Efek kalau gagal | Boleh menyentuh apa |
| --- | --- | --- |
| Startup | Container dimatikan, lalu mengikuti restart policy-nya | Tidak ada. Cukup membuktikan process selesai start |
| Liveness | Container di-restart begitu kegagalan melewati toleransi yang diatur | Hanya state lokal process — tanpa database, tanpa queue, tanpa HTTP keluar |
| Readiness | IP pod dikeluarkan dari EndpointSlice milik Service yang cocok | Dependency yang dibutuhkan request nyata, masing-masing dengan timeout |

Ada satu jebakan lagi yang letaknya di mekanisme, bukan di desain: httpGet probe dianggap sukses untuk status apa pun dari 200 sampai di bawah 400. Handler yang menangkap error database-nya sendiri lalu mengembalikan 200 dengan body berisi status error adalah probe yang lulus, dan pod itu terus menerima traffic yang tidak bisa dilayaninya. Lempar ServiceUnavailableException supaya status code yang membawa keputusan, atau serahkan penyusunan indicator dan status code ke library seperti @nestjs/terminus.

## Migration jalan sekali, atau sekali per replica

Satu-satunya tempat aman untuk migration adalah langkah yang selesai sebelum pod baru mana pun diterima. TypeORM menyediakan migrationsRun di DataSource, yang didokumentasikan sebagai penanda apakah migration harus auto-run pada setiap application launch; di tiga replica itu berarti tiga launch. Prisma sebenarnya lebih baik daripada anggapan umum di sini: dokumentasinya menyatakan bahwa concurrent deploy aman, karena di PostgreSQL seluruh proses apply berjalan di dalam satu transaction yang dijaga advisory lock, sehingga dua run akan serialise, bukan saling menyela.

Lock itu melindungi schema. Ia tidak melindungi rollout, dan perbedaan itulah kegagalan yang pernah saya rilis. Setiap replica menjalankan migrator saat start, satu replica memegang lock dan menerapkan perubahan, dua sisanya menunggu di antrean sambil menghabiskan startup budget yang diberikan platform untuk pod baru. Keduanya dinyatakan gagal karena tidak pernah menjadi ready, release di-rollback, dan schema sudah maju, sehingga rollback justru menempatkan build lama di depan tabel baru. Memisahkannya menjadi langkah berurutan hanya butuh satu baris konfigurasi platform. Urutan deploy dari dokumentasi Prisma sendiri terdiri dari tiga command, dan yang tengah layak dipertahankan: migration check jalan offline, db migrate dengan flag show mencatat persis apa yang akan dijalankan, dan hanya command ketiga yang menyentuh database.

> Migration yang jalan tepat sekali pun masih bisa merusak deploy. Selama rolling release, build sebelumnya masih melayani di atas schema baru, jadi column yang di-rename atau di-drop menjatuhkan production selama rolling berlangsung. Setiap migration yang dideploy dengan cara ini harus tetap bisa dibaca oleh code yang sudah berjalan: tambah dan backfill dulu, drop hanya setelah build lama benar-benar hilang.

*Band paling bawah adalah versi yang saya rilis lebih dulu: schema-nya tidak pernah rusak, tetapi release-nya tetap gagal, karena dua dari tiga pod menghabiskan seluruh startup budget-nya mengantre di belakang sebuah lock.*

## Ukuran pool dikali jumlah replica adalah angka yang penting

Ukuran pool bukan setelan per service, melainkan anggaran per cluster. Angka yang perlu dibandingkan dengan batas database adalah ukuran pool dikali jumlah replica, ditambah semua hal lain yang membuka koneksi. PostgreSQL mendokumentasikan max_connections dengan default umumnya 100 dan superuser_reserved_connections sebanyak tiga, jadi sekitar 97 slot tersedia untuk role biasa sebelum migration job, metrics exporter, background worker dan session psql Anda sendiri mengambil bagiannya.

Kegagalannya lalu mendarat di momen paling buruk. Rolling deploy berarti replica set lama dan baru sama-sama terkoneksi, jadi puncak pemakaian koneksi terjadi saat release, bukan saat jam sibuk, dan gejalanya adalah pod yang gagal start, bukan halaman yang lambat. Autoscaler membuatnya lebih parah karena mengubah jumlah replica menjadi angka yang tidak pernah Anda pilih, jadi hitung pool terhadap batas atas autoscaler, bukan terhadap jumlah replica hari ini. Kalau hitungannya menuntut concurrency lebih besar daripada yang diizinkan batas itu, pertanyaannya soal transaction pooler, bukan soal pool yang lebih besar.

## Pemisahan environment adalah kontrol keamanan data

Environment variable biasanya dianggap urusan kenyamanan. Di ERP ia adalah kontrol keamanan data, karena akibat terburuknya bukan key yang bocor, melainkan pod staging yang memegang database URL production: traffic staging adalah traffic uji, dan traffic uji di ERP membuat jurnal. Ada tiga kelompok variable yang wajib berbeda per environment, dan tidak satu pun boleh jatuh ke nilai default.

- Database URL beserta credential-nya — instance yang terpisah, bukan satu instance berisi dua schema dengan konvensi penamaan sebagai satu-satunya pembatas.
- Setiap endpoint integrasi keluar: pajak dan e-invoicing, payment channel, notifikasi WhatsApp. Posting uji tidak boleh bisa mengirim dokumen nyata ke pihak nyata.
- Semua switch yang membuat atau menghapus data: seeding, option synchronize milik TypeORM yang dokumentasinya sendiri melarang dipakai di production karena data production bisa hilang, dan feature flag apa pun yang menulis.

Setelah itu validasi seluruh set variable saat boot dan tolak start kalau ada yang hilang, karena service yang tetap start dengan variable kosong baru menemukan masalahnya pada request pertama, bukan pada waktu deploy. Tulis juga nama environment di baris log saat start. Membacanya butuh satu detik dan itu sudah beberapa kali menahan saya menjalankan migration ke database yang salah.

## Yang sama sekali tidak layak tinggal di web service

Keputusan terakhir adalah apa yang harus dikeluarkan. Tutup bulan, batch posting depresiasi, revaluasi stok sepanjang setahun mutasi, laporan yang menelusuri seluruh general ledger: apa pun yang durasinya bisa melewati request timeout platform, atau hidup lebih lama daripada rotasi pod, tidak layak berada di belakang HTTP handler. Begitu juga scheduler yang didaftarkan di dalam aplikasi, karena ia jalan di setiap replica — job posting malam di tiga pod berarti tiga posting, kecuali ada sesuatu di luar jadwal yang membuatnya idempotent.

Bentuk yang bekerja di PaaS adalah tiga deployment dari satu image: web service yang hanya melayani request pendek, worker yang mengonsumsi queue dengan concurrency satu untuk job yang harus serial, dan scheduler yang meng-enqueue, bukan mengeksekusi. Platform lalu memperlakukan job panjang sebagai process tersendiri dengan umur sendiri, dan restart di tengah job menjadi job yang perlu di-retry, bukan posting yang setengah jadi.

> Bangun satu image dan ubah hanya start command-nya untuk web, worker dan scheduler. Ketiga deployment itu jadi terbukti menjalankan code yang sama, dan migration yang dibutuhkan worker tidak mungkin datang satu deploy lebih lambat daripada migration yang dibutuhkan API.

Aturan yang saya bawa dari semua ini pendek saja. Deploy schema dan code sebagai dua langkah berurutan, jangan sebagai satu langkah. Beri platform dua endpoint, karena ia memang menanyakan dua hal berbeda dan akan bertindak atas keduanya. Dan hitung koneksi sebelum autoscaler menghitungnya untuk Anda. Managed PaaS menghilangkan server, bukan urutannya, dan pada ERP API urutan itulah bagian yang akhirnya muncul di buku besar.

## Sumber

- Dokumentasi NestJS — Lifecycle events dan application shutdown hooks
- Dokumentasi Kubernetes — Liveness, readiness dan startup probe
- Dokumentasi Prisma — Applying a migration, termasuk concurrent deploy
- Dokumentasi TypeORM — DataSource options, migrationsRun dan poolSize
- Dokumentasi PostgreSQL — Connection settings dan slot koneksi yang direservasi

*Tidak ada yang berubah pada setelan pool di antara ketiga panel; hanya jumlah replica-nya, dan itulah sebabnya kegagalan datang saat autoscale, bukan saat deploy.*

## Source links

- https://docs.nestjs.com/fundamentals/lifecycle-events
- https://kubernetes.io/docs/concepts/workloads/pods/probes/
- https://www.prisma.io/docs/orm/migrations/applying-a-migration
- https://typeorm.io/docs/data-source/data-source-options/
- https://www.postgresql.org/docs/current/runtime-config-connection.html

## FAQ

### Apakah enableShutdownHooks benar-benar perlu untuk NestJS di PaaS?

Perlu, kalau ada request yang menulis data. Dokumentasi NestJS menyebut shutdown hooks dimatikan secara default dan listener-nya harus Anda aktifkan sendiri, jadi tanpa pemanggilan itu signal terminasi mengakhiri process dengan request yang masih berjalan. Di ERP API hal ini bisa menyisakan header jurnal yang tertulis tanpa baris-barisnya. Dengan hooks aktif, framework menjalankan onModuleDestroy, beforeApplicationShutdown, lalu onApplicationShutdown sebagai kesempatan untuk drain.

### Apakah health check endpoint NestJS boleh query database?

Readiness boleh, liveness jangan. Kubernetes mendokumentasikan bahwa liveness probe menentukan kapan container di-restart, sedangkan readiness probe yang gagal hanya membuat IP pod dikeluarkan dari Service yang cocok, jadi pemeriksaan database di jalur liveness mengubah query lambat menjadi restart loop. Sediakan /healthz yang murah untuk liveness dan letakkan pemeriksaan dependency di /readyz dengan timeout pendek.

### Di mana sebaiknya migration Prisma atau TypeORM dijalankan saat multi replica?

Di satu release step yang selesai sebelum pod baru mana pun diterima, bukan di bootstrap aplikasi. Option migrationsRun milik TypeORM didokumentasikan sebagai auto-run pada setiap application launch, dan di tiga replica itu berarti tiga launch. Dokumentasi Prisma menyebut concurrent deploy aman di PostgreSQL karena apply berjalan dalam transaction yang dijaga advisory lock, tetapi antrean itu tetap memakan startup budget pod yang menunggu.

### Berapa besar connection pool yang aman di managed PaaS?

Atur agar ukuran pool dikali jumlah replica maksimum autoscaler, ditambah semua client lain, tetap di bawah batas database. PostgreSQL mendokumentasikan max_connections dengan default umumnya 100 dan superuser_reserved_connections sebanyak tiga, jadi tersisa sekitar 97 slot untuk API, worker, migration job, metrics exporter dan session psql Anda. Sisakan ruang untuk replica set lama yang masih terkoneksi selama rolling deploy.

### Bolehkah job batch tutup bulan ERP dijalankan di API pada PaaS?

Sebaiknya tidak, jangan di dalam web service. Apa pun yang bisa melewati request timeout platform atau hidup lebih lama daripada rotasi pod sebaiknya pindah ke deployment worker terpisah yang mengonsumsi queue, dengan concurrency satu untuk job yang harus serial. Scheduler yang didaftarkan di dalam API lebih berisiko karena jalan di setiap replica, sehingga posting malam di tiga pod menjadi tiga posting kecuali dibuat idempotent.

---

- HTML version: https://www.matthewswong.com/id/blog/deploy-nestjs-erp-api-paas-guide/
- English: https://www.matthewswong.com/en/blog/deploy-nestjs-erp-api-paas-guide/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
