diff --git a/deploy.sh b/deploy.sh index a0bfa04..66e1766 100755 --- a/deploy.sh +++ b/deploy.sh @@ -1,7 +1,8 @@ #!/bin/bash set -e -VPS="root@64.176.82.100" +VPS="setia@ims.setia.com.my" +VPS_SSH_PORT=9321 REMOTE_DIR="/var/www/ims" PORT=3003 @@ -13,9 +14,9 @@ cp -r .next/static .next/standalone/.next/static cp -r public .next/standalone/public echo "==> Syncing to VPS..." -rsync -az --delete --exclude='.env' .next/standalone/ "$VPS:$REMOTE_DIR/" +rsync -az --delete --exclude='.env' -e "ssh -p ${VPS_SSH_PORT}" .next/standalone/ "$VPS:$REMOTE_DIR/" echo "==> Restarting service on VPS..." -ssh "$VPS" "systemctl restart ims" +ssh -p "${VPS_SSH_PORT}" "$VPS" "systemctl restart ims" -echo "==> Done. http://64.176.82.100/ims/" +echo "==> Done. https://ims.setia.com.my/" diff --git a/docs/new-server-setup.md b/docs/new-server-setup.md new file mode 100644 index 0000000..c7d366a --- /dev/null +++ b/docs/new-server-setup.md @@ -0,0 +1,344 @@ +# New Server Setup Runbook — ims.setia.com.my + +**Target:** Fresh Ubuntu 22.04 LTS server at `ims.setia.com.my` +**SSH port:** 9321 +**Purpose:** Host the IMS Next.js app with self-hosted PostgreSQL 16 (replacing Supabase cloud) + +Run every command manually over SSH unless stated otherwise. + +--- + +## A. Connect & Initial Hardening + +```bash +ssh -p 9321 setia@ims.setia.com.my +# Use the initial password provided by the server administrator. CHANGE IT IMMEDIATELY after first login. +``` + +Change the password on first login: + +```bash +passwd +``` + +Update the system: + +```bash +sudo apt-get update && sudo apt-get upgrade -y +``` + +--- + +## B. Install Dependencies + +### Node.js 20 (via NodeSource) + +```bash +curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - +sudo apt-get install -y nodejs +node -v # should print v20.x.x +``` + +### PostgreSQL 16 + pgvector + +```bash +sudo apt-get install -y postgresql-16 postgresql-16-pgvector +sudo systemctl enable postgresql +sudo systemctl start postgresql +``` + +### nginx + certbot + +```bash +sudo apt-get install -y nginx certbot python3-certbot-nginx +sudo systemctl enable nginx +sudo systemctl start nginx +``` + +### ufw firewall + +```bash +sudo apt-get install -y ufw +``` + +--- + +## C. Firewall Rules + +```bash +sudo ufw allow 9321/tcp # SSH (custom port — do this BEFORE enabling ufw) +sudo ufw allow 80/tcp +sudo ufw allow 443/tcp +sudo ufw deny 5432/tcp # Postgres: localhost only +sudo ufw enable +sudo ufw status +``` + +> **Warning:** Always allow port 9321 before enabling ufw, or you will lock yourself out. + +--- + +## D. PostgreSQL Setup + +Generate strong passwords first (run locally or in a separate shell): + +```bash +openssl rand -base64 32 # use output as STRONG_PASSWORD_1 (app_user) +openssl rand -base64 32 # use output as STRONG_PASSWORD_2 (app_admin) +``` + +Record both passwords — they go into `/var/www/ims/.env` in step I. + +Connect as the postgres superuser: + +```bash +sudo -u postgres psql +``` + +Run the following SQL: + +```sql +CREATE DATABASE ims; +\c ims + +CREATE EXTENSION IF NOT EXISTS vector; +CREATE EXTENSION IF NOT EXISTS pgcrypto; + +-- app_user: subject to RLS (mirrors Supabase anon/authenticated role) +CREATE ROLE app_user LOGIN PASSWORD ''; + +-- app_admin: bypasses RLS (mirrors Supabase service-role) +CREATE ROLE app_admin LOGIN PASSWORD '' BYPASSRLS; + +GRANT CONNECT ON DATABASE ims TO app_user, app_admin; +-- Table-level grants are applied after schema load in Phase 1 +``` + +Exit psql: + +```sql +\q +``` + +Verify extensions loaded: + +```bash +sudo -u postgres psql -d ims -c "SELECT extname FROM pg_extension WHERE extname IN ('vector','pgcrypto');" +# Expected: 2 rows +``` + +--- + +## E. nginx Configuration + +Create the site config: + +```bash +sudo nano /etc/nginx/sites-available/ims +``` + +Paste the following: + +```nginx +# /etc/nginx/sites-available/ims +server { + listen 80; + server_name ims.setia.com.my; + + location / { + proxy_pass http://127.0.0.1:3003/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + 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_cache_bypass $http_upgrade; + client_max_body_size 200M; + } +} +``` + +Enable the site and test: + +```bash +sudo ln -s /etc/nginx/sites-available/ims /etc/nginx/sites-enabled/ +sudo nginx -t && sudo systemctl reload nginx +``` + +--- + +## F. TLS (certbot) + +DNS must already point `ims.setia.com.my` to this server's public IP before running certbot. + +```bash +sudo certbot --nginx -d ims.setia.com.my +# Follow the interactive prompts. +# Certbot will auto-edit the nginx config to add HTTPS and an HTTP→HTTPS redirect. +sudo systemctl reload nginx +``` + +Auto-renewal is configured by certbot automatically. Verify: + +```bash +sudo certbot renew --dry-run +``` + +--- + +## G. App Directory + Evidence Storage + +```bash +sudo mkdir -p /var/www/ims +sudo mkdir -p /var/lib/ims/evidence +sudo chown -R setia:setia /var/www/ims /var/lib/ims +``` + +--- + +## H. systemd Service + +Create the service unit: + +```bash +sudo nano /etc/systemd/system/ims.service +``` + +Paste the following: + +```ini +[Unit] +Description=IMS Next.js App +After=network.target postgresql.service + +[Service] +Type=simple +User=setia +WorkingDirectory=/var/www/ims +ExecStart=/usr/bin/node .next/standalone/server.js +Restart=on-failure +RestartSec=5 +StandardOutput=journal +StandardError=journal +SyslogIdentifier=ims +EnvironmentFile=/var/www/ims/.env + +[Install] +WantedBy=multi-user.target +``` + +Enable and start: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable ims +sudo systemctl start ims +sudo systemctl status ims +``` + +> The service will fail to start until `.env` is populated (step I) and the app is deployed (Phase 9). This is expected. + +--- + +## I. .env File on Server + +Create the file — **never commit this to git**: + +```bash +nano /var/www/ims/.env +``` + +Template — fill ALL values before starting the service: + +```bash +# /var/www/ims/.env + +# Database (local PostgreSQL) +DATABASE_URL=postgres://app_user:@127.0.0.1:5432/ims +DATABASE_URL_ADMIN=postgres://app_admin:@127.0.0.1:5432/ims + +# Auth +JWT_SECRET=<32+ byte random — openssl rand -base64 32> + +# App +APP_URL=https://ims.setia.com.my +SITE_URL=https://ims.setia.com.my +CRON_SECRET= + +# Storage (local filesystem) +EVIDENCE_DIR=/var/lib/ims/evidence +EVIDENCE_URL_SECRET= + +# Email (Brevo) +BREVO_API_KEY= +BREVO_FROM_EMAIL=noreply@setia.com.my + +# AI (server-side only) +ANTHROPIC_API_KEY= +GOOGLE_AI_API_KEY= +``` + +Lock down permissions: + +```bash +chmod 600 /var/www/ims/.env +``` + +--- + +## J. Cron Jobs (Register After Phase 9 Deploy) + +Refer to `docs/vps-cron.md` (to be created in Phase 9) for application-level cron jobs (e.g. NADOPOD reminders, CAPA escalation). + +--- + +## K. Backup + +Create the backup directory: + +```bash +sudo mkdir -p /var/lib/ims/backups +sudo chown setia:setia /var/lib/ims/backups +``` + +Set up daily `pg_dump` backup (add after Phase 8): + +```bash +sudo nano /etc/cron.d/ims-backup +``` + +Paste: + +``` +0 2 * * * setia pg_dump ims > /var/lib/ims/backups/ims-$(date +\%Y\%m\%d).sql +``` + +> Backups land in `/var/lib/ims/backups/`. Consider adding off-server backup (S3, rclone) for disaster recovery. + +--- + +## L. Verify Installation + +Run these checks after completing all steps above: + +```bash +# PostgreSQL extensions +sudo -u postgres psql -d ims -c "SELECT extname FROM pg_extension WHERE extname IN ('vector','pgcrypto');" +# Expected: 2 rows + +# nginx + TLS (before app deploy — 502 is expected here) +curl -I https://ims.setia.com.my +# Expected: HTTP/2 502 (before Phase 9 deploy) or HTTP/2 200 (after deploy) + +# App service (after deploy) +sudo systemctl status ims +``` + +--- + +## Next Steps + +- **Phase 1:** Load the database schema (migrations + RLS policies + table grants) +- **Phase 9:** Deploy the Next.js app, register cron jobs (`docs/vps-cron.md`)