Node.js App #

Caddy sangat cocok bertindak sebagai server reverse proxy di depan aplikasi Node.js kita. Pada arsitektur web modern, menjalankan aplikasi Node.js (seperti Express, Fastify, atau NestJS) secara langsung menghadap ke internet publik adalah tindakan yang kurang direkomendasikan. Aplikasi Node.js sebaiknya fokus menangani logika bisnis (business logic) dan operasi basis data, sementara tugas-tugas infrastruktur—seperti terminasi SSL/TLS otomatis, kompresi konten Gzip/Zstd, pengelolaan header keamanan, dan penyajian berkas statis berkinerja tinggi—didelegasikan sepenuhnya kepada Caddy di gerbang terdepan. Kolaborasi ini tidak hanya meningkatkan kinerja dan responsivitas aplikasi kita, tetapi juga memberikan perlindungan keamanan berlapis bagi server internal kita. Kita akan mengulas tuntas arsitektur deployment Node.js di belakang Caddy, mempraktikkan konfigurasi produksi standar industri, mengelola siklus hidup proses Node.js menggunakan process manager PM2, melakukan penyeimbangan beban (load balancing) multi-instansi, mengonfigurasi penanganan WebSocket Socket.io secara transparan, hingga menyusun skrip otomatisasi deployment tanpa gangguan (zero-downtime deployment).

Arsitektur Deployment #

Sebelum masuk ke detail konfigurasi, mari kita pelajari struktur aliran data request dari browser pengguna hingga diproses oleh runtime Node.js di server kita.

Dalam arsitektur reverse proxy, Caddy bertindak sebagai perantara tunggal yang menerima seluruh lalu lintas terenkripsi HTTPS (port 443) dari internet luar. Caddy menangani jabat tangan TLS dan validasi sertifikat, mendekripsi paket data, lalu meneruskan request tersebut secara lokal melalui jaringan internal HTTP bersih (port 3000) ke proses Node.js yang berjalan di mesin yang sama atau di jaringan lokal virtual (VPC) kita.

flowchart TD
    Internet["Internet (HTTPS - Port 443)"] --> Caddy["Caddy<br>(Terminasi TLS, Caching, Kompresi, Serves Static Assets)"]
    Caddy -->|"HTTP Lokal (Port 3000)"| Node["Node.js<br>(Express / Fastify / NestJS App di bawah PM2)"]
    Node --> DB["Database / Microservices"]

    style Caddy stroke:#0288d1,stroke-width:2px
    style Node stroke:#43a047,stroke-width:2px

Dengan topologi di atas, aplikasi Node.js kita terisolasi dengan aman dari paparan port internet langsung, sehingga meminimalkan risiko eksploitasi celah keamanan runtime.


Konfigurasi Dasar #

Dalam bentuknya yang paling sederhana, mengonfigurasi Caddy untuk mengarahkan lalu lintas domain ke aplikasi Node.js hanya membutuhkan satu direktif sederhana pada Caddyfile:

# Konfigurasi reverse proxy Node.js paling dasar
example.com {
    reverse_proxy localhost:3000
}

Meskipun sangat minimalis, Caddy secara otomatis melakukan hal-hal hebat berikut di latar belakang:

  1. Mengajukan sertifikat SSL/TLS gratis ke Let’s Encrypt atau ZeroSSL dan memasangnya secara otomatis.
  2. Memicu pengalihan (redirect) otomatis dari HTTP (port 80) ke HTTPS (port 443) bagi seluruh pengunjung.
  3. Mengaktifkan protokol HTTP/2 secara default untuk mempercepat waktu muat aset halaman web.

Konfigurasi Lengkap dengan Best Practices #

Untuk deployment di lingkungan produksi riil yang melayani trafik tinggi, kita memerlukan konfigurasi Caddyfile yang lebih kokoh. Kita harus mengonfigurasi logging akses terstruktur, kompresi respons dinamis, header keamanan HTTP, serta memisahkan penyajian aset statis (JS, CSS, gambar) agar langsung ditangani oleh Caddy tanpa membebani proses Node.js.

Berikut adalah templat konfigurasi produksi lengkap yang direkomendasikan:

