← Semua pembelajaran / Astro Nol → Portal Berita
Fase 10 · Deploy di AWS

Node adapter & bentuk hasil build

Sebelum menulis Dockerfile, kamu perlu tahu bentuk artefaknya — karena separuh kesalahan deploy berasal dari menyalin hal yang salah.

Sumber asli docs.astro.build Resmi Rangkuman ~8 menit baca

Intisari

  • mode: "standalone" menghasilkan server yang bisa dijalankan sendiri. middleware untuk disisipkan ke server lain.
  • Hasilnya: dist/server/ (kode server), dist/client/ (aset publik), dan entry.mjs.
  • HOST harus 0.0.0.0 di container — localhost berarti tidak bisa dihubungi dari luar.
  • Graceful shutdown wajib: ECS mengirim SIGTERM, dan tanpa penanganan, request yang sedang berjalan terputus.
  • Node tidak menyajikan aset seefisien nginx — tapi di belakang Cloudflare, itu hampir tidak berarti.

Konfigurasi

// astro.config.mjs
import node from "@astrojs/node";

export default defineConfig({
  site: "https://portal.contoh.id",
  output: "server",
  adapter: node({ mode: "standalone" }),
});
ModeHasilnyaPakai kalau
standaloneServer HTTP lengkap, jalankan dengan node ./dist/server/entry.mjsContainer
middlewareHandler untuk disisipkan ke Express/FastifyMenempel ke aplikasi Node yang sudah ada

Bentuk dist/

dist/
├── server/
│   ├── entry.mjs            ← titik masuk; ini yang dijalankan
│   ├── manifest_*.mjs
│   ├── pages/               ← satu modul per rute on-demand
│   ├── chunks/
│   └── renderers.mjs
└── client/
    ├── _astro/              ← JS & CSS ber-hash
    ├── favicon.ico          ← isi public/ disalin apa adanya
    └── robots.txt
pnpm build
du -sh dist/server dist/client
find dist/client/_astro -name '*.js' | wc -l

Yang perlu dibawa ke container hanya dist/, package.json, dan node_modules produksi. Bukan src/, bukan .git, bukan node_modules lengkap. Sebagian besar dependensi sudah dibundel ke dalam dist/server/; yang tersisa di node_modules produksi biasanya cuma yang di-externalize seperti mysql2 dan sharp.

Variabel lingkungan yang dibaca adapter

VariabelDefaultDi container
HOSTlocalhost0.0.0.0
PORT4321Boleh tetap

HOST=0.0.0.0 adalah kesalahan deploy pertama yang hampir semua orang lakukan. Dengan default localhost, server hanya mendengarkan antarmuka loopback di dalam container — ALB tidak bisa menghubunginya, health check gagal, dan ECS terus-menerus mengganti task. Log aplikasinya bersih: server memang berjalan, hanya tidak bisa dijangkau siapa pun.

Server pembungkus untuk hal yang tidak diurus Astro

// server.mjs — dijalankan alih-alih entry.mjs langsung
import { handler as astro } from "./dist/server/entry.mjs";
import { createServer } from "node:http";
import { db, dbBaca, dbTulis } from "./dist/server/lib-db.mjs";

const PORT = Number(process.env.PORT ?? 4321);
let sedangMati = false;
let aktif = 0;

const server = createServer((req, res) => {
  // Health check TIDAK menyentuh database — lihat catatan di bawah.
  if (req.url === "/sehat") {
    res.writeHead(sedangMati ? 503 : 200, { "Content-Type": "text/plain" });
    return res.end(sedangMati ? "mati" : "ok");
  }

  aktif++;
  res.on("finish", () => aktif--);
  res.on("close", () => aktif--);

  astro(req, res, (err) => {
    if (err) {
      console.error(JSON.stringify({ level: "error", e: String(err) }));
      res.writeHead(500).end("error");
    } else {
      res.writeHead(404).end("not found");
    }
  });
});

