feat(ops): add new server runbook and update deploy.sh for ims.setia.com.my
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
VPS="root@64.176.82.100"
|
VPS="setia@ims.setia.com.my"
|
||||||
|
VPS_SSH_PORT=9321
|
||||||
REMOTE_DIR="/var/www/ims"
|
REMOTE_DIR="/var/www/ims"
|
||||||
PORT=3003
|
PORT=3003
|
||||||
|
|
||||||
@@ -13,9 +14,9 @@ cp -r .next/static .next/standalone/.next/static
|
|||||||
cp -r public .next/standalone/public
|
cp -r public .next/standalone/public
|
||||||
|
|
||||||
echo "==> Syncing to VPS..."
|
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..."
|
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/"
|
||||||
|
|||||||
@@ -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 '<STRONG_PASSWORD_1>';
|
||||||
|
|
||||||
|
-- app_admin: bypasses RLS (mirrors Supabase service-role)
|
||||||
|
CREATE ROLE app_admin LOGIN PASSWORD '<STRONG_PASSWORD_2>' 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:<STRONG_PASSWORD_1>@127.0.0.1:5432/ims
|
||||||
|
DATABASE_URL_ADMIN=postgres://app_admin:<STRONG_PASSWORD_2>@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=<random — openssl rand -base64 24>
|
||||||
|
|
||||||
|
# Storage (local filesystem)
|
||||||
|
EVIDENCE_DIR=/var/lib/ims/evidence
|
||||||
|
EVIDENCE_URL_SECRET=<random HMAC key — openssl rand -base64 32>
|
||||||
|
|
||||||
|
# Email (Brevo)
|
||||||
|
BREVO_API_KEY=<from Brevo dashboard>
|
||||||
|
BREVO_FROM_EMAIL=noreply@setia.com.my
|
||||||
|
|
||||||
|
# AI (server-side only)
|
||||||
|
ANTHROPIC_API_KEY=<from Anthropic>
|
||||||
|
GOOGLE_AI_API_KEY=<from Google AI Studio>
|
||||||
|
```
|
||||||
|
|
||||||
|
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`)
|
||||||
Reference in New Issue
Block a user