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