# Konfigurasi produksi optimal untuk aplikasi Node.js
example.com {
    # 1. Konfigurasi Logging Akses Terstruktur (JSON)
    log {
        output file /var/log/caddy/nodejs-access.log {
            roll_size 100mb
            roll_keep 7
            roll_keep_days 30
        }
        format json
    }
    
    # 2. Kompresi Respons Otomatis (Zstd dan Gzip)
    encode zstd gzip
    
    # 3. Pengamanan Menggunakan Header Keamanan HTTP
    header {
        # Cegah pembajakan klik (Clickjacking)
        X-Frame-Options "SAMEORIGIN"
        # Aktifkan paksaan enkripsi browser (HSTS)
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        # Cegah browser menebak MIME type file (MIME sniffing)
        X-Content-Type-Options "nosniff"
        # Konfigurasi referrer policy yang aman
        Referrer-Policy "strict-origin-when-cross-origin"
        # Sembunyikan informasi biner server web demi keamanan
        -Server
        -X-Powered-By
    }
    
    # 4. Penyajian Aset Statis Secara Efisien Langsung oleh Caddy.
    # Seluruh request ke folder /static/ atau berkas favicon/robots
    # akan dibaca langsung dari disk oleh Caddy tanpa memicu proses Node.js.
    @static path /static/* /favicon.ico /robots.txt
    handle @static {
        root * /var/www/myapp
        file_server
        # Pasang Cache-Control agresif 1 tahun karena file statis memiliki hash nama unik
        header Cache-Control "public, max-age=31536000, immutable"
    }
    
    # 5. Semua Request Lain (Dinamis/API) Dialirkan ke Node.js
    handle {
        reverse_proxy localhost:3000 {
            # Teruskan header IP asli pengunjung ke Node.js
            header_up X-Real-IP {remote_host}
            header_up X-Forwarded-For {remote_host}
            header_up X-Forwarded-Proto {scheme}
            header_up Host {host}
            
            # Pengujian Kesehatan Backend (Active Health Check)
            health_uri /health
            health_interval 10s
            health_timeout 5s
            health_status 200
            
            # Konfigurasi batas waktu timeout transportasi
            transport http {
                dial_timeout 5s
                response_header_timeout 60s
                read_timeout 120s
                write_timeout 120s
            }
        }
    }
}

Node.js dengan PM2 Process Manager #

Di lingkungan produksi Linux, kita tidak boleh menjalankan aplikasi Node.js menggunakan perintah mentah node app.js di terminal. Jika terjadi error yang tidak tertangkap (uncaught exception), proses Node.js akan langsung mati (crash) dan menghentikan seluruh layanan situs kita.

Kita memerlukan manajer proses tingkat produksi seperti PM2. PM2 bertindak sebagai supervisor yang akan memantau proses Node.js kita 24 jam sehari, mendistribusikan beban ke seluruh core CPU (Clustering Mode), dan otomatis menghidupkan kembali aplikasi kita jika mengalami crash atau setelah server reboot.

1. Instalasi PM2 Global #

# Menginstal PM2 secara global di sistem Linux kita
sudo npm install -g pm2

2. Menyusun Konfigurasi Aplikasi (ecosystem.config.js) #

Buat berkas bernama ecosystem.config.js di direktori proyek Node.js kita untuk mendefinisikan parameter operasional aplikasi:

// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'nodejs-prod-app',
    script: 'dist/server.js',        // Berkas entry point aplikasi kita
    instances: 'max',                 // Jalankan instansi sebanyak jumlah core CPU (Cluster Mode)
    exec_mode: 'cluster',             // Aktifkan mode klaster untuk penyeimbangan beban internal
    
    // Konfigurasi Variabel Lingkungan
    env: {
      NODE_ENV: 'development',
      PORT: 3000
    },
    env_production: {
      NODE_ENV: 'production',
      PORT: 3000
    },
    
    // Konfigurasi Berkas Log
    error_file: '/var/log/myapp/error.log',
    out_file: '/var/log/myapp/access.log',
    log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
    
    // Kebijakan Pemulihan Crash (Restart Policy)
    max_restarts: 10,                 // Maksimal restart otomatis beruntun sebelum berhenti
    min_uptime: '15s',                // Durasi minimal aplikasi harus hidup agar dianggap stabil
    max_memory_restart: '500M',       // Restart otomatis jika konsumsi RAM melebihi 500MB (leak protection)
    kill_timeout: 3000                // Waktu tunggu graceful shutdown sebelum dihentikan paksa (ms)
  }]
};

3. Menjalankan dan Menyimpan Proses PM2 #

# Jalankan aplikasi menggunakan file ecosystem dalam mode produksi
pm2 start ecosystem.config.js --env production

# Simpan daftar proses aktif PM2 ke konfigurasi sistem
pm2 save

# Mengonfigurasi PM2 agar otomatis berjalan kembali saat server Linux dinyalakan (reboot)
pm2 startup

Multi-Instance Load Balancing #

Jika kita memiliki server Node.js yang melayani lalu lintas sangat tinggi, menjalankan satu instansi pada satu port lokal terkadang tidak cukup. Kita dapat menjalankan beberapa proses Node.js pada port yang berbeda (misalnya port 3000, 3001, 3002, dan 3003) lalu memanfaatkan Caddy untuk mendistribusikan beban lalu lintas secara merata ke seluruh port tersebut.

Berikut adalah konfigurasi penyeimbangan beban (load balancing) kustom pada Caddyfile:

# Konfigurasi Load Balancing Caddy untuk klaster Node.js lokal
example.com {
    encode zstd gzip
    
    reverse_proxy {
        # Definisikan seluruh target instansi Node.js (Upstreams)
        to localhost:3000 localhost:3001 localhost:3002 localhost:3003
        
        # Kebijakan distribusi request: Round Robin (bergantian berurutan)
        lb_policy round_robin
        
        # Active Health Check: Uji rute /health setiap 10 detik
        health_uri /health
        health_interval 10s
        
        # Passive Health Check (Circuit Breaker):
        # Jika sebuah instansi gagal merespons sebanyak 3 kali berturut-turut,
        # isolasi instansi tersebut dari daftar routing selama 30 detik.
        fail_duration 30s
        max_fails 3
        
        # Teruskan header IP asli
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-Proto {scheme}
    }
}

Node.js dengan WebSocket #

WebSocket membolehkan komunikasi dua arah (full-duplex) secara real-time antara browser pengguna dengan server Node.js (misalnya untuk aplikasi chat atau dashboard live data).

Pada server proxy tradisional, kita harus menulis banyak baris konfigurasi tambahan untuk mengidentifikasi dan meneruskan header koneksi Upgrade dan Connection agar jabat tangan WebSocket dapat berjalan. Namun, di Caddy, dukungan WebSocket telah aktif secara transparan. Caddy secara cerdas mendeteksi request upgrade protokol dan langsung membuka jalur terowongan TCP biner tanpa memerlukan konfigurasi tambahan apa pun.

# Contoh perutean WebSocket Socket.io di Caddy
example.com {
    # Rute biasa untuk halaman statis frontend
    handle /static/* {
        root * /var/www/myapp
        file_server
    }
    
    # Rute koneksi WebSocket Socket.io.
    # Caddy akan meneruskan koneksi secara otomatis tanpa flag tambahan.
    handle /socket.io/* {
        reverse_proxy localhost:3000
    }
    
    # Rute API utama
    handle {
        reverse_proxy localhost:3000
    }
}

Zero-Downtime Deployment dengan Node.js #

Setiap kali kita memperbarui kode aplikasi Node.js di server, kita harus mematikan proses lama dan menjalankan proses baru. Jika kita melakukan restart secara kasar, server akan mengalami jeda mati (downtime) selama beberapa detik hingga menit yang menyebabkan pengguna melihat pesan error “502 Bad Gateway”.

Kita dapat menggabungkan kemampuan PM2 Cluster Mode (yang mendukung rolling restart) dengan validasi konfigurasi Caddy untuk menyusun skrip otomatisasi deployment tanpa downtime (zero-downtime deployment):

#!/bin/bash
# deploy-nodejs.sh — Skrip otomatisasi deploy aman tanpa downtime

PROJECT_DIR="/var/www/myapp"
CADDY_BIN="/usr/bin/caddy"

echo "=== Memulai Proses Zero-Downtime Deployment ==="

# 1. Masuk ke direktori proyek dan tarik kode terbaru dari repositori Git
cd "$PROJECT_DIR" || exit 1
git pull origin main

# 2. Instalasi dependensi baru khusus untuk kebutuhan produksi
npm ci --production

# 3. Lakukan kompilasi kode jika menggunakan TypeScript atau bundler
npm run build

# 4. Validasi apakah konfigurasi Caddyfile sistem saat ini masih valid
echo "Memvalidasi konfigurasi Caddyfile..."
$CADDY_BIN validate --config /etc/caddy/Caddyfile
if [ $? -ne 0 ]; then
    echo "✗ Validasi Caddyfile gagal! Batalkan deployment."
    exit 1
fi

# 5. Picu PM2 untuk melakukan rolling restart secara bertahap.
# PM2 akan me-reload satu per satu instansi cluster di latar belakang.
# Instansi lama tetap aktif melayani pengguna sampai instansi baru siap menerima traffic.
echo "Memulai rolling restart proses Node.js di PM2..."
pm2 reload ecosystem.config.js --env production

# 6. Tunggu selama 5 detik untuk memberikan waktu inisialisasi aplikasi
sleep 5

# 7. Lakukan verifikasi kesehatan sistem internal (Local Health Check)
echo "Menguji kesehatan rute aplikasi..."
HTTP_STATUS=$(curl -sf -o /dev/null -w "%{http_code}" http://localhost:3000/health)

if [ "$HTTP_STATUS" != "200" ]; then
    echo "✗ Uji kesehatan gagal dengan status: $HTTP_STATUS!"
    echo "Memicu rollback ke versi proses sebelumnya..."
    pm2 revert nodejs-prod-app
    exit 1
fi

echo "✓ Deployment berhasil diselesaikan dengan aman tanpa downtime!"

Express.js — Konfigurasi yang Diperlukan #

Ketika aplikasi Node.js Express.js berjalan di belakang reverse proxy seperti Caddy, Express secara default menganggap koneksi tersebut tidak aman (karena komunikasi Caddy ke Express menggunakan HTTP biasa, bukan HTTPS). Akibatnya, Express tidak dapat membaca alamat IP asli pengunjung atau protokol HTTPS asli yang digunakan browser.

Untuk mengatasinya, kita wajib mengonfigurasi Express agar mempercayai header proxy yang dikirimkan oleh Caddy, serta membatasi proses binding port hanya pada alamat IP localhost (127.0.0.1) agar tidak dapat diakses langsung dari internet luar:

// server.js (Aplikasi Express.js Produksi)
const express = require('express');
const app = express();

// PENTING: Aktifkan trust proxy agar Express mempercayai header X-Forwarded-* dari Caddy.
// Ini menjamin properti 'req.ip' mengembalikan IP asli browser pengunjung,
// dan 'req.secure' mendeteksi status HTTPS secara tepat untuk keamanan cookie session.
app.set('trust proxy', 1);

// Menyediakan endpoint uji kesehatan (health check)
app.get('/health', (req, res) => {
    res.status(200).json({
        status: 'UP',
        uptime: process.uptime(),
        timestamp: new Date().toISOString()
    });
});

// Middleware Logging dengan alamat IP asli klien
app.use((req, res, next) => {
    // req.ip sekarang berisi IP asli (misalnya 203.0.113.50) berkat 'trust proxy'
    console.log(`[${new Date().toISOString()}] ${req.ip} - ${req.method} ${req.path}`);
    next();
});

app.get('/api/users', (req, res) => {
    res.json([{ id: 1, name: 'Budi' }, { id: 2, name: 'Ani' }]);
});

// Jalankan server Express
const PORT = process.env.PORT || 3000;
const HOST = '127.0.0.1'; // BIND HANYA KE LOCALHOST (Aman dari akses port langsung luar)

app.listen(PORT, HOST, () => {
    console.log(`Server Express aktif secara internal pada http://${HOST}:${PORT}`);
});

Konfigurasi CORS Sentral di Level Caddy Gateway #

Skenario umum aplikasi web modern adalah memisahkan domain frontend SPA (misalnya app.example.com) dengan domain backend API (api.example.com). Browser pengguna akan memblokir request API karena aturan kebijakan keamanan asal yang sama (Same-Origin Policy).

Kita dapat menangani Cross-Origin Resource Sharing (CORS) langsung di tingkat server Caddy, sehingga meniadakan penulisan kode middleware CORS yang berulang di setiap layanan microservices Node.js kita:

# Konfigurasi CORS terpusat di Caddy
api.example.com {
    # Penanganan khusus untuk request metode OPTIONS (Preflight Request)
    @options method OPTIONS
    handle @options {
        header Access-Control-Allow-Origin      "https://app.example.com"
        header Access-Control-Allow-Methods     "GET, POST, PUT, DELETE, OPTIONS"
        header Access-Control-Allow-Headers     "Content-Type, Authorization"
        header Access-Control-Allow-Credentials "true"
        header Access-Control-Max-Age           "86400" # Simpan cache preflight selama 24 jam
        respond "" 204 # Kembalikan respons kosong tanpa konten
    }
    
    # Sisipkan header CORS untuk request transaksi reguler (GET, POST, dll.)
    header Access-Control-Allow-Origin      "https://app.example.com"
    header Access-Control-Allow-Credentials "true"
    
    # Kompresi dan teruskan ke backend Node.js
    encode zstd gzip
    reverse_proxy localhost:3000
}

Streaming Response dari Node.js (Server-Sent Events) #

Jika aplikasi Node.js kita menyediakan fitur streaming data berkelanjutan (seperti pembaruan live feed, pemantauan log server secara real-time, atau integrasi AI text streaming dengan Server-Sent Events / SSE), Caddy secara bawaan akan menahan respons streaming tersebut di dalam buffer memorinya sampai data selesai terkumpul baru dikirimkan sekaligus ke pengguna. Hal ini merusak pengalaman live streaming data kita.

Untuk mengatasinya, kita harus menonaktifkan mekanisme buffering respons pada reverse proxy Caddy dengan menyetel parameter flush_interval -1:

# Konfigurasi Caddyfile khusus untuk Streaming Response (SSE)
example.com {
    # Endpoint khusus untuk streaming data real-time
    handle /events/* {
        reverse_proxy localhost:3000 {
            # Menyetel flush interval ke -1 memaksa Caddy untuk segera
            # mengalirkan setiap chunk data dari Node.js ke browser klien tanpa buffering.
            flush_interval -1
        }
    }
    
    # Endpoint reguler lainnya tetap menggunakan buffering standar untuk efisiensi
    handle {
        reverse_proxy localhost:3000
    }
}

Diagram Alur Pemrosesan Request Node.js oleh Caddy #

Untuk memberikan visualisasi perjalanan request pengguna melewati server edge Caddy hingga diproses oleh klaster aplikasi Node.js, mari kita perhatikan diagram alir (flowchart) berikut:

flowchart TD
    A["Request Masuk Klien\n(HTTPS - Port 443)"] --> B["1. Caddy menangani jabat tangan TLS\n(Dekripsi data & validasi keamanan)"]
    
    B --> C{"2. Apakah rute request\ncocok dengan aset statis?"}
    
    C -- "Ya" --> D["3. Caddy baca file langsung dari disk\n(e.g., /static/app.js)"]
    D --> E["4. Sisipkan header Cache-Control agresif\ndan kirim kembali ke Klien"]
    
    C -- "Tidak" --> F["5. Evaluasi rute dinamis / API"]
    F --> G{"6. Apakah ada endpoint khusus\nServer-Sent Events (SSE)?"}
    
    G -- "Ya" --> H["7. Salurkan request via reverse_proxy\n(Flush interval -1: no buffering)"]
    G -- "Tidak" --> I["8. Salurkan request via reverse_proxy\n(Gunakan load balancer Round Robin)"]
    
    H --> J["9. PM2 Cluster Node.js\n(Distribusi ke proses Node.js Port 3000-3003)"]
    I --> J
    
    J --> K["10. Eksekusi logika bisnis aplikasi\n(Express/Fastify memproses data)"]
    K --> L["11. Respons dikirim kembali ke Caddy"]
    L --> M["12. Caddy mengompresi respons (Gzip/Zstd)\ndan mengirimkan ke Klien"]

Ringkasan #

  • Pola Isolasi Port: Selalu lakukan binding port aplikasi Node.js ke alamat lokal 127.0.0.1 (bukan 0.0.0.0) agar tidak dapat diakses langsung tanpa melewati Caddy.
  • Keamanan Cookie & Session: Aktifkan app.set('trust proxy', 1) pada Express.js agar aplikasi mengenali protokol HTTPS asli dan mendeteksi IP asli klien.
  • Efisiensi Aset Statis: Gunakan named matcher @static di Caddyfile untuk menyajikan berkas CSS/JS secara langsung guna menghemat beban pemrosesan RAM Node.js.
  • Supervisi Proses: Gunakan PM2 dalam mode klaster (exec_mode: cluster) di produksi untuk kestabilan proses, pemulihan crash otomatis, dan pemanfaatan seluruh core CPU.
  • Graceful Reload: Terapkan perintah pm2 reload alih-alih pm2 restart saat melakukan deployment baru demi menjamin transisi proses tanpa downtime.
  • Streaming Real-time: Setel properti flush_interval -1 pada rute proxy untuk API berbasis Server-Sent Events (SSE) agar data tidak tertahan di buffer.
  • Sentralisasi CORS: Sentralisasikan penanganan header CORS di tingkat Caddyfile untuk mempermudah konfigurasi keamanan API multi-layanan.

← Sebelumnya: Buat Plugin   Berikutnya: PHP-FPM →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact