Panduan ini menjelaskan pola deployment di VPS: PostgreSQL + Payload (Next.js backend) tetap di Docker, sementara frontend (aplikasi Next.js terpisah) di-build, dijalankan dengan PM2, dan dihadapkan ke internet lewat Nginx (HTTPS).
Catatan: Repositori frontend diasumsikan terpisah dari repositori backend (
hanoman-website-be). Sesuaikan path dan nama folder dengan proyek Anda.
Internet
│
▼
┌──────────────────┐
│ Nginx :80 / :443│ (SSL, reverse proxy)
└────────┬─────────┘
│
┌────┴────┐
│ │
▼ ▼
┌────────┐ ┌─────────────────────────────┐
│ PM2 │ │ Docker Compose │
│ Next │ │ • postgres:5432 │
│ FE │ │ • payload → host :3000 │
│ :3001 │ │ • volume ./media → /app/media│
└────────┘ └─────────────────────────────┘
http://127.0.0.1:3001 (hanya localhost; Nginx yang memproksi ke domain publik).payload memetakan 3000:3000 — di host, API Payload tersedia di http://127.0.0.1:3000 (REST di /api/...).apt).docker compose (lihat README repositori backend).www.example.com → frontendapi.example.com → backend Payload (disarankan subdomain terpisah agar jelas dan tidak bentrok route /api di Next frontend).Di mesin VPS, di direktori repositori backend:
cd /path/to/hanoman-website-be
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.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):
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:
curl -sS -o /dev/null -w "%{http_code}\n" http://127.0.0.1:3000/api/
Jika backend sehat, Anda akan mendapat respons HTTP (bukan koneksi ditolak).
Samakan major Node dengan yang dipakai proyek (cek engines di package.json frontend). Contoh memakai Node 22 via nvm (disarankan):
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
npm install -g pm2
# opsional: startup otomatis setelah reboot
pm2 startup
# jalankan perintah yang di-print oleh pm2 startup (sudo env ...)
sudo mkdir -p /var/www
sudo chown $USER:$USER /var/www
cd /var/www
git clone <URL-REPO-FRONTEND> hanoman-frontend
cd hanoman-frontend
Pasang paket (contoh pnpm; sesuaikan jika pakai yarn/npm):
corepack enable
corepack prepare pnpm@latest --activate
pnpm install --frozen-lockfile
Buat file .env.production (atau .env sesuai konvensi Next) di root frontend. Minimal:
# URL publik backend Payload (tanpa slash di akhir)
# Gunakan https://api.example.com jika Nginx sudah memproksi ke :3000
NEXT_PUBLIC_PAYLOAD_API_URL=https://api.example.com
Sesuaikan dengan cara frontend Anda menyusun URL:
NEXT_PUBLIC_PAYLOAD_API_URL + '/api/posts' → isi tanpa /api di akhir.NEXT_PUBLIC_PAYLOAD_API_URL + '/posts' → isi dengan https://api.example.com/api.Build-time: NEXT_PUBLIC_* di-embed saat next build; setelah mengubahnya, harus build ulang.
pnpm run build
ecosystem.config.cjsDi root frontend (mis. /var/www/hanoman-frontend), buat ecosystem.config.cjs:
module.exports = {
apps: [
{
name: 'hanoman-fe',
cwd: '/var/www/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',
},
},
],
}
Port 3001 dipakai agar tidak bentrok dengan backend di 3000. Sesuaikan jika Anda memakai port lain.
Jalankan:
cd /var/www/hanoman-frontend
pm2 start ecosystem.config.cjs
pm2 save
Uji lokal:
curl -sS -o /dev/null -w "%{http_code}\n" http://127.0.0.1:3001/
sudo apt update
sudo apt install -y nginx
a) Backend — api.example.com → 127.0.0.1:3000
Buat /etc/nginx/sites-available/hanoman-api:
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;
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;
}
}
b) Frontend — www.example.com → 127.0.0.1:3001
Buat /etc/nginx/sites-available/hanoman-www:
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;
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;
}
}
Aktifkan situs:
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
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d api.example.com -d www.example.com -d example.com
Certbot akan mengubah blok listen 443 ssl dan jadwal renew otomatis.
Setelah HTTPS aktif, pastikan NEXT_PUBLIC_PAYLOAD_API_URL memakai https://api.example.com lalu build ulang frontend dan restart PM2:
cd /var/www/hanoman-frontend
pnpm run build
pm2 restart hanoman-fe
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status
Port 3000 dan 3001 tidak perlu dibuka ke publik jika hanya diakses lewat Nginx di localhost.
Konfigurasi aktual ada di src/payload.config.ts (bukan cors: '*').
PAYLOAD_SERVER_URL (.env backend)Set ke URL publik API, tanpa slash di akhir, mis.:
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.
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.
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).
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.
cd /var/www/hanoman-frontend
git pull
pnpm install --frozen-lockfile
# edit .env.production jika perlu
pnpm run build
pm2 restart hanoman-fe
Backend + DB:
cd /path/to/hanoman-website-be
git pull
docker compose --env-file .env -f docker-compose.yml up -d --build
| Gejala | Hal yang dicek |
|---|---|
| 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.* |
| Layanan | Port (localhost) | Publik |
|---|---|---|
| Payload (Docker) | 3000 |
Via Nginx api.example.com |
| Frontend (PM2) | 3001 |
Via Nginx www.example.com |
| PostgreSQL (Docker) | 5432 (hanya jika perlu; jangan expose ke internet tanpa kebutuhan) |
— |
.env lengkap (PAYLOAD_SECRET, DATABASE_URL dengan host postgres, PAYLOAD_SERVER_URL, FRONTEND_ORIGIN untuk contact).trustedBrowserOrigins + image sudah di-build ulang.db-push sudah dijalankan jika DB kosong..env.production + pnpm build setelah setiap ubah NEXT_PUBLIC_*.client_max_body_size cukup untuk upload admin.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.