Deploy artinya memindahkan catatan-api dari laptop ke server yang menyala 24 jam dan bisa diakses orang lain. Bayangin beda masak di rumah dengan buka warung. Di rumah, kamu masak kalau lapar. Warung harus buka tiap hari, ada yang menyalakan kompor lagi kalau mati, ada etalase di depan yang menerima pembeli, ada papan "BUKA" yang bisa dicek orang dari jauh, dan saat tutup, pesanan yang sudah masuk diselesaikan dulu baru kompornya dimatikan. Di server, itu semua punya nama: process manager, reverse proxy, health check, dan graceful shutdown.
🧪 Tebakan cepat:
catatan-apisedang menyimpan sebuahPOST /catatanyang butuh 800 ms. Di tengah jalan, systemd mengirim sinyalSIGTERMkarena kamu deploy versi baru, dan kodemu gak menangani sinyal itu. Apa yang diterima client? Tebak dulu, jawabannya ada di bagian "Graceful shutdown: selesaikan dulu, baru tutup".
Daftar periksa deploy
| # | Yang dicek | Kenapa |
|---|---|---|
| 1 | NODE_ENV=production |
Express menyembunyikan stack trace, npm melewatkan devDependencies |
| 2 | PORT dan secret dari environment |
Nilainya diset server, bukan ditulis di kode |
| 3 | Process manager (PM2 atau systemd) atau Docker | Menyalakan lagi kalau crash dan setelah reboot |
| 4 | Reverse proxy (Nginx) dengan HTTPS | Menerima port 80/443, meneruskan ke port 3000 |
| 5 | GET /health |
Load balancer dan monitor tahu app-nya hidup |
| 6 | Graceful shutdown di SIGTERM dan SIGINT |
Request yang sedang jalan gak terputus saat deploy |
| 7 | Log ke stdout | Dikumpulkan systemd, PM2, atau Docker |
Nomor 1 dan 2 sudah kamu kerjakan di pelajaran Environment & Config. Sisanya di bawah.
GET /health: tanda server hidup
app.get("/health", (req, res) => {
res.json({ status: "ok", uptime: Math.round(process.uptime()) });
});
$ curl -s https://catatan.example.com/health
{"status":"ok","uptime":125}
process.uptime() adalah berapa detik proses ini sudah jalan. Load balancer, Docker, atau layanan monitoring memanggil endpoint ini tiap beberapa detik. Jawaban 200 dengan "status":"ok" artinya sehat; gak menjawab atau error artinya app-nya perlu diganti atau dinyalakan ulang.
Graceful shutdown: selesaikan dulu, baru tutup
Saat deploy, process manager menghentikan proses lama dengan sinyal: SIGTERM dari systemd, Docker, dan Kubernetes, SIGINT dari PM2 dan dari Ctrl+C. Tanpa handler, Node langsung keluar, dan request yang sedang jalan ikut terputus. Simpan server dari app.listen, lalu:
const server = app.listen(port, () => {
console.log(`Server jalan di http://localhost:${port}`);
});
function shutdown(signal) {
console.log(`Menerima signal ${signal}...`);
console.log("Menutup HTTP server...");
server.close(async () => {
console.log("Menutup koneksi database...");
await db.end(); // pool dari pelajaran Koneksi Database
console.log("Server berhasil di-shutdown");
process.exit(0);
});
setTimeout(() => {
console.error("Masih ada request setelah 10 detik, paksa keluar");
process.exit(1);
}, 10_000).unref();
}
process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown)memasang handler. Begitu ada handler, Node gak keluar sendiri lagi: kodemu yang memutuskan kapan.server.close(callback)berhenti menerima koneksi baru, menunggu request yang sedang jalan selesai, lalu memanggil callback-nya.- Database ditutup di dalam callback, setelah request terakhir selesai, karena request itu mungkin masih butuh database.
setTimeout(...).unref()adalah batas waktu: kalau ada request yang gak selesai dalam 10 detik, keluar paksa.unref()supaya timer ini sendiri gak menahan proses tetap hidup.
Urutan kejadian saat deploy, dengan POST /catatan yang butuh 800 ms:
| Waktu | Yang terjadi |
|---|---|
| 0 ms | POST /catatan masuk |
| 100 ms | systemd mengirim SIGTERM: tercetak Menerima signal SIGTERM... dan Menutup HTTP server... |
| 800 ms | POST /catatan selesai, client menerima 201 {"id":3,"judul":"Olahraga","isi":"lari 5 km"} |
| 800 ms | callback jalan: Menutup koneksi database..., Server berhasil di-shutdown, keluar dengan kode 0 |
Tanpa handler, di 100 ms itu proses langsung mati, dan client menerima curl: (52) Empty reply from server. Itu jawaban tebakannya: jawabannya kosong, dan client gak tahu catatannya tersimpan atau gak.
Process manager: PM2 atau systemd
node app.js di sesi SSH mati begitu terminalnya ditutup, dan gak ada yang menyalakannya lagi setelah crash atau reboot. Pilih salah satu:
PM2, yang sudah kamu kenal dari pelajaran Clustering:
// ecosystem.config.js
module.exports = {
apps: [{
name: "catatan-api",
script: "app.js",
instances: "max",
exec_mode: "cluster",
kill_timeout: 10000, // tunggu 10 detik sebelum SIGKILL (bawaannya 1.600 ms)
env_production: { NODE_ENV: "production", PORT: 3000 },
}],
};
pm2 start ecosystem.config.js --env production menyalakan, pm2 reload catatan-api untuk deploy, lalu pm2 save dan pm2 startup supaya hidup lagi setelah reboot.
systemd, bawaan Linux, di /etc/systemd/system/catatan-api.service:
[Unit]
Description=catatan-api
After=network.target
[Service]
User=budi
WorkingDirectory=/home/budi/catatan-api
ExecStart=/usr/bin/node app.js
Environment=NODE_ENV=production
Environment=PORT=3000
EnvironmentFile=/etc/catatan-api.env
Restart=on-failure
[Install]
WantedBy=multi-user.target
sudo systemctl enable --now catatan-api menyalakan sekarang dan setiap boot, sudo systemctl restart catatan-api untuk deploy. EnvironmentFile menunjuk file berisi secret seperti JWT_SECRET; batasi izinnya dengan chmod 600 supaya cuma root yang bisa membacanya. Hasil systemctl status catatan-api (dipotong, angka contoh):
● catatan-api.service - catatan-api
Loaded: loaded (/etc/systemd/system/catatan-api.service; enabled; preset: enabled)
Active: active (running) since Fri 2026-10-09 03:00:00 UTC; 2min 5s ago
Main PID: 5120 (node)
CGroup: /system.slice/catatan-api.service
└─5120 /usr/bin/node app.js
Oct 09 03:00:00 vps systemd[1]: Started catatan-api.service - catatan-api.
Oct 09 03:00:00 vps node[5120]: Server jalan di http://localhost:3000
Reverse proxy dengan HTTPS
Node jalan di port 3000 sebagai user biasa. Di depannya, Nginx menerima port 80 dan 443, memegang sertifikat HTTPS (misalnya dari Let's Encrypt lewat certbot), lalu meneruskan request ke 127.0.0.1:3000:
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
Tambahkan app.set("trust proxy", 1) di Express supaya req.ip berisi alamat pengunjung, bukan 127.0.0.1, yang penting untuk rate limit. Kalau app-nya mati, Nginx menjawab 502 Bad Gateway.
Log ke stdout
Cukup console.log (atau logger JSON seperti pino) ke stdout. Process manager yang mengumpulkan: journalctl -u catatan-api -f untuk systemd, pm2 logs untuk PM2, docker logs untuk Docker. App yang menulis file log sendiri cepat atau lambat memenuhi disk.
📌 Kalau pakai Docker
FROM node:24-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
ENV NODE_ENV=production
USER node
EXPOSE 3000
CMD ["node", "app.js"]
--omit=dev menggantikan --only=production yang sudah usang. CMD langsung menjalankan node, bukan npm start, supaya SIGTERM dari docker stop sampai ke Node. Di container, Node adalah proses nomor 1, dan tanpa handler dia mengabaikan SIGTERM: docker stop menunggu 10 detik lalu SIGKILL.
Di latihan: health check dan urutan shutdown
healthCheck() di kode awal sudah mengembalikan object { status: "ok", version: "1.0.0", environment: "production" }, dan pemanggilnya yang mencetak dengan JSON.stringify. gracefulShutdown() sudah mencetak tiga baris. Tukar dua baris tengahnya supaya HTTP server ditutup sebelum database (urutan yang benar, walaupun gak dinilai), lalu tutup dengan console.log("Server berhasil di-shutdown"); di dalam function itu, karena simulator memanggilnya sekali lagi.
⚠️ Jebakan yang sering ketemu
process.exit()langsung setelahserver.close(), di luar callback-nya. Proses keluar sebelum request selesai, sama saja dengan gak punya handler.- Menutup database sebelum server. Request yang masih jalan gagal di tengah query.
- Cuma menangani
SIGTERM. PM2 dan Ctrl+C mengirimSIGINT, dan PM2 cuma menunggu 1,6 detik sebelumSIGKILLkecualikill_timeoutdinaikkan. node app.jsdi SSH, atau lupapm2 savedanpm2 startup. Terminal ditutup atau server reboot, app-nya hilang.
TL;DR: Sebelum deploy, cek tujuh hal: NODE_ENV=production, PORT dan secret dari environment, process manager (PM2 dengan ecosystem.config.js, atau unit systemd dengan Restart=on-failure), Nginx dengan HTTPS di depan port 3000, GET /health yang menjawab {"status":"ok"}, graceful shutdown, dan log ke stdout. Graceful shutdown: process.on("SIGTERM") dan process.on("SIGINT") memanggil server.close(), yang berhenti menerima koneksi baru dan menunggu request yang sedang jalan; di callback-nya baru database ditutup dan process.exit(0). Tanpa handler, request yang sedang jalan dapat Empty reply from server.