// Timeout server — sama pentingnya dengan timeout kueri (Fase 2).
server.headersTimeout = 20_000;
server.requestTimeout = 30_000;
server.keepAliveTimeout = 65_000;    // HARUS lebih besar dari idle timeout ALB

server.listen(PORT, "0.0.0.0", () => {
  console.log(JSON.stringify({ level: "info", pesan: "siap", port: PORT }));
});

keepAliveTimeout harus lebih besar dari idle timeout ALB. Kalau lebih kecil, Node menutup koneksi tepat saat ALB mengira masih bisa dipakai — dan ALB mengembalikan 502 acak yang tidak meninggalkan jejak apa pun di log aplikasimu. Dengan idle timeout ALB 60 detik, setel keepAliveTimeout ke 65 detik. Ini penyebab paling umum dari 502 misterius pada aplikasi Node di belakang ALB.

Graceful shutdown

async function matikan(sinyal) {
  if (sedangMati) return;
  sedangMati = true;

  console.log(JSON.stringify({ level: "info", pesan: "mulai mati", sinyal, aktif }));

  // 1. Health check langsung 503 → ALB berhenti mengirim request baru.
  // 2. Beri waktu ALB menyadarinya sebelum menutup listener.
  await new Promise((r) => setTimeout(r, 5000));

  // 3. Berhenti menerima koneksi baru; selesaikan yang berjalan.
  server.close();

  // 4. Tunggu request yang sedang jalan, maksimal 20 detik.
  const batas = Date.now() + 20_000;
  while (aktif > 0 && Date.now() < batas) {
    await new Promise((r) => setTimeout(r, 200));
  }

  // 5. Tutup pool database.
  await Promise.allSettled([dbBaca.destroy(), dbTulis.destroy()]);

  console.log(JSON.stringify({ level: "info", pesan: "mati bersih", sisa: aktif }));
  process.exit(0);
}

process.on("SIGTERM", () => matikan("SIGTERM"));
process.on("SIGINT", () => matikan("SIGINT"));
Tanpa graceful shutdownDengan
Request yang sedang jalan terputusSelesai normal
Pembaca melihat 502 saat tiap deployTidak ada yang menyadari
Webhook Midtrans gagal dan diulangSelesai
Koneksi database menggantungDitutup rapi

Jeda 5 detik di langkah 2 adalah bagian yang paling sering dilewati. ALB butuh beberapa detik untuk menyadari health check sudah gagal. Kalau kamu langsung menutup listener saat menerima SIGTERM, ALB masih akan mengirim request ke port yang sudah tertutup — dan itu tepatnya 502 yang ingin kamu hindari.

Dua health check yang berbeda

// /sehat — untuk ALB. TIDAK menyentuh database.
// /siap — untuk pemeriksaan manual & alarm. Menyentuh database.

if (req.url === "/siap") {
  try {
    await sql`SELECT 1`.execute(dbBaca);
    res.writeHead(200).end(JSON.stringify({ ok: true }));
  } catch (e) {
    res.writeHead(503).end(JSON.stringify({ ok: false, e: String(e) }));
  }
  return;
}

Health check ALB yang menyentuh database mengubah gangguan sebagian jadi pemadaman total. Kalau database melambat, seluruh health check gagal, ECS membunuh semua task, dan portalmu mati sepenuhnya — padahal halaman yang di-cache dan halaman statis masih bisa dilayani. Health check ALB harus menjawab pertanyaan "apakah proses ini hidup", bukan "apakah semua dependensi sehat".

Latihan: bangun proyekmu dan periksa isi dist/. Tulis server.mjs dengan graceful shutdown, jalankan, lalu kirim SIGTERM di tengah request yang lambat — gunakan endpoint uji yang tidur 5 detik. Verifikasi request itu selesai dan proses keluar dengan kode 0. Lalu ukur berapa lama proses mati saat tidak ada request sama sekali.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.