Deployment Guide — Lofty Golden Oil ERP
Version: 2.0
Last Updated: July 2026
Environment: Production (https://loftygoldenoil.com) / Staging (http://170.9.14.60:8443)
Table of Contents
- Prerequisites
- CI/CD Pipeline Overview
- How the Pipeline Works
- Branch Strategy
- Deploying to Production
- Deploying to Staging
- Manual Deployment
- PM2 Management
- Nginx Management
- SSL Certificate Management
- Environment Variables
- Database Migrations
- Rollback Procedure
- Monitoring
- Troubleshooting
1. Prerequisites
Before deploying, ensure you have:
GitHub Repository Access
- Repository:
https://github.com/abono200/lofty-golden-oil-erp.git - You need push access to
main(production) anddevelop(staging) branches - Verify access:
git remote -vshould show theoriginremote
VPS SSH Access
- IP: 170.9.14.60
- User:
opc - SSH Key:
/Users/fuhsi/Documents/helpdesk/lofty-golden-oil-erp/ssh-key-2026-07-25.key - Test connection:
ssh -i /path/to/ssh-key-2026-07-25.key opc@170.9.14.60 "echo 'Connected OK'"
DNS Configured
loftygoldenoil.comA record must point to170.9.14.60- Verify:
dig loftygoldenoil.com +shortshould return170.9.14.60
GitHub Secrets Configured
VPS_HOST:170.9.14.60VPS_SSH_KEY: Contents ofdeploy/oracle_key(private SSH key)- These are configured in GitHub → Settings → Secrets and variables → Actions
2. CI/CD Pipeline Overview
The deployment pipeline is defined in .github/workflows/deploy.yml and runs automatically on every push to main.
Pipeline Architecture
Developer pushes to main
│
▼
GitHub Actions triggers
│
▼
┌─────────────────┐
│ Checkout code │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Setup Node 22 │
└────────┬────────┘
│
▼
┌─────────────────┐
│ npm ci │
└────────┬────────┘
│
▼
┌─────────────────┐
│ npm run lint │
└────────┬────────┘
│
▼
┌─────────────────┐
│ npm run build │ ← Vite produces dist/
└────────┬────────┘
│
▼
┌─────────────────────┐
│ SCP dist/ to VPS │ → /opt/lgo-erp/dist/
└────────┬────────────┘
│
▼
┌─────────────────────┐
│ SCP server files │ → /opt/lgo-erp/server/
└────────┬────────────┘
│
▼
┌─────────────────────┐
│ SSH: pm2 restart │
│ + pm2 save │
└─────────────────────┘
3. How the Pipeline Works
Step-by-Step Walkthrough
Step 1 — Trigger
The workflow triggers on push to main branch, or via manual workflow_dispatch.
Step 2 — Checkout
- uses: actions/checkout@v4
Clones the repository into the runner.
Step 3 — Setup Node.js 22
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
Installs Node.js 22 and caches the npm store for faster runs.
Step 4 — Install Dependencies
- run: npm ci
Clean install of all npm packages from package-lock.json.
Step 5 — Lint
- run: npm run lint
Runs ESLint to catch code quality issues before building.
Step 6 — Build
- run: npm run build
Vite compiles the React frontend into the dist/ directory (ESNext target, WASM support enabled).
Step 7 — Setup SSH
- name: Setup SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.VPS_SSH_KEY }}" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh-keyscan -H ${{ secrets.VPS_HOST }} >> ~/.ssh/known_hosts 2>/dev/null
Writes the private deploy key and adds the VPS host key to known_hosts.
Step 8 — Test SSH
- name: Test SSH
run: ssh -i ~/.ssh/deploy_key -o ConnectTimeout=10 opc@${{ secrets.VPS_HOST }} "echo 'SSH OK'"
Verifies SSH connectivity before attempting deployment.
Step 9 — Deploy Frontend
- name: Deploy frontend
run: |
ssh -i ~/.ssh/deploy_key opc@${{ secrets.VPS_HOST }} \
"rm -rf /opt/lgo-erp/dist/assets /opt/lgo-erp/dist/index.html /opt/lgo-erp/dist/*.svg /opt/lgo-erp/dist/*.wasm"
scp -i ~/.ssh/deploy_key -r dist/* opc@${{ secrets.VPS_HOST }}:/opt/lgo-erp/dist/
Removes old build artifacts, then copies the new dist/ contents via SCP.
Step 10 — Deploy Server
- name: Deploy server
run: |
scp -i ~/.ssh/deploy_key server/server.js server/email.js opc@${{ secrets.VPS_HOST }}:/opt/lgo-erp/server/
ssh -i ~/.ssh/deploy_key opc@${{ secrets.VPS_HOST }} \
"pm2 restart lgo-prod && pm2 save && echo 'DEPLOYED OK'"
Copies server files, then restarts the PM2 process and saves the process list.
4. Branch Strategy
| Branch | Environment | URL | PM2 Process |
|---|---|---|---|
main | Production | https://loftygoldenoil.com | lgo-prod (port 4000) |
develop | Staging | http://170.9.14.60:8443 | lgo-staging (port 8443) |
Rules
- Never push directly to
main— always merge via PR fromdevelop developis the integration branch; feature branches merge here first- Production deployments only happen when code is merged to
main - Hotfixes branch from
main, get merged back to bothmainanddevelop
Workflow
feature-branch → develop → main
↓
Production deploy
5. Deploying to Production
Automated (Recommended)
# Ensure your changes are on develop
git checkout develop
git merge feature/your-feature
git push origin develop # Deploys to staging
# After testing on staging, merge to main
git checkout main
git merge develop
git push origin main # Triggers production deployment
Verify Deployment
After pushing to main:
- Go to GitHub → Actions tab → verify the workflow completes successfully
- Visit https://loftygoldenoil.com and verify changes are live
- Check API health:
curl https://loftygoldenoil.com/api/health
6. Deploying to Staging
git checkout develop
git push origin develop
Verify Staging
- Check GitHub Actions for successful workflow run
- Visit http://170.9.14.60:8443
- API health check:
curl http://170.9.14.60:8443/api/health
Note: Staging runs on port 8443 directly (no Nginx SSL, no API proxy). The PM2 process serves both static files and the API.
7. Manual Deployment
Use when GitHub Actions is unavailable or for emergency patches.
SSH into VPS
ssh -i /path/to/ssh-key-2026-07-25.key opc@170.9.14.60
Navigate to Application Directory
cd /opt/lgo-erp
Pull Latest Code
git pull origin main
Install Dependencies
cd server && npm install --production && cd ..
Build Frontend
npm ci && npm run build
Restart PM2
pm2 restart lgo-prod
pm2 save
Verify
pm2 status
curl https://loftygoldenoil.com/api/health
Manual Deploy for Staging
# Same steps but with different PM2 process
pm2 restart lgo-staging
pm2 save
curl http://170.9.14.60:8443/api/health
8. PM2 Management
PM2 manages the Node.js processes on the VPS.
View Process Status
pm2 status
Output shows process name, PID, CPU/memory usage, uptime, and restart count.
View Logs
# Real-time logs
pm2 logs lgo-prod
# Last 100 lines
pm2 logs lgo-prod --lines 100
# Error logs only
pm2 logs lgo-prod --err
# Output logs only
pm2 logs lgo-prod --out
# Logs are also stored at:
# /opt/lgo-erp/server/logs/error.log
# /opt/lgo-erp/server/logs/out.log
Restart Process
pm2 restart lgo-prod
Stop Process
pm2 stop lgo-prod
Start Process
pm2 start lgo-prod
Monitor Resources (Interactive)
pm2 monit
Displays real-time CPU, memory, and network metrics for all processes.
Save Process List
pm2 save
Saves the current process list so it can be restored after reboot.
Restore Saved Processes
pm2 resurrect
View PM2 Configuration
pm2 show lgo-prod
Delete Process
pm2 delete lgo-prod
PM2 Startup (Auto-Start on Boot)
pm2 startup
# Follow the instructions it prints
pm2 save
9. Nginx Management
Nginx serves the React static files and proxies API requests to Node.js.
Test Configuration
sudo nginx -t
Always run this before reloading. It checks for syntax errors.
Reload Configuration
sudo systemctl reload nginx
Reloads config without dropping connections.
Restart Nginx
sudo systemctl restart nginx
Check Status
sudo systemctl status nginx
View Access Logs
sudo tail -f /var/log/nginx/access.log
View Error Logs
sudo tail -f /var/log/nginx/error.log
Configuration File Location
/etc/nginx/conf.d/lgo-erp.conf
Key Nginx Configuration Details
- Listens on port 443 (SSL) for production
- Serves React static files from
/opt/lgo-erp/dist/ - Proxies
/api/requests tohttp://127.0.0.1:4000 - SPA fallback:
try_files $uri $uri/ /index.html - Gotcha: The default
nginx.confserver block must be commented out to avoid conflicts
10. SSL Certificate Management
SSL is managed via Let's Encrypt with certbot.
Check Certificate Status
sudo certbot certificates
Test Auto-Renewal
sudo certbot renew --dry-run
Manual Renewal
sudo certbot renew
Auto-Renewal
Certbot installs a systemd timer that runs twice daily:
sudo systemctl status certbot-renew.timer
View Certificate Files
/etc/letsencrypt/live/loftygoldenoil.com/
├── fullchain.pem # Certificate chain
├── privkey.pem # Private key
├── cert.pem # Certificate only
└── chain.pem # CA chain only
Revoke Certificate
sudo certbot revoke --cert-name loftygoldenoil.com
Install New Certificate
sudo certbot --nginx -d loftygoldenoil.com -d www.loftygoldenoil.com
11. Environment Variables
Environment variables are set in the PM2 ecosystem config file:
/opt/lgo-erp/ecosystem.config.cjs
Production Variables
| Variable | Value | Description |
|---|---|---|
NODE_ENV | production | Runtime environment |
PORT | 4000 | Express server port |
DB_HOST | 127.0.0.1 | MySQL host |
DB_USER | lgo | MySQL username |
DB_PASS | Lg0Erp2026!Db | MySQL password |
DB_NAME | lgo_erp_prod | MySQL database name |
JWT_SECRET | (see ecosystem.config.cjs) | JWT signing secret |
LGO_STATIC_DIR | /opt/lgo-erp/dist | React build output path |
Staging Variables
| Variable | Value | Description |
|---|---|---|
NODE_ENV | staging | Runtime environment |
PORT | 8443 | Express server port |
DB_NAME | lgo_erp_staging | MySQL database name |
Updating Environment Variables
# SSH to VPS
ssh -i /path/to/ssh-key opc@170.9.14.60
# Edit the ecosystem config
nano /opt/lgo-erp/ecosystem.config.cjs
# Restart to apply
pm2 restart lgo-prod
pm2 save
Important: Never commit secrets to git. The ecosystem config on the VPS contains production secrets.
12. Database Migrations
The system currently uses manual schema updates (no automated migration tool).
Connecting to MySQL
# Production
mysql -u lgo -p lgo_erp_prod
# Staging
mysql -u lgo -p lgo_erp_staging
Password: Lg0Erp2026!Db
Running a Migration
-
Create a backup first:
mysqldump -u lgo -p lgo_erp_prod > /tmp/lgo_prod_backup_$(date +%Y%m%d).sql -
Write your ALTER statements:
-- Example: Adding a new column to customersALTER TABLE customers ADD COLUMN credit_limit DOUBLE DEFAULT 0; -
Run against production:
mysql -u lgo -p lgo_erp_prod < migration.sql -
Run against staging first:
mysql -u lgo -p lgo_erp_staging < migration.sql -
Test on staging before applying to production
Schema File Location
server/schema.sql # Current schema definition
server/seed.sql # Seed data
Gotcha
The schema.sql file contains USE lgo_erp; — this must be stripped or replaced with the correct database name when applying to production or staging.
13. Rollback Procedure
Application Rollback (Code)
-
Identify the last good commit:
git log --oneline -20 -
Revert to previous version:
git revert HEADgit push origin mainThis triggers the CI/CD pipeline to deploy the previous code.
OR reset to a specific commit:
git reset --hard <commit-hash>git push --force origin main⚠️ Use
--forceonly in emergencies. -
Verify:
curl https://loftygoldenoil.com/api/health
Database Rollback
-
Restore from backup:
mysql -u lgo -p lgo_erp_prod < /tmp/lgo_prod_backup_YYYYMMDD.sql -
Or use the API backup restore:
- Navigate to the admin panel
- Upload a JSON backup file via the restore endpoint
Full Rollback Checklist
- Revert application code
- Restore database from backup
- Restart PM2:
pm2 restart lgo-prod - Verify functionality
- Notify stakeholders
14. Monitoring
PM2 Monitoring
# Real-time dashboard
pm2 monit
# Quick status check
pm2 status
# Resource usage
pm2 show lgo-prod | grep -E "(memory|cpu|status)"
Log Monitoring
# Application logs
pm2 logs lgo-prod --lines 50
# Error log file
tail -f /opt/lgo-erp/server/logs/error.log
# Nginx access log
sudo tail -f /var/log/nginx/access.log
# Nginx error log
sudo tail -f /var/log/nginx/error.log
MySQL Monitoring
# Check MySQL status
sudo systemctl status mysqld
# Check connections
mysql -u lgo -p -e "SHOW STATUS LIKE 'Threads_connected';" lgo_erp_prod
# Check slow queries
mysql -u lgo -p -e "SHOW STATUS LIKE 'Slow_queries';" lgo_erp_prod
Health Check
# API health
curl -s https://loftygoldenoil.com/api/health | jq .
# Expected response:
# {"status":"ok","time":"2026-07-26T...","version":2}
Uptime Monitoring
Consider adding an external uptime monitor (e.g., UptimeRobot, Pingdom) to:
https://loftygoldenoil.com/api/healthhttp://170.9.14.60:8443/api/health(staging)
15. Troubleshooting
Common Issues and Fixes
1. Deployment Fails — SSH Connection Error
Symptom: GitHub Actions fails at "Test SSH" step.
Fix:
- Verify
VPS_SSH_KEYsecret contains the full private key (including-----BEGIN OPENSSH PRIVATE KEY-----) - Verify
VPS_HOSTis170.9.14.60 - Test SSH manually:
ssh -i /path/to/key opc@170.9.14.60 "echo ok"
2. Site Shows 502 Bad Gateway
Symptom: Nginx returns 502 at https://loftygoldenoil.com.
Fix:
# Check if PM2 process is running
pm2 status
# Restart if stopped
pm2 restart lgo-prod
# Check if port 4000 is listening
ss -tlnp | grep 4000
3. Static Files Not Loading (404 on JS/CSS)
Symptom: React app loads but JS/CSS files return 404.
Fix:
- Verify
/opt/lgo-erp/dist/containsindex.htmlandassets/folder - Check Nginx root directive:
root /opt/lgo-erp/dist; - Rebuild and redeploy:
npm run build && scp -r dist/* opc@170.9.14.60:/opt/lgo-erp/dist/
4. SSL Certificate Expired
Symptom: Browser shows security warning.
Fix:
sudo certbot renew
sudo systemctl reload nginx
5. MySQL Connection Refused
Symptom: API returns 500 with "Connection refused" error.
Fix:
# Check MySQL is running
sudo systemctl status mysqld
# Start if stopped
sudo systemctl start mysqld
# Check credentials
mysql -u lgo -p lgo_erp_prod
6. PM2 Process Keeps Restarting
Symptom: pm2 status shows restarting state.
Fix:
# Check error logs
pm2 logs lgo-prod --lines 50
# Common causes:
# - Port already in use
# - Database connection failure
# - Missing environment variables
7. Port 443 Binding Error
Symptom: Node.js crashes with "EACCES: permission denied" on port 443.
Fix: Node.js cannot bind to port 443 as non-root. Use Nginx as reverse proxy on port 443 → proxy to Node.js on port 4000. This is the current architecture.
8. SELinux Blocking Connections
Symptom: Nginx cannot proxy to Node.js.
Fix:
sudo setsebool -P httpd_can_network_connect on
sudo setsebool -P httpd_can_network_connect_db on
9. Oracle Cloud Security List Blocks Traffic
Symptom: Cannot access the site from external network.
Fix: Verify Oracle Cloud security list:
- Source Port: All
- Destination Port: 80, 443, 8443, 22
- ⚠️ Gotcha: Do not swap source and destination ports
10. Default Nginx Server Block Conflict
Symptom: Nginx serves default page instead of LGO ERP.
Fix:
# Comment out the default server block in /etc/nginx/nginx.conf
sudo nano /etc/nginx/nginx.conf
# Comment or remove the default `server { ... }` block
sudo nginx -t && sudo systemctl reload nginx
Quick Reference Commands
| Task | Command |
|---|---|
| Deploy to production | git push origin main |
| Deploy to staging | git push origin develop |
| Check PM2 status | pm2 status |
| Restart production | pm2 restart lgo-prod |
| View production logs | pm2 logs lgo-prod |
| Test Nginx config | sudo nginx -t |
| Reload Nginx | sudo systemctl reload nginx |
| Test SSL renewal | sudo certbot renew --dry-run |
| Health check | curl https://loftygoldenoil.com/api/health |
| SSH to VPS | ssh -i /path/to/key opc@170.9.14.60 |