Просмотр исходного кода

feat: add db:push script and api.hanoman.co.id to trusted origins

yusuf 1 месяц назад
Родитель
Сommit
2890a87a46

+ 62 - 4
docs/DEPLOY-FRONTEND-PM2-NGINX.md

@@ -54,8 +54,17 @@ docker compose --env-file .env -f docker-compose.yml up -d
 Pastikan:
 
 - `.env` berisi `DATABASE_URL`, `PAYLOAD_SECRET`, kredensial Postgres, dll.
+- **`DATABASE_URL` di dalam Docker** harus memakai hostname **`postgres`** (nama service di Compose), bukan `127.0.0.1`, agar container `payload` bisa konek ke DB.
 - Folder **`media/`** di host ter-mount ke container (lihat `docker-compose.yml`) agar upload media tidak hilang.
 
+**Deploy pertama / database baru:** dengan `NODE_ENV=production`, Payload **tidak** menjalankan push schema otomatis. Setelah Postgres sehat, jalankan sekali (dari folder backend di VPS):
+
+```bash
+docker compose --profile db-push run --rm db-push
+```
+
+Atau dari mesin yang bisa reach DB dengan env yang sama: `pnpm run db:push`. Baru setelah tabel ada, buka admin dan buat user pertama.
+
 Uji cepat dari VPS:
 
 ```bash
@@ -195,6 +204,9 @@ server {
     listen 80;
     server_name api.example.com;
 
+    # Admin upload media — naikkan jika perlu (default Nginx 1m sering terlalu kecil)
+    client_max_body_size 50M;
+
     location / {
         proxy_pass http://127.0.0.1:3000;
         proxy_http_version 1.1;
@@ -204,6 +216,8 @@ server {
         proxy_set_header X-Forwarded-Proto $scheme;
         proxy_set_header Upgrade $http_upgrade;
         proxy_set_header Connection "upgrade";
+        proxy_read_timeout 300s;
+        proxy_send_timeout 300s;
     }
 }
 ```
