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
|
||||
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/"
|
||||
|
||||
@@ -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