Kenapa ini penting
API yang hanya hidup di localhost:3000 belum berguna bagi siapa pun selain kamu. Deploy adalah proses memindahkan aplikasi ke komputer yang menyala 24 jam, punya alamat publik, dan bisa diakses lewat HTTPS. Di sinilah banyak pemula tersandung, karena masalahnya bukan lagi kode, melainkan "lingkungan": versi Node yang berbeda, environment variable yang lupa diset, database yang hilang setiap redeploy, proses yang mati dan tidak hidup lagi, atau sertifikat HTTPS yang kedaluwarsa.
Analoginya seperti memindahkan warung dari rumah ke ruko di jalan besar. Barang dagangannya sama (kodemu), tapi kamu butuh alamat yang jelas (domain), pintu depan dengan satpam (reverse proxy + HTTPS), listrik yang tidak mati (process manager yang menghidupkan ulang aplikasi), gudang yang tidak ikut terbongkar saat renovasi (volume persisten untuk database dan upload), dan seseorang yang rutin mengecek warung masih buka (health check).
Konsep inti
Script build dan start
Backend JavaScript murni tidak perlu langkah build: Node menjalankan file .js langsung. Langkah build baru dibutuhkan kalau kamu memakai TypeScript (dikompilasi ke JavaScript) atau bundler. Yang wajib ada di package.json:
start: perintah menjalankan aplikasi di produksi, tanpa--watch. Platform hosting umumnya menjalankannpm startsecara default.engines.node: versi Node minimum, supaya platform memilih versi yang benar. Gunakan versi LTS. Saat ini Node 24 (LTS "Krypton"). Node 26 adalah rilis "Current" dan baru akan menjadi LTS belakangan.- Pemasangan dependency di produksi memakai
npm ci --omit=dev: memasang persis sesuaipackage-lock.json, tanpa devDependencies seperti Vitest dan drizzle-kit.package-lock.jsonwajib di-commit.
Process manager: PM2 atau systemd
Kalau kamu menyewa VPS (Virtual Private Server) sendiri, menjalankan node src/server.js di terminal SSH tidak cukup. Begitu SSH ditutup, server mati. Begitu aplikasi crash, ia tidak bangun lagi. Begitu VPS reboot, tidak ada yang menyalakannya. Kamu butuh process manager:
| PM2 (7.1.1) | systemd | |
|---|---|---|
| Apa | Paket npm khusus aplikasi Node | Pengelola layanan bawaan hampir semua distro Linux |
| Kelebihan | Mudah, ada pm2 logs, pm2 monit, mode cluster |
Tidak perlu instal apa-apa, standar Linux, terintegrasi dengan log sistem (journalctl) |
| Restart otomatis | Ya | Ya (Restart=always) |
| Nyala saat boot | pm2 startup + pm2 save |
systemctl enable |
Keduanya sama baiknya. Pilih satu, jangan dua-duanya untuk aplikasi yang sama.
Catatan untuk SQLite: jalankan satu proses saja. SQLite mengizinkan satu penulis dalam satu waktu, dan rate limiter memori kita juga hanya benar untuk satu proses. Mode cluster (beberapa proses) baru masuk akal setelah pindah ke Postgres dan store rate limit bersama seperti Redis.
Docker dasar
Docker membungkus aplikasimu beserta versi Node, sistem operasi minimal, dan dependency-nya menjadi satu image. Image itu berjalan sama persis di laptop, di server, dan di platform cloud. Masalah "di laptopku jalan kok" jauh berkurang. Istilah dasarnya:
- Dockerfile: resep membuat image, dibaca dari atas ke bawah. Setiap instruksi menjadi satu layer yang di-cache.
- Image: hasil build, tidak bisa diubah.
- Container: image yang sedang berjalan.
- Volume: folder di luar container yang tidak hilang saat container dihapus. Wajib untuk file database SQLite dan folder upload.
Trik penting di Dockerfile: salin package.json + package-lock.json dan jalankan npm ci sebelum menyalin kode. Selama dependency tidak berubah, layer instalasi diambil dari cache dan build berikutnya jauh lebih cepat. Gunakan image resmi node:24-slim (berbasis Debian, ukurannya kecil, dan cocok dengan binary siap pakai untuk paket native seperti better-sqlite3 dan argon2), lalu jalankan aplikasi sebagai user non-root node.
Pilihan tempat deploy
| Pilihan | Gaya | Yang perlu diperhatikan |
|---|---|---|
| Railway, Render | PaaS: hubungkan repo GitHub, platform membangun dan menjalankan otomatis | Set environment variable di dashboard. Disk default sementara, jadi SQLite dan upload butuh volume/disk persisten, atau pakai Postgres terkelola + object storage |
| Fly.io | Menjalankan container (Dockerfile-mu) di dekat pengguna | Volume persisten tersedia, tapi terikat ke satu mesin |
| VPS (DigitalOcean, Vultr, penyedia lokal, dll.) | Kamu mengurus semuanya: OS, Node, Nginx, HTTPS, backup | Paling fleksibel dan sering paling murah, tapi tanggung jawab keamanan dan pembaruan server ada di kamu |
Harga dan kuota gratis platform berubah-ubah, jadi cek halaman harga masing-masing sebelum memilih. Apa pun platformnya, aturannya sama: aplikasi membaca PORT dari environment, semua rahasia diset sebagai environment variable, dan data yang harus bertahan disimpan di volume persisten atau layanan terkelola.
Reverse proxy Nginx dan HTTPS
Di VPS, aplikasi Node tidak dibuka langsung ke internet. Di depannya dipasang reverse proxy, biasanya Nginx. Nginx menerima request di port 80/443, mengurus HTTPS, lalu meneruskan request ke localhost:3000. Keuntungannya:
- HTTPS diurus Nginx. Sertifikat gratis dari Let's Encrypt dipasang dan diperpanjang otomatis dengan certbot.
- Satu VPS bisa melayani banyak domain/aplikasi.
- Nginx bisa menyajikan file statis (misalnya folder upload) dengan sangat efisien, membatasi ukuran body, dan menyaring koneksi lambat.
Karena request sekarang datang dari Nginx, aplikasi harus diberi tahu lewat app.set('trust proxy', 1) (Modul 6) dan Nginx harus meneruskan header X-Forwarded-For dan X-Forwarded-Proto. Tanpa itu, req.ip selalu 127.0.0.1 dan cookie secure bisa bermasalah.
Health check
Endpoint /health yang ringan dan tanpa autentikasi memberi tahu dunia luar bahwa aplikasimu hidup. Dipakai oleh HEALTHCHECK di Docker, pengaturan health check di platform hosting, load balancer, dan layanan pemantau uptime. Health check yang baik juga memeriksa dependency penting (misalnya SELECT 1 ke database) dan membalas 503 Service Unavailable kalau ada yang bermasalah, supaya platform tidak mengirim trafik ke instance yang sakit. Jangan masukkan informasi sensitif (versi library, isi konfigurasi) di respons health check.
Contoh kode
package.json Proyek Akhir:
{
"name": "toko-api",
"type": "module",
"engines": { "node": ">=24" },
"scripts": {
"dev": "node --watch --env-file-if-exists=.env src/server.js",
"start": "node src/server.js",
"test": "vitest run",
"db:generate": "drizzle-kit generate",
"seed:admin": "node --env-file-if-exists=.env src/seed-admin.js"
}
}
start sengaja tidak memakai --env-file. Di produksi, environment variable diset oleh platform, systemd, PM2, atau Docker, bukan oleh file .env di dalam repo.
Health check di src/app.js:
app.get('/health', (req, res) => {
try {
db.$client.prepare('SELECT 1').get(); // pastikan database masih hidup
res.json({ status: 'ok', uptime: Math.round(process.uptime()) });
} catch (err) {
req.log.error({ err }, 'health check gagal');
res.status(503).json({ status: 'error' }); // 503 = jangan kirimi trafik dulu
}
});
PM2
// ecosystem.config.cjs, konfigurasi PM2 (.cjs karena PM2 membaca file ini dengan require)
module.exports = {
apps: [{
name: 'toko-api',
script: 'src/server.js',
node_args: '--env-file=.env', // di VPS, .env berada di server (tidak di-commit)
instances: 1, // SQLite = 1 proses penulis; Postgres boleh 'max' + exec_mode 'cluster'
autorestart: true,
max_memory_restart: '300M',
kill_timeout: 10000, // beri waktu graceful shutdown sebelum dipaksa
env: { NODE_ENV: 'production', PORT: 3000 },
}],
};
npm install -g pm2@7.1.1
pm2 start ecosystem.config.cjs
pm2 logs toko-api # lihat log
pm2 restart toko-api # setelah deploy kode baru
pm2 startup && pm2 save # nyalakan otomatis saat VPS reboot (ikuti perintah yang dicetak)
systemd (alternatif PM2)
# /etc/systemd/system/toko-api.service
[Unit]
Description=Toko API
After=network.target
[Service]
Type=simple
User=toko
WorkingDirectory=/srv/toko-api
EnvironmentFile=/etc/toko-api.env
ExecStart=/usr/bin/node src/server.js
Restart=always
RestartSec=3
TimeoutStopSec=15
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now toko-api # nyalakan sekarang + saat boot
sudo journalctl -u toko-api -f # ikuti log
/etc/toko-api.env berisi NODE_ENV=production, JWT_SECRET=..., dan seterusnya, dengan izin file 600 supaya hanya root yang bisa membaca. Path ExecStart harus menunjuk ke binary Node yang benar di servermu (cek dengan which node).
Docker
# Dockerfile, image produksi toko-api
FROM node:24-slim
WORKDIR /app
ENV NODE_ENV=production
# 1) salin daftar dependency dulu → layer ini di-cache selama package*.json tidak berubah
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
# 2) baru salin kode dan file migrasi
COPY src ./src
COPY drizzle ./drizzle
# folder data & upload dimiliki user non-root "node" (sudah ada di image resmi)
RUN mkdir -p /app/data /app/uploads && chown -R node:node /app/data /app/uploads
USER node
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s CMD node -e "fetch('http://localhost:3000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
CMD ["node", "src/server.js"]
# .dockerignore, jangan ikut masuk image
node_modules
.env
*.db
*.db-*
uploads
test
.git
docker build -t toko-api .
docker run -d --name toko -p 3000:3000 \
-e JWT_SECRET="$(openssl rand -base64 48)" \
-e DATABASE_FILE=/app/data/toko.db \
-e CORS_ORIGIN=https://tokoku.id \
-v toko-data:/app/data -v toko-uploads:/app/uploads \
toko-api
docker exec toko node src/seed-admin.js admin@tokoku.id 'PasswordKuat123'
CMD memakai bentuk array (["node", "src/server.js"]) supaya Node menjadi proses utama dan menerima SIGTERM langsung. Dengan bentuk teks biasa atau lewat npm start, sinyal bisa tertahan di shell atau npm, sehingga graceful shutdown tidak pernah terpicu. HEALTHCHECK memakai fetch bawaan Node karena image slim tidak menyertakan curl.
Nginx + HTTPS
# /etc/nginx/sites-available/api.tokoku.id
server {
listen 80;
server_name api.tokoku.id;
client_max_body_size 3m; # sedikit di atas batas upload 2 MB di multer
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Request-Id $request_id; # request id dari Nginx ikut ke log aplikasi
}
}
sudo ln -s /etc/nginx/sites-available/api.tokoku.id /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx # nginx -t = cek sintaks dulu
sudo certbot --nginx -d api.tokoku.id # pasang sertifikat Let's Encrypt + redirect HTTPS
certbot mengubah blok server di atas supaya juga mendengarkan port 443 dengan sertifikat, lalu memasang perpanjangan otomatis. Pastikan DNS domain (record A) sudah menunjuk ke IP VPS sebelum menjalankan certbot.
Konfigurasi PM2, systemd, Docker, dan Nginx di atas adalah templat standar. Saat materi ini ditulis, yang dijalankan sungguhan adalah aplikasinya sendiri (start, health check, graceful shutdown, dan smoke test di bawah). Uji templat itu di servermu sendiri, dan selalu jalankan nginx -t sebelum reload.
Smoke test setelah deploy
Setelah deploy, jalankan pemeriksaan cepat dari luar. Kalau gagal, segera rollback ke versi sebelumnya:
node scripts/smoke.js http://localhost:3000
# → OK /health → 200 (harap 200)
# → OK /api/products?limit=1 → 200 (harap 200)
# → OK /api/orders → 401 (harap 401)
# → OK /tidak-ada → 404 (harap 404)
(Isi scripts/smoke.js ada di Latihan 3.)
Kesalahan umum
- Menyimpan SQLite atau upload di disk sementara platform. Semua data hilang saat redeploy. Pasang volume persisten (
-vdi Docker, fitur volume/disk di platform), atau pindah ke Postgres terkelola + object storage. - Lupa environment variable di produksi. Server menyala dengan nilai default development atau malah crash. Validasi konfigurasi saat start (Modul 6). Di Proyek Akhir,
JWT_SECRETyang kurang dari 32 karakter membuat server menolak menyala dengan pesanKonfigurasi tidak valid: { JWT_SECRET: [ 'JWT_SECRET minimal 32 karakter' ] }. - Menjalankan aplikasi sebagai root. Kalau ada celah, penyerang langsung mendapat kendali penuh atas server. Pakai user khusus (
USER nodedi Docker,User=tokodi systemd). - Membuka port 3000 langsung ke internet. Lalu lintas tanpa HTTPS dan tanpa Nginx di depan. Tutup port aplikasi dengan firewall (misalnya
ufw allow 80,443/tcpsaja) dan biarkan Nginx yang menerima. npm installalih-alihnpm cidi server.npm installbisa memasang versi berbeda dari yang kamu uji, dan memasang devDependencies yang tidak perlu. Pakainpm ci --omit=dev.
Latihan
- Mudah, Dari perintah
docker rundi atas, jelaskan fungsi setiap flag:-d,-p 3000:3000,-e, dan-v toko-data:/app/data. Apa yang terjadi pada data kalau flag-vdihapus lalu container dibuat ulang? - Sedang, Tulis tes Supertest yang membuktikan
/healthmembalas 200 saat database hidup dan 503 saat koneksi database ditutup. - Menantang, Tulis
scripts/smoke.jsyang menerima URL dasar, menunggu/healthsehat (maksimal 10 kali percobaan dengan jeda 1 detik), lalu memeriksa beberapa endpoint dengan status yang diharapkan, dan keluar dengan kode1kalau ada yang gagal (supaya pipeline deploy bisa berhenti).
Lihat solusi
1.
-d(detached): container berjalan di latar belakang, terminal bisa dipakai lagi.-p 3000:3000: port 3000 di host diteruskan ke port 3000 di container. FormatHOST:CONTAINER.-e NAMA=nilai: mengisi environment variable di dalam container. Rahasia diberikan di sini (atau lewat--env-file), bukan dimasukkan ke image.-v toko-data:/app/data: volume bernamatoko-datadipasang di/app/data. Isinya disimpan di luar container.
Tanpa -v, file toko.db berada di lapisan tulis container itu sendiri. Saat container dihapus (misalnya ketika deploy versi baru dengan docker rm lalu docker run lagi), database ikut hilang dan toko kembali kosong.
2. Tutup koneksi database lewat db.$client.close() untuk mensimulasikan database mati.
import { it, expect } from 'vitest';
import request from 'supertest';
import { createDb } from '../src/db/index.js';
import { createApp } from '../src/app.js';
it('health 503 saat database mati', async () => {
const db = createDb(':memory:');
const app = createApp({ db, config }); // config tes yang sama dengan Modul 8
await request(app).get('/health').expect(200);
db.$client.close();
const res = await request(app).get('/health');
expect(res.status).toBe(503);
expect(res.body.status).toBe('error');
});
3.
// scripts/smoke.js, cek cepat setelah deploy: node scripts/smoke.js https://api.tokoku.id
const base = process.argv[2] ?? 'http://localhost:3000';
const cek = [
['/health', 200],
['/api/products?limit=1', 200],
['/api/orders', 401], // wajib login → harus 401, bukan 200
['/tidak-ada', 404],
];
async function tunggu(url, percobaan = 10) {
for (let i = 1; i <= percobaan; i++) {
try { if ((await fetch(url)).ok) return true; } catch {}
await new Promise((r) => setTimeout(r, 1000)); // server mungkin belum siap, tunggu 1 detik
}
return false;
}
if (!(await tunggu(base + '/health'))) {
console.error('GAGAL: server tidak sehat setelah 10 detik');
process.exit(1);
}
let gagal = 0;
for (const [path, harap] of cek) {
const res = await fetch(base + path);
const ok = res.status === harap;
if (!ok) gagal++;
console.log(`${ok ? 'OK ' : 'GAGAL'} ${path} → ${res.status} (harap ${harap})`);
}
process.exit(gagal ? 1 : 0);
node scripts/smoke.js http://localhost:3999; echo "exit=$?" # tidak ada server di port ini
# → GAGAL: server tidak sehat setelah 10 detik
# → exit=1
node scripts/smoke.js http://localhost:3000; echo "exit=$?" # server Proyek Akhir menyala
# → OK /health → 200 (harap 200)
# → ... (4 baris OK)
# → exit=0
Pemeriksaan /api/orders → 401 penting: kalau suatu hari endpoint itu membalas 200 tanpa login, berarti requireAuth hilang, dan deploy harus dihentikan.
Ringkasan
- Produksi memakai
npm ci --omit=devdannpm start(tanpa--watch), denganengines.nodemenunjuk ke LTS (Node 24). - Di VPS, pakai process manager (PM2 atau systemd) untuk restart otomatis dan nyala saat boot. SQLite = satu proses.
- Docker membungkus aplikasi + runtime. Salin
package*.jsondulu untuk cache, jalankan sebagai user non-root,CMDbentuk array, data di volume. - PaaS (Railway, Render, Fly.io) mengurus server untukmu, tapi data tetap butuh volume persisten atau layanan terkelola.
- Nginx sebagai reverse proxy mengurus HTTPS (Let's Encrypt via certbot) dan meneruskan header
X-Forwarded-*. Aplikasi memakaitrust proxy. /healthringan tanpa login, membalas 503 kalau dependency penting bermasalah. Jalankan smoke test setelah setiap deploy.