kodingup.camp

Beranda/Node.js Backend: Database, Auth, Keamanan & Deploy

Deploy Backend

Modul 9 dari 108 menit bacaDeployment

Isi modul

    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:

    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:

    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:

    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

    1. Menyimpan SQLite atau upload di disk sementara platform. Semua data hilang saat redeploy. Pasang volume persisten (-v di Docker, fitur volume/disk di platform), atau pindah ke Postgres terkelola + object storage.
    2. Lupa environment variable di produksi. Server menyala dengan nilai default development atau malah crash. Validasi konfigurasi saat start (Modul 6). Di Proyek Akhir, JWT_SECRET yang kurang dari 32 karakter membuat server menolak menyala dengan pesan Konfigurasi tidak valid: { JWT_SECRET: [ 'JWT_SECRET minimal 32 karakter' ] }.
    3. Menjalankan aplikasi sebagai root. Kalau ada celah, penyerang langsung mendapat kendali penuh atas server. Pakai user khusus (USER node di Docker, User=toko di systemd).
    4. 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/tcp saja) dan biarkan Nginx yang menerima.
    5. npm install alih-alih npm ci di server. npm install bisa memasang versi berbeda dari yang kamu uji, dan memasang devDependencies yang tidak perlu. Pakai npm ci --omit=dev.

    Latihan

    1. Mudah, Dari perintah docker run di atas, jelaskan fungsi setiap flag: -d, -p 3000:3000, -e, dan -v toko-data:/app/data. Apa yang terjadi pada data kalau flag -v dihapus lalu container dibuat ulang?
    2. Sedang, Tulis tes Supertest yang membuktikan /health membalas 200 saat database hidup dan 503 saat koneksi database ditutup.
    3. Menantang, Tulis scripts/smoke.js yang menerima URL dasar, menunggu /health sehat (maksimal 10 kali percobaan dengan jeda 1 detik), lalu memeriksa beberapa endpoint dengan status yang diharapkan, dan keluar dengan kode 1 kalau 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. Format HOST: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 bernama toko-data dipasang 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

    Video pendamping

    Kalau penjelasan di atas belum masuk, tonton ini. Video dari kanal YouTube pihak lain.

    Full Node.js Deployment - NGINX, SSL With Lets EncryptTraversy Media · Bahasa InggrisBuka di YouTube

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.