Deployment & Infrastructure¶
Production deployment on PM2 + host-level NGINX
Last Updated: 2026-03-09 (v4.3.0)
Infrastructure Overview¶
VitaraVox runs on two cloud instances: an OCI ARM64 server hosting the application, and an AWS instance running the OSCAR EMR development environment.
Production Server (OCI Toronto)¶
| Component | Specification |
|---|---|
| Provider | Oracle Cloud Infrastructure (OCI) |
| Region | ca-toronto-1 |
| Shape | VM.Standard.A1.Flex (ARM64) |
| OCPUs | 2 |
| Memory | 24 GB |
| Storage | 200 GB block volume |
| OS | Ubuntu (Linux 6.x, aarch64) |
| Cost | Free tier (Always Free eligible) |
OSCAR Dev Instance (AWS Montreal)¶
| Component | Specification |
|---|---|
| Provider | AWS |
| Region | ca-central-1 (Montreal) |
| Instance | t3a.medium |
| vCPU | 2 |
| Memory | 4 GB |
| Storage | 30 GB gp3 |
| IP | 15.222.50.48 |
| Cost | ~$30/month |
Production clinics
In production, each clinic connects its own OSCAR instance. The AWS instance is for development and testing only. The SOAP adapter connects directly from the OCI server to the clinic OSCAR -- no sidecar or bridge is deployed on the clinic side.
Architecture Diagram¶
┌─────────────────────────┐
│ Vapi Voice Platform │
│ (squad of 9 agents) │
└───────────┬─────────────┘
│ HTTPS webhooks
▼
┌───────────────────────────────────────────────────────────┐
│ OCI Toronto — VM.Standard.A1.Flex (ARM64) │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ NGINX (host-level) │ │
│ │ :80 → redirect to :443 │ │
│ │ :443 → proxy_pass localhost:3002 │ │
│ │ SSL: Let's Encrypt │ │
│ └──────────────────────┬───────────────────────────┘ │
│ │ │
│ ┌──────────────────────▼───────────────────────────┐ │
│ │ PM2 → vitara-admin-api │ │
│ │ Node.js 18 + TypeScript (tsx) │ │
│ │ Port 3002 │ │
│ └──────────────────────┬───────────────────────────┘ │
│ │ │
│ ┌──────────────────────▼───────────────────────────┐ │
│ │ PostgreSQL 16 (host-level) │ │
│ │ localhost:5432 │ │
│ │ Database: vitara_platform │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ NGINX (host-level) — vitdocs.vitaravox.ca │ │
│ │ Static MkDocs site │ │
│ └──────────────────────────────────────────────────┘ │
└───────────────────┬───────────────────────────────────────┘
│ SOAP / OAuth REST
▼
┌───────────────────────────────────────────────────────────┐
│ AWS Montreal — OSCAR EMR (t3a.medium) │
│ 15.222.50.48 │
│ CXF SOAP endpoint + OAuth REST API │
└───────────────────────────────────────────────────────────┘
No Docker for the app server
The application server does not use Docker. The PM2 process, PostgreSQL, and NGINX all run directly on the host OS. The old Docker-based documentation is obsolete.
Prerequisites¶
Ensure the following are installed on the OCI server before deploying.
Required Software¶
| Software | Version | Purpose |
|---|---|---|
| Node.js | 18.x LTS | Application runtime |
| npm | 9.x | Package management |
| TypeScript | 5.3+ | Compile-time type checking |
| tsx | 4.x | TypeScript execution (used by PM2) |
| PM2 | Latest | Process manager |
| PostgreSQL | 16 | Database |
| NGINX | Latest | Reverse proxy + SSL termination |
| Certbot | Latest | SSL certificate management |
| Git | Latest | Version control |
Install Commands¶
# Node.js 18 (via NodeSource)
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs
# PM2 (global)
sudo npm install -g pm2
# PostgreSQL 16
sudo apt-get install -y postgresql-16
# NGINX
sudo apt-get install -y nginx
# Certbot
sudo apt-get install -y certbot python3-certbot-nginx
Environment Configuration¶
Location¶
The environment file lives at:
Required Variables¶
# ===========================================
# VitaraPlatform Admin Dashboard - Environment
# ===========================================
# Server
PORT=3002
NODE_ENV=production
CORS_ORIGIN=https://dev.vitaravox.ca
# JWT Authentication
JWT_SECRET=<generate-secure-random-string>
JWT_REFRESH_SECRET=<generate-another-secure-random-string>
JWT_EXPIRES_IN=1h
JWT_REFRESH_EXPIRES_IN=7d
# PostgreSQL Database
DATABASE_URL=postgresql://vitara:<password>@localhost:5432/vitara_platform
# Vapi AI Voice Platform
VAPI_API_KEY=<vapi-api-key>
VAPI_BASE_URL=https://api.vapi.ai
VAPI_WEBHOOK_SECRET=<vapi-webhook-secret>
# OSCAR REST Bridge Connection (dev/fallback only)
OSCAR_BRIDGE_URL=http://15.222.50.48:3000/api/v1
OSCAR_BRIDGE_API_KEY=<oscar-bridge-api-key>
# Encryption
ENCRYPTION_KEY=<generate-32-byte-hex-key>
Never commit .env to version control
The .env file contains secrets (JWT keys, database credentials, API keys). It is gitignored. Copy from .env.example and fill in production values.
Generate Secrets¶
# JWT secrets (64-char base64)
openssl rand -base64 64
# Encryption key (32-byte hex)
openssl rand -hex 32
# Database password
openssl rand -base64 24
Optional Variables¶
# Debug mode (auto-expires after 4 hours, DO NOT leave on unattended)
# VITARA_DEBUG=false
# Log level override (trace, debug, info, warn, error, fatal)
# LOG_LEVEL=info
Middleware Stack¶
The Express server applies middleware in the following order (defined in src/index.ts):
Source: index.ts:33-79
| # | Middleware | Purpose |
|---|---|---|
| 1 | helmet() |
Security headers (CSP, HSTS, X-Frame-Options) |
| 2 | requestLogger (pino-http) |
Request logging with x-request-id correlation |
| 3 | cors({ origin: CORS_ORIGIN }) |
CORS with configured origin |
| 4 | express.json({ limit: '1mb' }) |
JSON body parser |
| 5 | auditMiddleware |
Global — logs POST/PUT/DELETE to AuditLog table |
| 6 | Rate limiters (per-route) | 5/min auth, 100/min API, 300/min webhooks |
| 7 | vapiWebhookAuth |
HMAC-SHA256 / API key / Bearer token on /api/vapi |
| 8 | authMiddleware |
JWT verification on /api/admin/* and /api/clinic/* |
| 9 | requireRole() |
RBAC enforcement (vitara_admin / clinic_manager) |
| 10 | requireClinicAccess |
Tenant isolation (clinic managers see own clinic only) |
EXPRESS MIDDLEWARE STACK (source: index.ts:33-79)
Incoming Request
│
▼
┌──────────────────┐
│ helmet() │ Security headers (CSP, HSTS, X-Frame)
├──────────────────┤
│ requestLogger │ pino-http: request logging + x-request-id
├──────────────────┤
│ cors() │ CORS policy enforcement
├──────────────────┤
│ express.json() │ Body parser (1mb limit)
├──────────────────┤
│ auditMiddleware │ Global: logs POST/PUT/DELETE to AuditLog
└────────┬─────────┘
│
Route matching
┌────┴────────────────────────────┐
│ │
▼ ▼
/api/vapi/* /api/clinic/* & /api/admin/*
│ │
▼ ▼
┌──────────────┐ ┌──────────────────┐
│webhookLimiter │ 300/min │ apiLimiter │ 100/min
├──────────────┤ ├──────────────────┤
│vapiWebhookAuth│ │ authMiddleware │ JWT validation
│(HMAC/Bearer/ │ ├──────────────────┤
│ API Key) │ │ requireRole() │ Role check
└──────┬───────┘ ├──────────────────┤
│ │requireClinicAccess│ Tenant check
│ └────────┬─────────┘
│ │
└───────────┬───────────────────┘
▼
Route Handler
│
▼
Response
JWT Authentication Flow¶
1. POST /api/auth/login → bcrypt compare → issue access (1h) + refresh (7d) tokens
2. Client includes Authorization: Bearer {access_token} on every request
3. authMiddleware verifies HS256 signature, extracts userId/role
4. On 401, client calls POST /api/auth/refresh with refresh token
5. Server issues new access token (refresh token unchanged)
No Token Revocation
JWT tokens cannot be revoked server-side — there is no blacklist. Tokens remain valid until expiry (1h access, 7d refresh). This is acceptable for the current single-instance deployment but needs a Redis-backed blacklist for HA or multi-instance setups.
Database Setup¶
Create the Database¶
# Switch to postgres user
sudo -u postgres psql
# Create role and database
CREATE USER vitara WITH PASSWORD '<secure-password>';
CREATE DATABASE vitara_platform OWNER vitara;
GRANT ALL PRIVILEGES ON DATABASE vitara_platform TO vitara;
\q
Run Prisma Migrations¶
cd /home/ubuntu/vitara-platform/admin-dashboard/server
# Generate the Prisma client
npx prisma generate
# Deploy migrations to production (non-interactive, does NOT reset data)
npx prisma migrate deploy
Migration commands
npx prisma migrate deploy-- Apply pending migrations (production-safe, non-interactive).npx prisma migrate dev-- Create and apply migrations during development.npx prisma migrate reset --force-- Drop and recreate the database. Destructive. Dev only.npx prisma studio-- Launch the visual database browser on port 5555.
Seed Data (Optional)¶
Deployment Procedure¶
PM2 Process Configuration¶
The PM2 ecosystem file is at:
module.exports = {
apps: [{
name: 'vitara-admin-api',
script: 'npx',
args: 'tsx src/index.ts',
cwd: '/home/ubuntu/vitara-platform/admin-dashboard/server',
env: {
NODE_ENV: 'production',
PORT: 3002,
CORS_ORIGIN: 'https://dev.vitaravox.ca'
},
watch: false,
max_memory_restart: '500M'
}]
};
tsx, not compiled JS
The PM2 process runs npx tsx src/index.ts directly rather than compiling to JavaScript first. The tsx loader handles TypeScript transpilation at runtime. For manual TypeScript verification, you can still run npx tsc --noEmit.
First-Time Deployment¶
cd /home/ubuntu/vitara-platform/admin-dashboard/server
# Install dependencies
npm install
# Generate Prisma client
npx prisma generate
# Apply database migrations
npx prisma migrate deploy
# Start with PM2
pm2 start ecosystem.config.cjs
# Save the PM2 process list (survives reboot)
pm2 save
# Enable PM2 startup on boot
pm2 startup
Standard Deployment (Code Updates)¶
cd /home/ubuntu/vitara-platform/admin-dashboard/server
# Pull latest code
git pull origin main
# Install any new dependencies
npm install
# Apply pending database migrations
npx prisma migrate deploy
# Generate Prisma client (if schema changed)
npx prisma generate
# Restart the PM2 process
pm2 restart vitara-admin-api
Adapter Warm-Up on Startup¶
PM2 process startup triggers EMR adapter warm-up for all preferRest clinics via an IIFE in index.ts:
- Queries database for clinics with
preferRest: true - Creates adapter instances and awaits
warmUp()for each - Prevents cold TLS handshake (~6s to Kai Cloudflare) from exceeding the 4s circuit breaker on first production call
- Non-blocking: warm-up failures are logged but don't prevent server startup
PM2 STARTUP SEQUENCE
pm2 restart vitara-admin-api
│
▼
┌────────────────────────────┐
│ index.ts loads │
│ ├─ Express app setup │
│ ├─ Middleware stack │
│ ├─ Route registration │
│ └─ server.listen(3002) │
└────────────┬───────────────┘
│
▼
┌────────────────────────────┐
│ IIFE: Adapter Warm-Up │
│ ├─ Query DB for clinics │
│ │ with preferRest=true │
│ ├─ For each clinic: │
│ │ ├─ Create adapter │
│ │ ├─ await warmUp() │
│ │ │ (TLS handshake │
│ │ │ to Kai CF ~6s) │
│ │ └─ Cache adapter │
│ └─ Log: "X adapters warm" │
│ │
│ ⚠ Non-blocking: │
│ Failures logged, server │
│ still accepts traffic │
└────────────────────────────┘
Graceful Shutdown¶
Source: index.ts:169-188
When PM2 sends SIGTERM or SIGINT, the server drains in-flight requests before exiting. This is critical for update_appointment, which books a new slot then cancels the old one — interruption between those steps creates duplicate appointments.
SIGTERM/SIGINT received
│
▼
server.close() ← stop accepting new connections
│
├── Wait for in-flight requests to complete
│
├── All drained → exit(0)
│
└── 10s timeout → forced exit(1)
- Drain timeout: 10 seconds (hardcoded in
index.ts:182) - The 10s timer is
.unref()'d so it doesn't prevent shutdown if all connections drain first - PM2 sends SIGTERM first; if the process doesn't exit, PM2 sends SIGKILL after its own timeout
Quick Build & Restart (No Schema Changes)¶
When deploying code changes that do not affect the Prisma schema or dependencies:
TypeScript compilation
Running npx tsc is a type-check step (it compiles to dist/). The PM2 process itself uses tsx to run TypeScript directly, so the tsc step serves as a build-time validation that there are no type errors before restarting.
NGINX Configuration¶
NGINX runs at the host level (not in a Docker container). Configuration files are in /etc/nginx/.
API Proxy (api.vitaravox.ca)¶
Create or edit /etc/nginx/sites-available/api.vitaravox.ca:
# HTTPS Server
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name api.vitaravox.ca;
# SSL (managed by Certbot)
ssl_certificate /etc/letsencrypt/live/vitaravox.ca/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/vitaravox.ca/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
# Security headers
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Request size limit
client_max_body_size 1M;
# Proxy to PM2 process on port 3002
location / {
proxy_pass http://127.0.0.1:3002;
proxy_http_version 1.1;
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_set_header Connection "";
proxy_connect_timeout 5s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
}
# Health check (no access log)
location /health {
proxy_pass http://127.0.0.1:3002/health;
proxy_http_version 1.1;
proxy_set_header Connection "";
access_log off;
}
}
# HTTP redirect to HTTPS
server {
listen 80;
listen [::]:80;
server_name api.vitaravox.ca;
location /health {
proxy_pass http://127.0.0.1:3002/health;
access_log off;
}
location / {
return 301 https://$host$request_uri;
}
}
Documentation Site (vitdocs.vitaravox.ca)¶
The docs site is served as static files. Config at /etc/nginx/sites-enabled/vitdocs.vitaravox.ca:
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name vitdocs.vitaravox.ca;
ssl_certificate /etc/letsencrypt/live/vitaravox.ca/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/vitaravox.ca/privkey.pem;
root /home/ubuntu/vitaravox-docs/site;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
server {
listen 80;
server_name vitdocs.vitaravox.ca;
return 301 https://$host$request_uri;
}
Enable Sites and Reload¶
# Symlink to sites-enabled (if not already done)
sudo ln -s /etc/nginx/sites-available/api.vitaravox.ca /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/vitdocs.vitaravox.ca /etc/nginx/sites-enabled/
# Test configuration
sudo nginx -t
# Reload
sudo nginx -s reload
SSL Setup (Let's Encrypt)¶
Initial Certificate¶
# Stop NGINX temporarily if port 80 is in use
sudo systemctl stop nginx
# Obtain certificate (standalone mode)
sudo certbot certonly --standalone \
-d vitaravox.ca \
-d "*.vitaravox.ca" \
--email admin@vitaravox.com \
--agree-tos \
--non-interactive \
--preferred-challenges dns
# Restart NGINX
sudo systemctl start nginx
Wildcard certificates
Wildcard certificates (*.vitaravox.ca) require DNS challenge verification. Certbot will prompt for a DNS TXT record. For automated renewal, configure a DNS plugin (e.g., Cloudflare).
Automatic Renewal¶
# Certbot installs a systemd timer by default. Verify it:
sudo systemctl status certbot.timer
# Or add a cron job:
# 0 3 * * * certbot renew --quiet --post-hook "sudo nginx -s reload"
Verify Certificate¶
# Check expiry date
sudo certbot certificates
# Or via OpenSSL
openssl s_client -connect api.vitaravox.ca:443 -servername api.vitaravox.ca < /dev/null 2>/dev/null | openssl x509 -noout -dates
Vapi GitOps Deployment¶
The Vapi voice agent configuration (assistants, tools, squad) is managed as code via the GitOps tool.
Location¶
Environment¶
The GitOps tool requires a .env.dev file with the Vapi token:
Environment variable name
The Vapi GitOps tool uses VAPI_TOKEN (not VAPI_API_KEY). Using the wrong name will cause silent authentication failures.
Push Configuration to Vapi¶
cd /home/ubuntu/vitara-platform/vapi-gitops
# Push all assistant/tool/squad configs to Vapi dev environment
npm run push:dev
# Force push (overwrites remote even if unchanged)
npm run push:dev:force
Pull Configuration from Vapi¶
cd /home/ubuntu/vitara-platform/vapi-gitops
# Pull current state from Vapi into local YAML files
npm run pull:dev
Available Commands¶
| Command | Description |
|---|---|
npm run push:dev |
Push local config to Vapi (dev) |
npm run push:prod |
Push local config to Vapi (prod) |
npm run pull:dev |
Pull remote config to local (dev) |
npm run apply:dev |
Apply changes (dev) |
npm run call:dev |
Make a test call (dev) |
npm run cleanup:dev |
Remove orphaned resources (dev) |
Documentation Site Deployment¶
The documentation is built with MkDocs Material and served as static HTML.
Source and Output¶
| Item | Path |
|---|---|
| Source files | /home/ubuntu/vitaravox-docs/docs/ |
| MkDocs config | /home/ubuntu/vitaravox-docs/mkdocs.yml |
| Built site | /home/ubuntu/vitaravox-docs/site/ |
| Public URL | https://vitdocs.vitaravox.ca |
Build and Deploy¶
# Activate the MkDocs virtual environment
source /tmp/mkdocs-env/bin/activate
# Build the site to a temp directory, then copy to the serve path
mkdocs build --site-dir /tmp/vitdocs-site
cp -r /tmp/vitdocs-site /home/ubuntu/vitaravox-docs/site
# Reload NGINX to pick up any config changes
sudo nginx -s reload
One-liner
Edit Docs¶
Only edit files under /home/ubuntu/vitaravox-docs/docs/. Never edit files in the site/ directory -- they are overwritten on every build.
Monitoring¶
PM2 Process Status¶
# Quick status table
pm2 status
# Expected output:
# ┌────┬──────────────────┬────────┬──────┬───────┬──────────┐
# │ id │ name │ mode │ pid │ status│ mem │
# ├────┼──────────────────┼────────┼──────┼───────┼──────────┤
# │ 0 │ vitara-admin-api │ fork │ 1234 │ online│ ~76mb │
# └────┴──────────────────┴────────┴──────┴───────┴──────────┘
Application Logs¶
# Stream real-time logs
pm2 logs vitara-admin-api
# Last 200 lines
pm2 logs vitara-admin-api --lines 200
# Error logs only
pm2 logs vitara-admin-api --err
Health Check¶
# Local health check
curl -s http://localhost:3002/health | jq .
# External health check
curl -s https://api.vitaravox.ca/health | jq .
Database Health¶
# Check PostgreSQL is accepting connections
pg_isready -h localhost -p 5432
# Check database size
psql -U vitara -d vitara_platform -c "SELECT pg_size_pretty(pg_database_size('vitara_platform'));"
NGINX Status¶
# Test configuration
sudo nginx -t
# Check process
systemctl status nginx
# Access logs
sudo tail -f /var/log/nginx/access.log
# Error logs
sudo tail -f /var/log/nginx/error.log
PM2 Monitoring Dashboard¶
Backup Procedures¶
Database Backup¶
# Full backup (plain SQL)
pg_dump -U vitara vitara_platform > backup_$(date +%Y%m%d).sql
# Compressed backup
pg_dump -U vitara vitara_platform | gzip > backup_$(date +%Y%m%d).sql.gz
# Custom format (supports parallel restore)
pg_dump -U vitara -Fc vitara_platform > backup_$(date +%Y%m%d).dump
No Docker exec
PostgreSQL runs on the host, not in a container. Use pg_dump directly -- do not use docker exec.
Automated Daily Backup¶
Add to crontab (crontab -e):
# Daily database backup at 2 AM, keep 30 days
0 2 * * * pg_dump -U vitara vitara_platform | gzip > /home/ubuntu/backups/vitara_$(date +\%Y\%m\%d).sql.gz
0 3 * * * find /home/ubuntu/backups -name "vitara_*.sql.gz" -mtime +30 -delete
Restore from Backup¶
# Plain SQL restore
psql -U vitara -d vitara_platform < backup_20260216.sql
# Compressed restore
gunzip -c backup_20260216.sql.gz | psql -U vitara -d vitara_platform
# Custom format restore
pg_restore -U vitara -d vitara_platform backup_20260216.dump
Vapi Configuration Backup¶
The Vapi GitOps config is version-controlled in Git. Additionally, manual backups exist at:
To create a fresh backup:
cd /home/ubuntu/vitara-platform/vapi-gitops
npm run pull:dev
# Configs are now saved as YAML in the gitops directory
Rollback Procedure¶
Quick Rollback (Code Only)¶
cd /home/ubuntu/vitara-platform
# See recent commits
git log --oneline -10
# Checkout previous version
git checkout HEAD~1
# Restart the PM2 process
cd admin-dashboard/server
pm2 restart vitara-admin-api
Rollback to a Specific Commit¶
cd /home/ubuntu/vitara-platform
# Checkout specific commit
git checkout <commit-sha>
# Install dependencies (in case they changed)
cd admin-dashboard/server
npm install
# Regenerate Prisma client
npx prisma generate
# Restart
pm2 restart vitara-admin-api
Rollback Database Migrations¶
Destructive operation
Rolling back migrations may cause data loss. Always take a backup first.
# Restore from a pre-migration backup
pg_dump -U vitara vitara_platform > pre_rollback_backup.sql
psql -U vitara -d vitara_platform < backup_before_migration.sql
Rollback Vapi Configuration¶
cd /home/ubuntu/vitara-platform/vapi-gitops
# Revert to previous Git commit
git checkout HEAD~1
# Push the old config back to Vapi
npm run push:dev:force
Service URLs¶
| Service | URL | Notes |
|---|---|---|
| API (HTTPS) | https://api.vitaravox.ca |
Production API endpoint |
| API Health | https://api.vitaravox.ca/health |
Health check |
| Vapi Webhook | https://api.vitaravox.ca/api/vapi |
Vapi webhook receiver |
| Legacy Webhook | https://api.vitaravox.ca/vapi-webhook |
Legacy webhook path |
| Admin API | https://api.vitaravox.ca/api/ |
Admin dashboard API |
| Documentation | https://vitdocs.vitaravox.ca |
MkDocs site |
| OSCAR Bridge | http://15.222.50.48:3000/api/v1 |
Dev EMR (REST bridge) |
| Vapi Phone | +1 236-305-7446 |
v3.0 voice agent |
Firewall Configuration¶
# Allow SSH
sudo ufw allow 22/tcp
# Allow HTTP (for ACME/redirect)
sudo ufw allow 80/tcp
# Allow HTTPS
sudo ufw allow 443/tcp
# Enable firewall
sudo ufw enable
# Verify
sudo ufw status
OCI Security Lists
In addition to ufw, ensure the OCI VCN Security List allows ingress on ports 22, 80, and 443. OCI security lists operate independently of the OS firewall.
Troubleshooting¶
PM2 Process Won't Start¶
# Check logs for errors
pm2 logs vitara-admin-api --lines 50
# Common causes:
# - Missing .env file
# - DATABASE_URL incorrect or PostgreSQL not running
# - Port 3002 already in use
# - npm dependencies not installed
# Verify port availability
ss -tlnp | grep 3002
# Restart with full log output
pm2 delete vitara-admin-api
pm2 start ecosystem.config.cjs
Database Connection Failed¶
# Check PostgreSQL is running
systemctl status postgresql
# Test connection
psql -U vitara -d vitara_platform -c "SELECT 1;"
# Check pg_hba.conf allows local connections
sudo cat /etc/postgresql/16/main/pg_hba.conf | grep vitara
OSCAR SOAP Connection Failed¶
# Test SOAP endpoint reachability
curl -s http://15.222.50.48:8080/oscar/ws/services/DemographicService?wsdl | head -5
# Check server logs for SOAP errors
pm2 logs vitara-admin-api --lines 100 | grep -i soap
# Note: SOAP cold-start WSDL fetch can take 3-4s.
# Circuit breaker timeout is set to 4s (under Vapi's 5s tool timeout).
SSL Certificate Issues¶
# Check certificate expiry
sudo certbot certificates
# Force renewal
sudo certbot renew --force-renewal
# Reload NGINX after renewal
sudo nginx -s reload
High Memory / Restart Loop¶
# Check PM2 restart count (the ↺ column)
pm2 status
# If max_memory_restart (500M) is being hit:
pm2 logs vitara-admin-api --err --lines 200
# Check system memory
free -h
Production Checklist¶
Use this checklist when deploying to a new environment or onboarding a new developer.
- [ ] OCI instance provisioned (ARM64, 2 OCPU, 24GB RAM)
- [ ] Node.js 18, npm, PM2 installed
- [ ] PostgreSQL 16 installed and running
- [ ] Database and user created (
vitara_platform/vitara) - [ ] NGINX installed and configured
- [ ] SSL certificate obtained via Let's Encrypt
- [ ]
.envfile created with all required variables - [ ]
npm installcompleted inadmin-dashboard/server/ - [ ] Prisma client generated (
npx prisma generate) - [ ] Migrations applied (
npx prisma migrate deploy) - [ ] PM2 process started and saved (
pm2 start && pm2 save) - [ ] PM2 startup hook enabled (
pm2 startup) - [ ] Health check passing (
curl https://api.vitaravox.ca/health) - [ ] Vapi webhook URL configured in Vapi dashboard
- [ ] Vapi GitOps
.env.devconfigured withVAPI_TOKEN - [ ] Firewall rules applied (ufw + OCI Security List)
- [ ] Automated database backups scheduled (cron)
- [ ] SSL auto-renewal verified (
certbot renew --dry-run) - [ ] Documentation site built and accessible
Key File Locations¶
| File | Path |
|---|---|
| Application source | /home/ubuntu/vitara-platform/admin-dashboard/server/src/ |
| PM2 ecosystem config | /home/ubuntu/vitara-platform/admin-dashboard/server/ecosystem.config.cjs |
| Environment variables | /home/ubuntu/vitara-platform/admin-dashboard/server/.env |
| Prisma schema | /home/ubuntu/vitara-platform/admin-dashboard/server/prisma/schema.prisma |
| Vapi GitOps config | /home/ubuntu/vitara-platform/vapi-gitops/ |
| NGINX configs | /etc/nginx/sites-enabled/ |
| SSL certificates | /etc/letsencrypt/live/vitaravox.ca/ |
| Docs source | /home/ubuntu/vitaravox-docs/docs/ |
| Docs built site | /home/ubuntu/vitaravox-docs/site/ |
| MkDocs config | /home/ubuntu/vitaravox-docs/mkdocs.yml |
| Database backups | /home/ubuntu/backups/ |
| Vapi backups | /home/ubuntu/vitara-platform/backups/vapi-20260210/ |