Files
ims/docs/new-server-setup.md
T

345 lines
7.0 KiB
Markdown

# 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`)