Aplikasi yang siap dipindahkan ke server belum otomatis berperilaku benar setelah menyala di sana. Di balik platform produksi ada penyeimbang beban, batas waktu, dan proxy yang bekerja dengan aturan sendiri — dan aplikasi yang tidak menyesuaikan diri akan menunjukkan gejala aneh: koneksi yang terputus sendiri, alamat IP pengguna yang salah terbaca, atau detail kesalahan internal yang bocor ke publik. Arsitektur pembacaan konfigurasi sudah kita bangun di chapter Konfigurasi Aplikasi; chapter ini fokus pada keputusan yang hanya relevan saat produksi: port dari environment, timeout server, aplikasi di balik proxy, dan penyembunyian detail galat.

Port dan Mode dari Environment #

Dua variabel paling sering disuntikkan platform adalah PORT dan NODE_ENV. Alasannya berbeda. Banyak platform — dari Heroku hingga container orchestrator — menetapkan port melalui environment karena merekalah yang mengelola port mana yang terbuka; aplikasi kita tinggal membaca dan mendengarkan port itu. Adapun NODE_ENV=production, seperti dibahas di Chapter Persiapan Deployment, dibaca oleh framework untuk memilih perilaku produksi. Penggunaan keduanya di titik masuk aplikasi terlihat seperti ini:

js Pola pembacaan PORT dan NODE_ENV di titik masuk
const port = Number(process.env.PORT ?? 3000);
const modeProduksi = process.env.NODE_ENV === 'production';

Perhatikan dua hal pada pola tersebut. Nilai dari environment selalu berupa string, sehingga PORT perlu dikonversi dan divalidasi — pekerjaan yang sebaiknya terkumpul di satu modul konfigurasi. Dan nilai cadangan pada ?? perlu dipilih dengan sadar: 3000 nyaman saat pengembangan, sementara 0 (port ephemeral) cocok untuk pengujian otomatis. Membaca environment variable langsung di banyak tempat tetap tidak dianjurkan — panduan environment variable Node.js dan arsitektur satu-modul dari chapter Konfigurasi Aplikasi menjelaskan alasannya.

Timeout Bawaan Server HTTP #

Server HTTP bawaan Node.js membawa empat pengaturan waktu yang jarang kita sentuh saat pengembangan, tetapi menentukan perilaku di bawah beban nyata:

PropertiMengaturNilai bawaan Node 24
server.keepAliveTimeoutlamanya koneksi idle ditahan setelah respons terakhir5000
server.headersTimeoutbatas waktu klien mengirim lengkap header request60000
server.requestTimeoutbatas waktu satu request diterima sampai selesai300000
server.timeoutbatas waktu socket secara umum0 (nonaktif)

Nilai keepAliveTimeout layak mendapat perhatian khusus ketika aplikasi berjalan di belakang penyeimbang beban. Penyeimbang beban juga menahan koneksi idle — biasanya lebih lama dari lima detik. Ketika penyeimbang menganggap koneksi masih hidup sementara server kita sudah menutupnya, request berikutnya bisa menabrak koneksi yang baru saja ditutup dan pengguna menerima galat acak yang sulit ditelusuri. Praktik yang dianjurkan: naikkan keepAliveTimeout aplikasi melebihi idle timeout penyeimbang beban, sehingga pihak yang menutup koneksi selalu penyeimbang — bukan server kita. Nilai bawaan seluruh properti ini terdokumentasi di referensi server.keepAliveTimeout .

Menyembunyikan Detail Galat di Respons #

Saat pengembangan, pesan kesalahan yang lengkap sangat membantu: kita langsung tahu tabel mana yang gagal dibaca. Di produksi, detail yang sama berbahaya — pesan galat database bisa membocorkan nama tabel, struktur query, sampai petunjuk bagi penyerang. Prinsipnya: detail lengkap hanya ke log di sisi server; klien menerima pesan umum tanpa rincian. Berkas berikut menerapkan prinsip itu dengan mode yang dikendalikan NODE_ENV:

