7.0 KiB
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
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:
passwd
Update the system:
sudo apt-get update && sudo apt-get upgrade -y
B. Install Dependencies
Node.js 20 (via NodeSource)
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
sudo apt-get install -y postgresql-16 postgresql-16-pgvector
sudo systemctl enable postgresql
sudo systemctl start postgresql
nginx + certbot
sudo apt-get install -y nginx certbot python3-certbot-nginx
sudo systemctl enable nginx
sudo systemctl start nginx
ufw firewall
sudo apt-get install -y ufw
C. Firewall Rules
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):
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:
sudo -u postgres psql
Run the following 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:
\q
Verify extensions loaded:
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:
sudo nano /etc/nginx/sites-available/ims
Paste the following:
# /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:
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.
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:
sudo certbot renew --dry-run
G. App Directory + Evidence Storage
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:
sudo nano /etc/systemd/system/ims.service
Paste the following:
[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:
sudo systemctl daemon-reload
sudo systemctl enable ims
sudo systemctl start ims
sudo systemctl status ims
The service will fail to start until
.envis 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:
nano /var/www/ims/.env
Template — fill ALL values before starting the service:
# /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:
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:
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):
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:
# 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)