@@ -217,6 +231,8 @@ server {
     listen 80;
     server_name www.example.com example.com;
 
+    client_max_body_size 20M;
+
     location / {
         proxy_pass http://127.0.0.1:3001;
         proxy_http_version 1.1;
@@ -226,6 +242,7 @@ server {
         proxy_set_header X-Forwarded-Proto $scheme;
         proxy_set_header Upgrade $http_upgrade;
         proxy_set_header Connection "upgrade";
+        proxy_read_timeout 120s;
     }
 }
 ```
@@ -272,10 +289,37 @@ Port **3000** dan **3001** tidak perlu dibuka ke publik jika hanya diakses lewat
 
 ---
 
-## 8. CORS dan form kontak
+## 8. Backend: URL publik, CORS, CSRF, dan form kontak
+
+Konfigurasi aktual ada di `src/payload.config.ts` (bukan `cors: '*'`).
+
+### 8.1 `PAYLOAD_SERVER_URL` (`.env` backend)
 
-- Backend Payload mengatur **`cors: '*'`** di `payload.config.ts`; untuk API umum biasanya cukup.
-- Endpoint **`POST /api/contact`** memakai **`FRONTEND_ORIGIN`** — set ke origin frontend production, mis. `https://www.example.com`, agar preflight CORS konsisten.
+Set ke URL publik API, **tanpa** slash di akhir, mis.:
+
+```env
+PAYLOAD_SERVER_URL=https://api.example.com
+```
+
+Ini dipakai Payload untuk admin, redirect, dan perilaku URL absolut. Setelah mengubahnya, **build ulang image** dan `docker compose up -d --build`.
+
+### 8.2 `trustedBrowserOrigins` (kode backend)
+
+Array **`trustedBrowserOrigins`** dipakai untuk **`cors`** dan **`csrf`**. Origin browser yang mengakses REST/GraphQL atau mengirim cookie admin **harus** terdaftar, mis.:
+
+- `https://www.example.com` (situs frontend)
+- `https://example.com` (jika dipakai)
+- `https://api.example.com` (jika admin dibuka dari subdomain yang sama)
+
+Tambahkan origin demo/production di array itu, commit, lalu deploy image baru. Jika origin tidak ada di daftar, fetch dari frontend atau login admin bisa gagal dengan error CORS/CSRF.
+
+### 8.3 `FRONTEND_ORIGIN` (`.env` backend)
+
+Endpoint **`POST /api/contact`** memakai **`FRONTEND_ORIGIN`** untuk header CORS. Isi persis origin frontend, mis. `https://www.example.com` (skema + host, tanpa path).
+
+### 8.4 Cookie admin (opsional)
+
+Jika admin di `api.example.com` dan frontend di `www.example.com`, cookie default **scoped per host**. Untuk skenario subdomain kompleks, pertimbangkan `COOKIE_DOMAIN` di `.env` (lihat `src/collections/Users.ts`) — hanya set jika Anda paham dampaknya terhadap keamanan cookie.
 
 ---
 
@@ -307,7 +351,11 @@ docker compose --env-file .env -f docker-compose.yml up -d --build
 | 502 Bad Gateway ke FE | `pm2 status`, `curl http://127.0.0.1:3001/` |
 | 502 ke API | `docker compose ps`, `curl http://127.0.0.1:3000/api/` |
 | Frontend tidak bisa fetch API | `NEXT_PUBLIC_*` salah atau belum rebuild; URL harus **https** setelah Certbot |
+| CORS / CSRF / fetch API dari browser gagal | Tambahkan origin frontend (dan API jika perlu) ke `trustedBrowserOrigins` di backend; set `PAYLOAD_SERVER_URL`; deploy ulang image backend |
+| Admin redirect aneh atau mixed content | `PAYLOAD_SERVER_URL` harus `https://...` sama dengan domain Nginx |
+| `relation "users" does not exist` | DB baru tanpa schema: jalankan `docker compose --profile db-push run --rm db-push` (lihat §3) |
 | Media backend 404 | Volume `./media` di Docker dan isi folder `media/` di host |
+| Upload admin gagal (413) | Naikkan `client_max_body_size` di Nginx untuk `api.*` |
 
 ---
 
@@ -319,4 +367,14 @@ docker compose --env-file .env -f docker-compose.yml up -d --build
 | Frontend (PM2) | `3001` | Via Nginx `www.example.com` |
 | PostgreSQL (Docker) | `5432` (hanya jika perlu; jangan expose ke internet tanpa kebutuhan) | — |
 
-Dokumen ini hanya menjelaskan **frontend + PM2 + Nginx**; manajemen secret, backup DB, dan hardening server mengikuti kebijakan Anda.
+## 12. Checklist singkat sebelum dianggap selesai
+
+- [ ] Backend: `.env` lengkap (`PAYLOAD_SECRET`, `DATABASE_URL` dengan host `postgres`, `PAYLOAD_SERVER_URL`, `FRONTEND_ORIGIN` untuk contact).
+- [ ] Backend: origin production ada di `trustedBrowserOrigins` + image sudah di-build ulang.
+- [ ] Deploy pertama: `db-push` sudah dijalankan jika DB kosong.
+- [ ] Frontend: `.env.production` + `pnpm build` setelah setiap ubah `NEXT_PUBLIC_*`.
+- [ ] Nginx: SSL aktif, `client_max_body_size` cukup untuk upload admin.
+- [ ] Firewall: hanya SSH + HTTP(S); port 3000/3001 tidak perlu publik.
+- [ ] `chmod 600 .env` (backend & frontend) di server.
+
+Dokumen ini fokus ke **frontend + PM2 + Nginx** dan integrasi dengan backend Docker; backup DB, rotasi secret, dan hardening lanjutan mengikuti kebijakan Anda.

+ 569 - 0
docs/README-DEPLOY-HANOMAN-SAME-DOMAIN.md

@@ -0,0 +1,569 @@
+# Deploy Hanoman — skenario A (IP / VPS sama, domain tetap)
+
+Panduan ini untuk **mengganti situs yang sudah ada** di **`hanoman.co.id`** dengan stack baru (frontend Next terpisah + backend Payload + Postgres di Docker), **tanpa pindah server** dan **tanpa mengganti IP** yang sudah dipakai domain tersebut.
+
+Asumsi:
+
+- Record DNS **`hanoman.co.id`** dan **`www.hanoman.co.id`** sudah mengarah ke **VPS yang sama** tempat Anda mengerjakan deploy.
+- Repositori **frontend** terpisah dari **backend** (`hanoman-website-be`).
+- Target URL publik setelah selesai:
+
+| Peran | URL |
+|-------|-----|
+| Situs pengunjung (Next.js frontend) | `https://hanoman.co.id` dan/atau `https://www.hanoman.co.id` |
+| API Payload, admin, REST `/api/...` | `https://api.hanoman.co.id` |
+
+Frontend memanggil backend lewat variabel `NEXT_PUBLIC_*` yang mengarah ke **`https://api.hanoman.co.id`**, bukan ke path `/api` di domain utama — menghindari bentrok dengan route Next di root domain.
+
+---
+
+## 1. Gambaran arsitektur di VPS
+
+```
+Internet
+   │
+   ▼
+┌────────────────────────┐
+│  Nginx :80 / :443      │  SSL (Let’s Encrypt), reverse proxy
+└───────────┬────────────┘
+            │
+       ┌────┴─────┐
+       ▼          ▼
+┌─────────────┐  ┌──────────────────────────────────┐
+│ PM2         │  │ Docker Compose                    │
+│ Next.js FE  │  │  • postgres                       │
+│ 127.0.0.1   │  │  • payload → 127.0.0.1:3000       │
+│ :3001       │  │  • volume ./media → /app/media    │
+└─────────────┘  └──────────────────────────────────┘
+```
+
+- **Nginx** menerima `hanoman.co.id` / `www` → memproksi ke **`http://127.0.0.1:3001`** (frontend).
+- **Nginx** menerima `api.hanoman.co.id` → memproksi ke **`http://127.0.0.1:3000`** (Payload).
+- Port **3000** dan **3001** cukup listen di localhost; yang terbuka ke internet adalah **80** dan **443** (Nginx).
+
+---
+
+## 2. Prasyarat di VPS
+
+- OS: **Ubuntu 22.04 atau 24.04 LTS** (perintah di bawah memakai **`apt`**).
+- **Docker** + **Docker Compose** (subperintah `docker compose`).
+- **Git**, **curl**, **ca-certificates**.
+- Akses pengguna ke **`sudo`**.
+- **Domain:** **A record** berikut mengarah ke **IP VPS yang sama**:
+  - `hanoman.co.id`
+  - `www.hanoman.co.id`
+  - **`api.hanoman.co.id`** (tambahkan jika belum ada).
+
+### 2.1 Cek cepat (sudah terpasang atau belum)
+
+Jalankan di VPS:
+
+```bash
+lsb_release -a 2>/dev/null || cat /etc/os-release
+command -v sudo && sudo -n true 2>/dev/null && echo "sudo: OK" || echo "sudo: perlu password atau belum ada"
+docker --version 2>/dev/null || echo "Docker: belum terpasang"
+docker compose version 2>/dev/null || echo "docker compose: belum terpasang"
+git --version 2>/dev/null || echo "Git: belum terpasang"
+curl --version 2>/dev/null | head -1 || echo "curl: belum terpasang"
+```
+
+### 2.2 Instalasi paket dasar (curl, git, sudo)
+
+Hanya jika belum ada (misalnya image VPS minimal tanpa `curl`/`git`):
+
+```bash
+apt update
+apt install -y sudo curl ca-certificates git
+```
+
+Jika Anda **bukan** root, pakai:
+
+```bash
+sudo apt update
+sudo apt install -y curl ca-certificates git
+```
+
+### 2.3 Instalasi Docker Engine + Docker Compose (plugin)
+
+**Opsi A — dari repositori Ubuntu (cukup untuk panduan ini)**
+
+```bash
+sudo apt update
+sudo apt install -y docker.io docker-compose-v2
+sudo systemctl enable --now docker
+sudo usermod -aG docker "$USER"
+```
+
+Keluar dari SSH dan masuk lagi (atau jalankan `newgrp docker`) agar grup **`docker`** aktif tanpa `sudo` untuk perintah `docker`.
+
+Cek:
+
+```bash
+docker --version
+docker compose version
+```
+
+Jika paket **`docker-compose-v2`** tidak ditemukan (distro lama), gunakan **Opsi B**.
+
+**Opsi B — skrip resmi Docker (Engine + Compose plugin, versi lebih baru)**
+
+```bash
+curl -fsSL https://get.docker.com -o /tmp/get-docker.sh
+sudo sh /tmp/get-docker.sh
+sudo usermod -aG docker "$USER"
+rm /tmp/get-docker.sh
+```
+
+Lalu **logout/login** atau `newgrp docker`, lalu:
+
+```bash
+docker --version
+docker compose version
+```
+
+### 2.4 Uji Docker tanpa sudo
+
+```bash
+docker run --rm hello-world
+```
+
+Jika error permission denied, pastikan user sudah di grup `docker` dan sesi login sudah diperbarui.
+
+### 2.5 Alat lain yang dipakai di panduan ini
+
+| Kebutuhan | Kapan muncul di panduan | Instal jika belum ada |
+|-----------|-------------------------|------------------------|
+| **Nginx** | Bagian reverse proxy | `sudo apt install -y nginx` |
+| **Certbot** | SSL Let’s Encrypt | `sudo apt install -y certbot python3-certbot-nginx` |
+| **UFW** | Firewall | `sudo apt install -y ufw` |
+| **build-essential** (opsional) | Beberapa `npm install` native | `sudo apt install -y build-essential` |
+
+### 2.6 Node.js (nvm, PM2)
+
+Node dan PM2 dijelaskan di **§4**; tidak wajib di §2 kecuali Anda ingin menginstal lebih dulu. Ringkas:
+
+```bash
+# nvm — ikuti §4.1
+# PM2 global — setelah Node ada:
+npm install -g pm2
+```
+
+---
+
+## 3. Persiapan kode backend (`hanoman-website-be`)
+
+### 3.1 Origin yang dipercaya (CORS / CSRF)
+
+File `src/payload.config.ts` memakai array **`trustedBrowserOrigins`** untuk **`cors`** dan **`csrf`**. Setiap origin browser yang mengakses API atau admin **harus** ada di daftar ini.
+
+Pastikan array tersebut memuat minimal:
+
+- `https://hanoman.co.id`
+- `https://www.hanoman.co.id`
+- `https://api.hanoman.co.id` (admin Payload dibuka dari subdomain ini)
+
+Simpan, commit, dan gunakan commit ini saat build image di VPS.
+
+### 3.2 Clone backend di VPS
+
+```bash
+sudo mkdir -p /opt
+sudo chown $USER:$USER /opt
+cd /opt
+git clone <URL-GIT-BACKEND> hanoman-website-be
+cd hanoman-website-be
+```
+
+### 3.3 File `.env` backend (di server)
+
+Buat atau salin `.env` di root `hanoman-website-be`. Isi wajar (sesuaikan password dan secret):
+
+```env
+# Postgres — dipakai service postgres di Docker Compose
+POSTGRES_DB=hanoman
+POSTGRES_USER=postgres
+POSTGRES_PASSWORD=ganti_password_kuat
+POSTGRES_HOST=postgres
+POSTGRES_PORT=5432
+
+# Koneksi dari container payload ke container postgres (hostname = nama service Compose)
+DATABASE_URL=postgresql://postgres:ganti_password_kuat@postgres:5432/hanoman
+
+PAYLOAD_SECRET=ganti_secret_panjang_acak
+NODE_ENV=production
+
+# URL publik API (tanpa slash di akhir)
+PAYLOAD_SERVER_URL=https://api.hanoman.co.id
+
+# Origin frontend untuk CORS endpoint kontak (skema + host, tanpa path)
+FRONTEND_ORIGIN=https://hanoman.co.id
+```
+
+Catatan:
+
+- **`DATABASE_URL`** untuk container **wajib** memakai hostname **`postgres`**, bukan `127.0.0.1`.
+- **`FRONTEND_ORIGIN`** harus sama dengan origin yang dipakai pengunjung (jika kanonis Anda `https://www.hanoman.co.id`, isi itu).
+- Variabel untuk email kontak / SMTP (jika dipakai) tetap sesuai kebutuhan proyek Anda.
+
+Amankan file:
+
+```bash
+chmod 600 .env
+```
+
+### 3.4 Jalankan Postgres dan Payload
+
+```bash
+cd /opt/hanoman-website-be
+docker compose --env-file .env -f docker-compose.yml up -d --build
+```
+
+Tunggu container `postgres` sehat, lalu cek:
+
+```bash
+docker compose ps
+```
+
+### 3.5 Buat tabel database (sekali, jika DB baru)
+
+Dengan **`NODE_ENV=production`**, Payload **tidak** mendorong schema otomatis. Setelah Postgres jalan, dari folder backend (dengan `pnpm` dan dependency terpasang):
+
+```bash
+cd /opt/hanoman-website-be
+corepack enable && corepack prepare pnpm@latest --activate
+pnpm install --frozen-lockfile
+pnpm run db:push
+```
+
+Perintah `db:push` menjalankan script yang memuat Payload dengan **`NODE_ENV=development`** hanya untuk proses tersebut, sehingga schema Drizzle diterapkan ke database.
+
+### 3.6 Uji backend di localhost VPS
+
+```bash
+curl -sS -o /dev/null -w "%{http_code}\n" http://127.0.0.1:3000/api/
+```
+
+Respons HTTP (bukan connection refused) berarti proses `payload` mendengarkan.
+
+### 3.7 Buat user admin
+
+Buka di browser (setelah langkah Nginx + SSL di bawah selesai, atau sementara dengan tunnel):  
+`https://api.hanoman.co.id/admin`  
+dan buat akun admin pertama.  
+(Jika SSL belum ada, Anda bisa uji sementara dari VPS dengan `curl` ke localhost — untuk login admin biasanya browser + HTTPS.)
+
+---
+
+## 4. Node.js, pnpm, dan PM2 (untuk frontend)
+
+### 4.1 Node (disarankan pakai nvm)
+
+Samakan **major** Node dengan `engines` di `package.json` frontend (misalnya 22):
+
+```bash
+curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
+source ~/.bashrc
+nvm install 22
+nvm use 22
+node -v
+```
+
+### 4.2 PM2 global
+
+```bash
+npm install -g pm2
+pm2 startup
+```
+
+Jalankan perintah `sudo` yang dikeluarkan `pm2 startup` agar PM2 hidup lagi setelah reboot.
+
+---
+
+## 5. Frontend — build dan PM2
+
+### 5.1 Clone dan install
+
+```bash
+cd /opt
+git clone <URL-GIT-FRONTEND> hanoman-frontend
+cd hanoman-frontend
+corepack enable
+corepack prepare pnpm@latest --activate
+pnpm install --frozen-lockfile
+```
+
+### 5.2 Environment production frontend
+
+Buat **`.env.production`** di root frontend. Contoh (sesuaikan nama variabel dengan yang dipakai kode frontend Anda):
+
+```env
+NEXT_PUBLIC_PAYLOAD_API_URL=https://api.hanoman.co.id
+```
+
+Aturan praktis:
+
+- Tanpa **slash** di akhir URL basis (`https://api.hanoman.co.id`).
+- Sesuaikan dengan cara kode menyusun path: jika kode menambahkan `'/api/posts'`, basis URL **tanpa** `/api`; jika kode menambahkan `'/posts'` ke basis yang sudah berisi `/api`, sesuaikan.
+
+Setiap mengubah variabel **`NEXT_PUBLIC_*`**, wajib **`pnpm run build`** ulang.
+
+### 5.3 Build
+
+```bash
+pnpm run build
+```
+
+### 5.4 PM2 — `ecosystem.config.cjs`
+
+Di `/opt/hanoman-frontend`, buat file `ecosystem.config.cjs`:
+
+```javascript
+module.exports = {
+  apps: [
+    {
+      name: 'hanoman-fe',
+      cwd: '/opt/hanoman-frontend',
+      script: 'node_modules/next/dist/bin/next',
+      args: 'start -p 3001 -H 127.0.0.1',
+      instances: 1,
+      autorestart: true,
+      max_memory_restart: '512M',
+      env: {
+        NODE_ENV: 'production',
+      },
+    },
+  ],
+}
+```
+
+Jalankan:
+
+```bash
+cd /opt/hanoman-frontend
+pm2 start ecosystem.config.cjs
+pm2 save
+```
+
+Uji:
+
+```bash
+curl -sS -o /dev/null -w "%{http_code}\n" http://127.0.0.1:3001/
+```
+
+---
+
+## 6. DNS untuk `api.hanoman.co.id`
+
+Di panel DNS (Cloudflare, registrar, dll.):
+
+- Tambah **A** record: **`api.hanoman.co.id`** → **IP VPS yang sama** dengan `hanoman.co.id`.
+- TTL bisa 300 detik saat pengujian.
+
+Tunggu propagasi (cek dengan `dig api.hanoman.co.id +short`).
+
+---
+
+## 7. Nginx — reverse proxy
+
+### 7.1 Pasang Nginx
+
+```bash
+sudo apt update
+sudo apt install -y nginx
+```
+
+### 7.2 Cadangkan konfigurasi situs lama (jika ada)
+
+```bash
+sudo cp -a /etc/nginx/sites-available/default /etc/nginx/sites-available/default.bak.$(date +%F) 2>/dev/null || true
+```
+
+Jika Anda punya file lain untuk domain lama, cadangkan juga sebelum mengganti isinya.
+
+### 7.3 Server block — API (`api.hanoman.co.id`)
+
+Buat `/etc/nginx/sites-available/hanoman-api`:
+
+```nginx
+server {
+    listen 80;
+    server_name api.hanoman.co.id;
+
+    client_max_body_size 50M;
+
+    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 Upgrade $http_upgrade;
+        proxy_set_header Connection "upgrade";
+        proxy_read_timeout 300s;
+        proxy_send_timeout 300s;
+    }
+}
+```
+
+### 7.4 Server block — frontend (`hanoman.co.id` dan `www`)
+
+Buat `/etc/nginx/sites-available/hanoman-www`:
+
+```nginx
+server {
+    listen 80;
+    server_name hanoman.co.id www.hanoman.co.id;
+
+    client_max_body_size 20M;
+
+    location / {
+        proxy_pass http://127.0.0.1:3001;
+        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 Upgrade $http_upgrade;
+        proxy_set_header Connection "upgrade";
+        proxy_read_timeout 120s;
+    }
+}
+```
+
+### 7.5 Aktifkan situs dan uji konfigurasi
+
+```bash
+sudo ln -sf /etc/nginx/sites-available/hanoman-api /etc/nginx/sites-enabled/
+sudo ln -sf /etc/nginx/sites-available/hanoman-www /etc/nginx/sites-enabled/
+sudo rm -f /etc/nginx/sites-enabled/default
+sudo nginx -t
+sudo systemctl reload nginx
+```
+
+---
+
+## 8. SSL (Let’s Encrypt)
+
+```bash
+sudo apt install -y certbot python3-certbot-nginx
+sudo certbot --nginx \
+  -d api.hanoman.co.id \
+  -d hanoman.co.id \
+  -d www.hanoman.co.id
+```
+
+Ikuti prompt. Certbot akan menambahkan `listen 443 ssl` dan pengaturan sertifikat.
+
+Setelah HTTPS aktif:
+
+1. Pastikan **`.env.production`** frontend memakai **`https://api.hanoman.co.id`** (bukan `http`).
+2. Build ulang frontend dan restart PM2:
+
+```bash
+cd /opt/hanoman-frontend
+pnpm run build
+pm2 restart hanoman-fe
+```
+
+3. Pastikan **`PAYLOAD_SERVER_URL`** di backend **`https://api.hanoman.co.id`**, lalu rebuild dan jalankan ulang container backend jika Anda mengubah `.env`:
+
+```bash
+cd /opt/hanoman-website-be
+docker compose --env-file .env -f docker-compose.yml up -d --build
+```
+
+---
+
+## 9. Firewall
+
+```bash
+sudo ufw allow OpenSSH
+sudo ufw allow 'Nginx Full'
+sudo ufw enable
+sudo ufw status
+```
+
+Tidak perlu membuka port **3000** dan **3001** ke publik jika semua lalu lintas lewat Nginx.
+
+---
+
+## 10. Cutover dari situs lama (skenario A)
+
+1. **Selesaikan** langkah backend (Docker), `db:push`, frontend (PM2), Nginx, dan SSL **sebelum** memutuskan mengganti traffic produksi, jika memungkinkan uji dengan hosts file lokal atau subdomain uji.
+2. **Cadangkan** file Nginx lama yang melayani `hanoman.co.id`.
+3. **Hentikan** proses lama yang memakai port **3001** (atau port lain yang bentrok) — misalnya PM2 app lama: `pm2 delete nama-app-lama`.
+4. **Reload Nginx** hanya jika `nginx -t` sukses.
+
+Downtime sering hanya **beberapa detik** saat `reload`. Jika perlu halaman maintenance, bisa sementara `return 503` di Nginx (opsional).
+
+---
+
+## 11. Memperbarui deploy nanti
+
+**Frontend:**
+
+```bash
+cd /opt/hanoman-frontend
+git pull
+pnpm install --frozen-lockfile
+# edit .env.production jika perlu
+pnpm run build
+pm2 restart hanoman-fe
+```
+
+**Backend:**
+
+```bash
+cd /opt/hanoman-website-be
+git pull
+docker compose --env-file .env -f docker-compose.yml up -d --build
+```
+
+Setelah mengubah **`trustedBrowserOrigins`** atau **`PAYLOAD_SERVER_URL`**, selalu **build ulang image** backend.
+
+---
+
+## 12. Troubleshooting
+
+| Gejala | Yang dicek |
+|--------|------------|
+| 502 ke frontend | `pm2 status`, `curl http://127.0.0.1:3001/` |
+| 502 ke API | `docker compose ps`, `curl http://127.0.0.1:3000/api/` |
+| CORS / CSRF / fetch gagal | Origin `https://hanoman.co.id`, `https://www.hanoman.co.id`, `https://api.hanoman.co.id` ada di `trustedBrowserOrigins`; `PAYLOAD_SERVER_URL=https://api.hanoman.co.id` |
+| Mixed content / redirect aneh di admin | Semua URL publik memakai **`https://`**, bukan `http` |
+| `relation "users" does not exist` | Jalankan **`pnpm run db:push`** sekali saat DB masih kosong |
+| Media 404 | Folder **`media/`** di host ter-mount ke container (`./media:/app/media`) dan file benar-benar ada di disk |
+| Upload admin gagal (413) | Naikkan `client_max_body_size` di blok `api.hanoman.co.id` |
+
+---
+
+## 13. Ringkasan port di host
+
+| Layanan | Alamat lokal | Publik |
+|---------|----------------|--------|
+| Payload (Docker) | `127.0.0.1:3000` | `https://api.hanoman.co.id` |
+| Frontend (PM2) | `127.0.0.1:3001` | `https://hanoman.co.id` / `https://www.hanoman.co.id` |
+| PostgreSQL | `127.0.0.1:5432` (opsional dari host) | Jangan expose ke internet tanpa kebutuhan |
+
+---
+
+## 14. Checklist sebelum dianggap selesai
+
+- [ ] A record `api.hanoman.co.id` mengarah ke IP VPS yang sama.
+- [ ] `.env` backend: `DATABASE_URL` memakai host `postgres`, `PAYLOAD_SERVER_URL`, `FRONTEND_ORIGIN`, `PAYLOAD_SECRET`.
+- [ ] `trustedBrowserOrigins` memuat ketiga origin HTTPS di atas.
+- [ ] `pnpm run db:push` sudah dijalankan untuk DB baru.
+- [ ] `.env.production` frontend memakai `https://api.hanoman.co.id` dan build sudah dijalankan setelah perubahan `NEXT_PUBLIC_*`.
+- [ ] Nginx + Certbot untuk ketiga hostname.
+- [ ] `ufw` aktif; SSH dan Nginx Full diizinkan.
+- [ ] `chmod 600` untuk `.env` backend dan file rahasia frontend.
+
+---
+
+## 15. Rollback kasar
+
+- Simpan salinan konfigurasi Nginx sebelum cutover.
+- Untuk mengembalikan perilaku lama: restore file Nginx, `sudo nginx -t && sudo systemctl reload nginx`, dan jalankan kembali proses/PM2 versi lama jika masih ada di server.
+
+Backup database Postgres (`pg_dump`) disarankan sebelum migrasi besar atau perubahan schema produksi.

+ 1 - 0
package.json

@@ -13,6 +13,7 @@
     "lint": "cross-env NODE_OPTIONS=--no-deprecation next lint",
     "payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
     "seed": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/seed.ts",
+    "db:push": "cross-env NODE_OPTIONS=--no-deprecation NODE_ENV=development tsx src/scripts/push-db-schema.ts",
     "start": "cross-env NODE_OPTIONS=--no-deprecation next start -H 0.0.0.0 -p 3000",
     "test": "pnpm run test:int && pnpm run test:e2e",
     "test:e2e": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" pnpm exec playwright test",

+ 1 - 0
src/payload.config.ts

@@ -30,6 +30,7 @@ const trustedBrowserOrigins: string[] = [
   'http://127.0.0.1:3001',
   'https://hanoman.co.id',
   'https://www.hanoman.co.id',
+  'https://api.hanoman.co.id',
 ]
 
 /** URL publik backend (mis. https://api.hanoman.co.id). Kosongkan di lokal jika akses lewat localhost:3000. */

+ 20 - 0
src/scripts/push-db-schema.ts

@@ -0,0 +1,20 @@
+/**
+ * Satu kali: terapkan schema Payload ke Postgres (tabel users, media, …).
+ * Jalankan dengan NODE_ENV=development (lihat script `db:push` di package.json).
+ */
+import 'dotenv/config'
+import { getPayload } from 'payload'
+
+import config from '../payload.config'
+
+async function main() {
+  const payloadConfig = await config
+  const payload = await getPayload({ config: payloadConfig })
+  console.log('Database schema push finished.')
+  await payload.destroy()
+}
+
+main().catch((err) => {
+  console.error(err)
+  process.exit(1)
+})