js
import http from 'node:http';

// Waktu dicetak memakai jam beku agar keluaran contoh selalu sama.
const JAM_DEMO = new Date(Date.UTC(2026, 0, 15, 8, 0, 0));

const modeProduksi = process.env.NODE_ENV === 'production';
const port = Number(process.env.PORT ?? 0);

function catatError(pesan) {
  console.error(JSON.stringify({
    waktu: JAM_DEMO.toISOString(),
    level: 'error',
    pesan,
  }));
}

const server = http.createServer((req, res) => {
  if (req.url === '/health') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ status: 'ok' }));
    return;
  }
  if (req.url === '/tugas') {
    // simulasi kegagalan tak terduga di dalam handler
    const galat = new Error('gagal membaca tabel tugas: berkas database terkunci');
    catatError(galat.message);
    res.writeHead(500, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify(modeProduksi
      ? { pesan: 'terjadi kesalahan internal' }
      : { pesan: galat.message }));
    return;
  }
  res.writeHead(404, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ pesan: 'tidak ditemukan' }));
});

server.listen(port, '127.0.0.1', async () => {
  const alamat = `http://127.0.0.1:${server.address().port}`;
  console.log(`mode           : ${modeProduksi ? 'production' : 'pengembangan'}`);
  console.log(`keepAliveTimeout: ${server.keepAliveTimeout}`);
  console.log(`headersTimeout : ${server.headersTimeout}`);
  console.log(`requestTimeout : ${server.requestTimeout}`);
  for (const jalur of ['/health', '/tugas']) {
    const r = await fetch(`${alamat}${jalur}`);
    console.log(`${jalur.padEnd(8)} -> ${r.status} ${await r.text()}`);
  }
  server.close();
});

Jalankan lebih dulu tanpa NODE_ENV — keadaan pengembangan:

bash
node server-produksi.mjs

Output yang Dihasilkan:

text
mode           : pengembangan
keepAliveTimeout: 5000
headersTimeout : 60000
requestTimeout : 300000
/health  -> 200 {"status":"ok"}
{"waktu":"2026-01-15T08:00:00.000Z","level":"error","pesan":"gagal membaca tabel tugas: berkas database terkunci"}
/tugas   -> 500 {"pesan":"gagal membaca tabel tugas: berkas database terkunci"}

Lalu ulangi dengan mode produksi:

bash
NODE_ENV=production node server-produksi.mjs

Output yang Dihasilkan:

text
mode           : production
keepAliveTimeout: 5000
headersTimeout : 60000
requestTimeout : 300000
/health  -> 200 {"status":"ok"}
{"waktu":"2026-01-15T08:00:00.000Z","level":"error","pesan":"gagal membaca tabel tugas: berkas database terkunci"}
/tugas   -> 500 {"pesan":"terjadi kesalahan internal"}

Mari kita bedah perbedaan kedua keluaran tersebut:

  1. Empat baris pertama: aplikasi mencetak mode aktif beserta tiga properti timeout bawaan — angka yang sama dengan tabel di atas, dibaca langsung dari instance server. Mencetak nilai ini di awal adalah kebiasaan yang baik: operator langsung tahu konfigurasi yang berlaku tanpa membaca kode.
  2. Baris /health: merespons 200 dengan {"status":"ok"} di kedua mode — titik pemeriksaan kesehatan dari chapter sebelumnya tetap bekerja.
  3. Baris JSON level:"error": detail galat lengkap selalu ditulis ke log sisi server lewat console.error — di aplikasi nyata inilah satu-satunya tempat detail hidup, dan waktu pada contoh sengaja dibekukan agar keluarannya dapat direproduksi.
  4. Baris /tugas pada mode pengembangan: respons 500 masih memuat pesan galat mentah — praktik yang bisa diterima saat kita men-debug aplikasi sendiri.
  5. Baris /tugas pada mode produksi: klien hanya menerima "terjadi kesalahan internal"; nama tabel dan penyebabnya tidak pernah meninggalkan server. Ekspresi kondisional pada JSON.stringify adalah seluruh mekanisme perubahannya.

Aplikasi di Balik Proxy #

Di produksi, request jarang datang langsung ke aplikasi. Umumnya ada proxy terbalik atau penyeimbang beban di depannya — Nginx, HAProxy, atau load balancer milik platform. Akibatnya, req.socket.remoteAddress yang kita baca bukan lagi alamat pengguna, melainkan alamat proxy. Solusi standarnya: proxy menambahkan header X-Forwarded-For berisi alamat asli, dan aplikasi memilih memercayai header itu hanya bila request memang datang dari proxy yang dikenal. Demonstrasi berikut mengendalikan pilihan itu lewat variabel TRUST_PROXY:

js
import http from 'node:http';

const percayaProxy = process.env.TRUST_PROXY === '1';

const server = http.createServer((req, res) => {
  const ipSocket = req.socket.remoteAddress;
  const ipTerusan = req.headers['x-forwarded-for'];
  const pakaiTerusan = percayaProxy && typeof ipTerusan === 'string';
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({
    percayaProxy,
    ipKlien: pakaiTerusan ? ipTerusan : ipSocket,
    sumber: pakaiTerusan ? 'header X-Forwarded-For' : 'alamat socket',
  }));
});

server.listen(0, '127.0.0.1', async () => {
  const alamat = `http://127.0.0.1:${server.address().port}/siapa`;
  const r = await fetch(alamat, { headers: { 'X-Forwarded-For': '203.0.113.7' } });
  console.log(await r.text());
  server.close();
});

Klien pengujian mengirim header X-Forwarded-For: 203.0.113.7 pada kedua percobaan. Tanpa kepercayaan proxy, header tersebut diabaikan:

bash
node server-proxy.mjs

Output yang Dihasilkan:

text
{"percayaProxy":false,"ipKlien":"127.0.0.1","sumber":"alamat socket"}

Dengan TRUST_PROXY=1, header tersebut dipercaya:

bash
TRUST_PROXY=1 node server-proxy.mjs

Output yang Dihasilkan:

text
{"percayaProxy":true,"ipKlien":"203.0.113.7","sumber":"header X-Forwarded-For"}

Perhatikan baik-baik apa yang barusan terjadi pada percobaan kedua: request kita datang langsung dari 127.0.0.1 — bukan dari proxy mana pun — tetapi server memercayai header palsu yang kita tulis sendiri. Inilah alasan kepercayaan tidak boleh berupa saklar 1/0 di dunia nyata: aplikasi harus memercayai hanya lapisan proxy yang dikenal alamatnya. Pada Express, pengaturan ini bernama trust proxy:

js Pada Express: batasi kepercayaan pada proxy yang dikenal
app.set('trust proxy', 1);

Nilai 1 berarti hanya satu hop di depan aplikasi yang dipercaya — pola yang sesuai untuk satu lapisan penyeimbang beban. Rincian seluruh bentuk nilainya terdokumentasi di panduan Express menjalankan aplikasi di balik proxy .

Satu Pintu Konfigurasi #

Seluruh nilai di atas — PORT, NODE_ENV, TRUST_PROXY, dan nantinya alamat database — sebaiknya mengalir melalui satu modul konfigurasi tervalidasi seperti bacaKonfigurasi pada chapter Konfigurasi Aplikasi. Untuk kenyamanan lokal, flag node --env-file dapat memuat berkas .env; di lingkungan produksi, suntikkan nilai langsung dari platform atau pengaturan rahasia layanan — mekanisme berkas .env dibahas tuntas di chapter Berkas Environment dengan –env-file, dan penyimpanan kredensial di chapter Manajemen Secret.

Konfigurasi yang benar membuat aplikasi berperilaku sesuai lingkungannya. Pertanyaan berikutnya: siapa yang menjaga aplikasi tetap menyala ketika prosesnya berhenti karena galat atau pembaruan? Jawabannya adalah process manager — topik Chapter Process Manager dengan PM2.