File: //tmp/lck_tmp/install-backend.sh
#!/bin/bash
# =========================================================================
# Ladang Cuan Kreator AI Backend - PM2 Auto Installer
# =========================================================================
# This script automates:
# 1. Dynamic OS Detection (Ubuntu, Debian, CentOS, RHEL, Rocky, AlmaLinux)
# 2. System Dependency Installation & Upgrades
# 3. Production Node.js Environment Setup (v18.x)
# 4. PM2 Global Installation & Setup
# 5. App Deployment & Production Dependency Extraction
# 6. SSL Self-Signed Certificate Generation
# 7. Port 1607 Firewall Auto-Configuration (UFW / Firewall-cmd)
# 8. PM2 Service Startup & Daemon Initialization
# =========================================================================
set -e # Exit on error
# --- Configuration ---
APP_NAME="lck-backend"
INSTALL_DIR="/opt/$APP_NAME"
# Prioritize command line argument, then environment variable, then default port 1607
PORT=${1:-${PORT:-1607}}
DISABLE_HTTPS=${2:-${DISABLE_HTTPS:-false}}
NODE_VERSION="18"
# Colors for outstanding terminal aesthetics
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
echo -e "${CYAN}=================================================================${NC}"
echo -e "${PURPLE} Ladang Cuan Kreator AI Backend - Automated PM2 Deployer ${NC}"
echo -e "${CYAN}=================================================================${NC}"
# 1. Verify Root Privileges
if [ "$EUID" -ne 0 ]; then
echo -e "${RED}Error: Please run as root or with sudo privileges.${NC}"
exit 1
fi
# Repair /tmp permissions and recreate, but if /tmp is a read-only filesystem (common on cPanel loopback mounts),
# redirect all system temporary writes to a safe, writable fallback folder (/var/tmp/lck_tmp or /root/lck_tmp).
echo -e "Configuring writable system temporary environment..."
LCK_TMP="/var/tmp/lck_tmp"
mkdir -p "$LCK_TMP" 2>/dev/null || LCK_TMP="/root/lck_tmp"
mkdir -p "$LCK_TMP" || true
chmod 777 "$LCK_TMP" || true
export TMPDIR="$LCK_TMP"
export TEMP="$LCK_TMP"
export TMP="$LCK_TMP"
echo -e "Temporary directory redirected to: ${GREEN}$TMPDIR${NC}"
# If /var/tmp is also read-only or symlinked to /tmp, override the RPM transaction path
# by writing a custom %_tmppath macro in the root user's personal .rpmmacros file.
if [ "$EUID" -eq 0 ]; then
echo -e "Configuring RPM macro overrides to use writable temp path..."
mkdir -p /root/lck_tmp || true
chmod 777 /root/lck_tmp || true
echo "%_tmppath /root/lck_tmp" > /root/.rpmmacros
echo -e "RPM temporary path redirected to: ${GREEN}/root/lck_tmp${NC}"
fi
# Check if Node.js and PM2 are already present and fully operational
SKIP_RUNTIME_SETUP=false
if command -v node >/dev/null 2>&1 && command -v pm2 >/dev/null 2>&1; then
SKIP_RUNTIME_SETUP=true
echo -e "${GREEN}Node.js ($(node -v)) and PM2 ($(pm2 -v 2>/dev/null || echo 'active')) are already installed.${NC}"
echo -e "${GREEN}Skipping system runtime/dependencies setup to complete updates in seconds!${NC}"
# Resolve PM2 command path dynamically for safe-path execution
PM2_CMD="pm2"
if [ -x "/usr/bin/pm2" ] && ( /usr/bin/pm2 -v >/dev/null 2>&1 ); then
PM2_CMD="/usr/bin/pm2"
elif [ -x "/usr/local/bin/pm2" ] && ( /usr/local/bin/pm2 -v >/dev/null 2>&1 ); then
PM2_CMD="/usr/local/bin/pm2"
fi
fi
if [ "$SKIP_RUNTIME_SETUP" = false ]; then
# 2. Dynamic OS Detection & Capability Assessment
echo -e "${CYAN}[1/8] Assessing Server OS & System Libraries...${NC}"
# Detect GLIBC Version to determine Node.js compatibility scientifically
GLIBC_VER=$(ldd --version 2>/dev/null | head -n1 | grep -oE '[0-9]+\.[0-9]+' | head -n1 || echo "2.17")
echo -e "Detected System GLIBC Version: ${GREEN}$GLIBC_VER${NC}"
if [ -f /etc/os-release ]; then
. /etc/os-release
OS_ID=$ID
OS_NAME=$NAME
OS_VERSION=$VERSION_ID
else
echo -e "${YELLOW}Warning: /etc/os-release not found. Checking standard release files...${NC}"
if [ -f /etc/cloudlinux-release ]; then
OS_ID="cloudlinux"
OS_NAME="CloudLinux"
OS_VERSION=$(cat /etc/cloudlinux-release | grep -oE '[0-9]+\.[0-9]+' | head -n1)
elif [ -f /etc/centos-release ]; then
OS_ID="centos"
OS_NAME="CentOS"
OS_VERSION=$(cat /etc/centos-release | grep -oE '[0-9]+\.[0-9]+' | head -n1)
elif [ -f /etc/redhat-release ]; then
OS_ID="rhel"
OS_NAME="RedHat"
OS_VERSION=$(cat /etc/redhat-release | grep -oE '[0-9]+\.[0-9]+' | head -n1)
else
OS_ID="generic"
OS_NAME="Generic Linux"
OS_VERSION="unknown"
fi
fi
echo -e "Detected OS: ${GREEN}$OS_NAME ($OS_ID, Version: $OS_VERSION)${NC}"
# Scientifically determine max supported Node.js version
# Node 18+ requires glibc 2.28+ (modern systems)
# Node 16+ requires glibc 2.17+ (EL7 / CloudLinux 7 / CentOS 7)
# Node 10-14 can run on glibc 2.12 (EL6 / CloudLinux 6 / CentOS 6)
if [ "$(echo -e "$GLIBC_VER\n2.28" | sort -V | head -n1)" = "2.28" ]; then
NODE_VERSION="18"
echo -e "System supports modern Node.js. Target version: ${GREEN}v18${NC}"
elif [ "$(echo -e "$GLIBC_VER\n2.17" | sort -V | head -n1)" = "2.17" ]; then
NODE_VERSION="16"
echo -e "GLIBC 2.17 limitation detected. Target version: ${GREEN}v16${NC}"
else
NODE_VERSION="10"
echo -e "GLIBC 2.12 limitation detected (CentOS 6 / CloudLinux 6). Target version: ${GREEN}v10${NC}"
fi
# Determine Package Manager and commands
if [[ "$OS_ID" == "ubuntu" || "$OS_ID" == "debian" || "$OS_ID" == "raspbian" ]]; then
PKG_UPDATE="apt-get update -y"
PKG_INSTALL="apt-get install -y"
FIREWALL="ufw"
elif [[ "$OS_ID" == "centos" || "$OS_ID" == "rhel" || "$OS_ID" == "rocky" || "$OS_ID" == "almalinux" || "$OS_ID" == "cloudlinux" ]]; then
if command -v dnf >/dev/null 2>&1; then
PKG_MANAGER="dnf"
else
PKG_MANAGER="yum"
fi
PKG_UPDATE="$PKG_MANAGER makecache --setopt=skip_if_unavailable=true"
PKG_INSTALL="$PKG_MANAGER install -y --skip-broken --setopt=skip_if_unavailable=true"
FIREWALL="firewalld"
else
echo -e "${RED}Unsupported OS: $OS_ID. Please install Node.js and PM2 manually.${NC}"
exit 1
fi
# 3. System Update & Base System Dependency Installation
echo -e "${CYAN}[2/8] Installing vital system build dependencies...${NC}"
$PKG_UPDATE
if [[ "$OS_ID" == "ubuntu" || "$OS_ID" == "debian" ]]; then
$PKG_INSTALL curl wget tar build-essential openssl unzip
else
$PKG_INSTALL curl wget tar gcc-c++ make openssl-devel unzip
fi
# 4. Install Node.js Environment
if command -v node >/dev/null 2>&1 && node -v >/dev/null 2>&1 && command -v npm >/dev/null 2>&1; then
CURRENT_NODE_VER=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
echo -e "Node.js and npm are already installed (Node: v$(node -v), npm: v$(npm -v 2>/dev/null || echo "unknown"))."
if [ "$CURRENT_NODE_VER" -lt "$NODE_VERSION" ]; then
echo -e "${YELLOW}Warning: Node version is too old. Upgrading to Node.js v${NODE_VERSION}...${NC}"
INSTALL_NODE=true
else
# If we are forced to v16 on EL7 or v10 on EL6, reinstall to satisfy glibc constraints
if [[ "$NODE_VERSION" == "16" && "$CURRENT_NODE_VER" -gt 16 ]]; then
echo -e "${YELLOW}Warning: Node.js v${CURRENT_NODE_VER} detected on EL7. Reinstalling Node.js v16...${NC}"
INSTALL_NODE=true
elif [[ "$NODE_VERSION" == "10" && "$CURRENT_NODE_VER" -gt 10 ]]; then
echo -e "${YELLOW}Warning: Node.js v${CURRENT_NODE_VER} detected on EL6. Reinstalling Node.js v10...${NC}"
INSTALL_NODE=true
else
INSTALL_NODE=false
fi
fi
else
echo -e "${YELLOW}Node.js/npm not found or is incomplete. Proceeding with clean engine installation...${NC}"
INSTALL_NODE=true
fi
if [ "$INSTALL_NODE" = true ]; then
echo -e "${CYAN}[3/8] Setting up Node.js v${NODE_VERSION}.x...${NC}"
# Resolve the exact pre-compiled tarball version based on capability detection
NODE_TAR_VER="18.20.2"
if [ "$NODE_VERSION" = "16" ]; then
NODE_TAR_VER="16.20.2"
elif [ "$NODE_VERSION" = "10" ]; then
NODE_TAR_VER="10.24.1"
fi
echo -e "${YELLOW}Universal Linux Deployment: Installing pre-compiled Node.js v${NODE_TAR_VER} x64 binary...${NC}"
# Purge old Nodesource repositories and local conflicting package manager files
echo -e "Purging conflicting node/npm packages and caching..."
if command -v apt-get >/dev/null 2>&1; then
apt-get remove -y nodejs npm >/dev/null 2>&1 || true
apt-get autoremove -y >/dev/null 2>&1 || true
elif command -v yum >/dev/null 2>&1; then
rm -f /etc/yum.repos.d/nodesource-*.repo || true
yum clean all >/dev/null 2>&1 || true
yum remove -y nodejs npm >/dev/null 2>&1 || true
fi
# Force delete existing system links to ensure clean rewrite
rm -f /usr/bin/node /usr/bin/npm /usr/bin/npx /usr/bin/pm2 /usr/local/bin/pm2 || true
NODE_TAR="node-v${NODE_TAR_VER}-linux-x64.tar.gz"
echo -e "Downloading Node.js tarball..."
if wget -q --no-check-certificate "https://nodejs.org/dist/v${NODE_TAR_VER}/$NODE_TAR"; then
echo -e "Download complete via wget."
elif curl -k -sL -O "https://nodejs.org/dist/v${NODE_TAR_VER}/$NODE_TAR"; then
echo -e "Download complete via curl."
else
echo -e "${RED}Error: Failed to download Node.js tarball using wget or curl.${NC}"
exit 1
fi
if [ -f "$NODE_TAR" ]; then
echo -e "Extracting Node.js v${NODE_TAR_VER} to /usr/local..."
tar -xzf "$NODE_TAR" -C /usr/local --strip-components=1
rm -f "$NODE_TAR"
# Create standard system absolute symlinks directly pointing to absolute library files
# This completely avoids writing/modifying /usr/local/bin/npm which may be write-protected (chattr/selinux) by the OS
ln -sf /usr/local/bin/node /usr/bin/node
ln -sf /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/bin/npm
ln -sf /usr/local/lib/node_modules/npm/bin/npx-cli.js /usr/bin/npx
# Ensure all binary links and modules are executable
chmod +x /usr/local/bin/node /usr/local/bin/npm /usr/local/bin/npx || true
chmod +x /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/lib/node_modules/npm/bin/npx-cli.js || true
echo -e "${GREEN}Node.js v${NODE_TAR_VER} pre-compiled binary installed successfully!${NC}"
else
echo -e "${RED}Error: Node.js tarball not found after download.${NC}"
exit 1
fi
fi
# 5. Install & Setup PM2 Globally
echo -e "${CYAN}[4/8] Installing & Setting up PM2 Process Manager globally...${NC}"
# Detect if PM2 is broken (circular symlinks, missing, or throws errors)
PM2_BROKEN=false
if command -v pm2 >/dev/null 2>&1; then
# Use a subshell to safely check pm2 -v without exiting under set -e
if ! (pm2 -v >/dev/null 2>&1); then
echo -e "${YELLOW}Warning: Existing pm2 installation is broken or has circular symbolic links.${NC}"
PM2_BROKEN=true
fi
else
# If command -v pm2 fails, but the files still exist (e.g. broken symlink not in PATH or failing resolve)
if [ -L "/usr/bin/pm2" ] || [ -L "/usr/local/bin/pm2" ]; then
echo -e "${YELLOW}Warning: Broken or circular pm2 symbolic links detected in system directories.${NC}"
PM2_BROKEN=true
fi
fi
# Clean up broken links if detected
if [ "$PM2_BROKEN" = true ]; then
echo -e "Cleaning up broken/circular pm2 symbolic links..."
rm -f /usr/bin/pm2 /usr/local/bin/pm2 || true
fi
if ! command -v pm2 >/dev/null 2>&1 || [ "$INSTALL_NODE" = true ] || [ "$PM2_BROKEN" = true ]; then
# Force clean up before npm install to prevent npm linking errors
rm -f /usr/bin/pm2 /usr/local/bin/pm2 || true
echo -e "Installing PM2 globally via npm..."
# Dynamically select PM2 version based on the active Node.js version
PM2_INSTALL_TARGET="pm2"
CURRENT_NODE_MAJOR=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
if [ "$CURRENT_NODE_MAJOR" -lt 18 ]; then
PM2_INSTALL_TARGET="pm2@5.3.0"
echo -e "${YELLOW}Warning: Active Node.js major version v${CURRENT_NODE_MAJOR} is less than v18.${NC}"
echo -e "Targeting compatible PM2 version: ${GREEN}${PM2_INSTALL_TARGET}${NC}"
fi
# Set a custom global prefix inside the root home folder to avoid permissions/lchown errors in /usr/local/bin
echo -e "Configuring secure npm global prefix in /root/.npm-global to bypass system blocks..."
mkdir -p /root/.npm-global || true
npm config set prefix '/root/.npm-global' || true
export PATH="/root/.npm-global/bin:$PATH"
echo -e "Attempting global PM2 installation (will fallback gracefully to local PM2 on failure)..."
if npm install -g "$PM2_INSTALL_TARGET" --unsafe-perm=true --allow-root; then
echo -e "${GREEN}PM2 installed/updated successfully globally.${NC}"
else
echo -e "${YELLOW}Warning: Global PM2 installation failed due to system permissions or lchown blocks.${NC}"
echo -e "${YELLOW}Graceful fallback: We will install and run PM2 locally inside the project folder.${NC}"
fi
else
# Safely get PM2 version in a subshell to avoid set -e crashing if something unexpected happens
PM2_CURRENT_VERSION=$(pm2 -v 2>/dev/null || echo "unknown")
echo -e "PM2 is already installed and working (v$PM2_CURRENT_VERSION). Skipping reinstall..."
fi
# Ensure PM2 is in system PATH with absolute, non-circular symlinks
echo -e "Verifying global PM2 executable path and resolving symlinks..."
# Find the real, physical PM2 CLI script in global node_modules
NPM_PREFIX=$(npm prefix -g 2>/dev/null || echo "/usr/local")
REAL_PM2=""
if [ -f "$NPM_PREFIX/lib/node_modules/pm2/bin/pm2" ]; then
REAL_PM2="$NPM_PREFIX/lib/node_modules/pm2/bin/pm2"
elif [ -f "/usr/local/lib/node_modules/pm2/bin/pm2" ]; then
REAL_PM2="/usr/local/lib/node_modules/pm2/bin/pm2"
elif [ -f "/usr/lib/node_modules/pm2/bin/pm2" ]; then
REAL_PM2="/usr/lib/node_modules/pm2/bin/pm2"
fi
if [ -n "$REAL_PM2" ]; then
echo -e "Found physical PM2 CLI entrypoint at: ${GREEN}$REAL_PM2${NC}"
# Delete symlinks first to prevent link-over-link issues
rm -f /usr/bin/pm2 /usr/local/bin/pm2 || true
ln -sf "$REAL_PM2" /usr/bin/pm2 || true
ln -sf "$REAL_PM2" /usr/local/bin/pm2 || true
echo -e "Created direct, non-circular symlinks for /usr/bin/pm2 and /usr/local/bin/pm2."
else
# Defensive fallback if npm global structure is non-standard
echo -e "${YELLOW}Warning: Physical pm2 entrypoint not found in standard node_modules. Using path-based linking...${NC}"
PM2_PATH=""
# Safely find a working pm2 binary (must be a regular file or a valid symlink, not a loop)
if [ -f "/usr/local/bin/pm2" ] && [ ! -L "/usr/local/bin/pm2" ]; then
PM2_PATH="/usr/local/bin/pm2"
elif [ -f "/usr/bin/pm2" ] && [ ! -L "/usr/bin/pm2" ]; then
PM2_PATH="/usr/bin/pm2"
else
# Ask npm where its global bin directory is
NPM_BIN_DIR=$(npm bin -g 2>/dev/null || npm prefix -g 2>/dev/null || echo "")
if [ -n "$NPM_BIN_DIR" ]; then
if [ -d "$NPM_BIN_DIR/bin" ] && [ -f "$NPM_BIN_DIR/bin/pm2" ]; then
PM2_PATH="$NPM_BIN_DIR/bin/pm2"
elif [ -f "$NPM_BIN_DIR/pm2" ]; then
PM2_PATH="$NPM_BIN_DIR/pm2"
fi
fi
fi
if [ -n "$PM2_PATH" ]; then
# Ensure we do not link to self
if [ "$PM2_PATH" != "/usr/bin/pm2" ]; then
rm -f /usr/bin/pm2 || true
ln -sf "$PM2_PATH" /usr/bin/pm2 || true
fi
if [ "$PM2_PATH" != "/usr/local/bin/pm2" ]; then
rm -f /usr/local/bin/pm2 || true
ln -sf "$PM2_PATH" /usr/local/bin/pm2 || true
fi
else
echo -e "${RED}Error: pm2 binary could not be located. PM2 setup may fail.${NC}"
fi
fi
# Clear bash command lookup path cache
hash -r || true
# Define PM2 command path dynamically, prioritizing absolute system executable paths
PM2_CMD="pm2"
if [ -x "/usr/bin/pm2" ] && ( /usr/bin/pm2 -v >/dev/null 2>&1 ); then
PM2_CMD="/usr/bin/pm2"
elif [ -x "/usr/local/bin/pm2" ] && ( /usr/local/bin/pm2 -v >/dev/null 2>&1 ); then
PM2_CMD="/usr/local/bin/pm2"
fi
echo -e "Resolved PM2 execution command to: ${GREEN}$PM2_CMD${NC}"
fi # End of SKIP_RUNTIME_SETUP skip
# 6. Extract Application Files and Install Production Dependencies
echo -e "${CYAN}[5/8] Creating installation directory at $INSTALL_DIR...${NC}"
mkdir -p "$INSTALL_DIR"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ZIP_FILE="$SCRIPT_DIR/backend.zip"
if [ -f "$ZIP_FILE" ]; then
# Calculate and log file size and MD5 checksum
FILE_SIZE=$(wc -c < "$ZIP_FILE" | tr -d '[:space:]' 2>/dev/null || echo "unknown")
if command -v md5sum >/dev/null 2>&1; then
FILE_MD5=$(md5sum "$ZIP_FILE" | awk '{print $1}')
elif command -v md5 >/dev/null 2>&1; then
FILE_MD5=$(md5 -q "$ZIP_FILE")
else
FILE_MD5="unknown"
fi
echo -e "Staged backend.zip verification: Size = ${FILE_SIZE} bytes, MD5 = ${FILE_MD5}"
# Verify Zip file integrity before extracting to prevent hangs and corrupt installs
echo -e "Testing archive integrity..."
if unzip -tq "$ZIP_FILE" >/dev/null 2>&1; then
echo -e "${GREEN}Staged zip archive is valid.${NC}"
else
echo -e "${RED}Critical Error: backend.zip is corrupted or not a valid zip archive!${NC}"
echo -e "${YELLOW}Analyzing file headers to diagnose download failure...${NC}"
# Read the first few chars to detect HTML/error pages
FIRST_CHARS=$(head -c 100 "$ZIP_FILE" 2>/dev/null || true)
echo -e "File signature preview: \"$FIRST_CHARS\""
if [[ "$FIRST_CHARS" == *"<!"* || "$FIRST_CHARS" == *"<html"* || "$FIRST_CHARS" == *"Request Rejected"* ]]; then
echo -e "${RED}WAF / Firewall detected! The download URL returned an HTML block page instead of the actual zip. Check connection/credentials.${NC}"
fi
exit 1
fi
echo -e "Existing config/session detection..."
if [ -f "$INSTALL_DIR/config.json" ]; then
echo -e "Backing up existing ${GREEN}config.json${NC}..."
cp "$INSTALL_DIR/config.json" "$LCK_TMP/config.json.bak"
fi
if [ -f "$INSTALL_DIR/cookie.json" ]; then
echo -e "Backing up existing ${GREEN}cookie.json${NC}..."
cp "$INSTALL_DIR/cookie.json" "$LCK_TMP/cookie.json.bak"
fi
if [ -f "$INSTALL_DIR/history.json" ]; then
echo -e "Backing up existing ${GREEN}history.json${NC}..."
cp "$INSTALL_DIR/history.json" "$LCK_TMP/history.json.bak"
fi
echo -e "Extracting deployment archive backend.zip into $INSTALL_DIR..."
unzip -o "$ZIP_FILE" -d "$INSTALL_DIR"
echo -e "Restoring backup configurations..."
if [ -f "$LCK_TMP/config.json.bak" ]; then
echo -e "Merging backup config.json with package configuration..."
node -e "
const fs = require('fs');
const backupPath = '$LCK_TMP/config.json.bak';
const currentPath = '$INSTALL_DIR/config.json';
let current = {};
try { if (fs.existsSync(currentPath)) current = JSON.parse(fs.readFileSync(currentPath, 'utf8')); } catch(e) {}
let backup = {};
try { if (fs.existsSync(backupPath)) backup = JSON.parse(fs.readFileSync(backupPath, 'utf8')); } catch(e) {}
const merged = { ...current, ...backup };
fs.writeFileSync(currentPath, JSON.stringify(merged, null, 2), 'utf8');
" 2>/dev/null || mv "$LCK_TMP/config.json.bak" "$INSTALL_DIR/config.json"
fi
if [ -f "$LCK_TMP/cookie.json.bak" ]; then
mv "$LCK_TMP/cookie.json.bak" "$INSTALL_DIR/cookie.json"
fi
if [ -f "$LCK_TMP/history.json.bak" ]; then
mv "$LCK_TMP/history.json.bak" "$INSTALL_DIR/history.json"
fi
# Update or create config.json with correct port and disableHttps
if [ ! -f "$INSTALL_DIR/config.json" ]; then
echo "{\"port\": $PORT}" > "$INSTALL_DIR/config.json"
fi
if [ "$DISABLE_HTTPS" = "true" ] || [ "$DISABLE_HTTPS" = "--disable-https" ]; then
echo -e "Configuring backend config to run with ${YELLOW}HTTPS Disabled${NC}..."
node -e "
const fs = require('fs');
const path = '$INSTALL_DIR/config.json';
const data = fs.existsSync(path) ? JSON.parse(fs.readFileSync(path, 'utf8')) : {};
data.port = parseInt('$PORT');
data.disableHttps = true;
fs.writeFileSync(path, JSON.stringify(data, null, 2), 'utf8');
" 2>/dev/null || {
sed -i 's/"port": [0-9]*/"port": '$PORT'/g' "$INSTALL_DIR/config.json"
if grep -q "disableHttps" "$INSTALL_DIR/config.json"; then
sed -i 's/"disableHttps": [a-z]*/"disableHttps": true/g' "$INSTALL_DIR/config.json"
else
sed -i 's/}/ ,\"disableHttps\": true\n}/g' "$INSTALL_DIR/config.json"
fi
}
else
node -e "
const fs = require('fs');
const path = '$INSTALL_DIR/config.json';
const data = fs.existsSync(path) ? JSON.parse(fs.readFileSync(path, 'utf8')) : {};
data.port = parseInt('$PORT');
fs.writeFileSync(path, JSON.stringify(data, null, 2), 'utf8');
" 2>/dev/null || {
sed -i 's/"port": [0-9]*/"port": '$PORT'/g' "$INSTALL_DIR/config.json"
}
fi
if [ -d "$INSTALL_DIR/node_modules" ] && [ -d "$INSTALL_DIR/node_modules/node-pty" ]; then
echo -e "${GREEN}node_modules directory and node-pty already exist. Skipping npm install...${NC}"
else
echo -e "Installing/updating production packages (npm install --production)..."
cd "$INSTALL_DIR"
npm install --production || {
echo -e "${YELLOW}Warning: Native module compilation failed. Retrying without node-pty...${NC}"
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
if (pkg.dependencies && pkg.dependencies['node-pty']) {
delete pkg.dependencies['node-pty'];
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2), 'utf8');
console.log('Removed node-pty dependency from package.json');
}
" 2>/dev/null || true
npm install --production
}
fi
# Ensure PM2 is installed. If not available globally, install it locally.
HAS_GLOBAL_PM2=false
if [ -x "/usr/bin/pm2" ] && ( /usr/bin/pm2 -v >/dev/null 2>&1 ); then
HAS_GLOBAL_PM2=true
elif [ -x "/usr/local/bin/pm2" ] && ( /usr/local/bin/pm2 -v >/dev/null 2>&1 ); then
HAS_GLOBAL_PM2=true
fi
if [ "$HAS_GLOBAL_PM2" = false ]; then
echo -e "${YELLOW}Global PM2 not found or broken. Ensuring local PM2 is installed inside $INSTALL_DIR...${NC}"
cd "$INSTALL_DIR"
# Determine PM2 version to install locally
PM2_INSTALL_TARGET="pm2"
CURRENT_NODE_MAJOR=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
if [ "$CURRENT_NODE_MAJOR" -lt 18 ]; then
PM2_INSTALL_TARGET="pm2@5.3.0"
fi
if [ ! -d "$INSTALL_DIR/node_modules/pm2" ]; then
echo -e "Installing local compatible PM2 ($PM2_INSTALL_TARGET)..."
npm install "$PM2_INSTALL_TARGET" --no-save --unsafe-perm=true --allow-root || true
fi
# Create local PM2 wrapper script to prevent quoting-expansion issues in bash
echo -e "Creating local PM2 wrapper script..."
cat << EOF > "$INSTALL_DIR/pm2-local.sh"
#!/bin/bash
node "$INSTALL_DIR/node_modules/pm2/bin/pm2" "\$@"
EOF
chmod +x "$INSTALL_DIR/pm2-local.sh" || true
PM2_CMD="$INSTALL_DIR/pm2-local.sh"
echo -e "PM2 execution command redirected to local wrapper: ${GREEN}$PM2_CMD${NC}"
fi
else
echo -e "${RED}Critical Error: backend.zip not found in $SCRIPT_DIR.${NC}"
echo -e "Ensure that backend.zip and this script are located in the same directory."
exit 1
fi
# 7. Generate Production SSL Self-Signed Certificates
echo -e "${CYAN}[6/8] Generating Self-Signed SSL Certificates...${NC}"
if [ ! -f "$INSTALL_DIR/cert.pem" ]; then
openssl req -x509 -newkey rsa:4096 -keyout "$INSTALL_DIR/key.pem" -out "$INSTALL_DIR/cert.pem" -days 365 -nodes -subj "/C=ID/ST=Jakarta/L=Jakarta/O=MagicPhotos/OU=AI/CN=localhost"
echo -e "SSL Certificates generated."
else
echo -e "SSL Certificates already exist. Skipping..."
fi
# 8. Firewall Configuration
echo -e "${CYAN}[7/8] Configuring Firewall to open port $PORT...${NC}"
HAS_FIREWALL=false
# 1. ConfigServer Firewall (CSF)
if command -v csf >/dev/null 2>&1; then
echo -e "CSF (ConfigServer Firewall) detected. Appending port $PORT..."
if [ -f /etc/csf/csf.conf ]; then
# Check if port is already allowed
if ! grep -q "$PORT" /etc/csf/csf.conf; then
sed -i "s/TCP_IN = \"/TCP_IN = \"$PORT,/g" /etc/csf/csf.conf
sed -i "s/TCP_OUT = \"/TCP_OUT = \"$PORT,/g" /etc/csf/csf.conf
echo -e "Port $PORT added to CSF TCP_IN and TCP_OUT successfully."
else
echo -e "Port $PORT is already allowed in CSF configuration."
fi
csf -r || true
echo -e "CSF firewall restarted."
HAS_FIREWALL=true
fi
fi
# 2. UFW Firewall
if [[ "$FIREWALL" == "ufw" ]]; then
if command -v ufw >/dev/null 2>&1; then
ufw allow $PORT/tcp || true
echo -e "UFW firewall port $PORT opened."
HAS_FIREWALL=true
fi
# 3. Firewalld
elif [[ "$FIREWALL" == "firewalld" ]]; then
if command -v firewall-cmd >/dev/null 2>&1; then
firewall-cmd --permanent --add-port=$PORT/tcp || true
firewall-cmd --reload || true
echo -e "Firewalld port $PORT opened."
HAS_FIREWALL=true
fi
fi
# 4. Raw iptables rules (Always run if iptables command is available to ensure packet acceptance)
if command -v iptables >/dev/null 2>&1; then
echo -e "iptables detected. Ensuring port $PORT is allowed..."
# Check if the rule already exists to prevent duplicate rules
if ! iptables -C INPUT -p tcp --dport $PORT -j ACCEPT >/dev/null 2>&1; then
iptables -I INPUT -p tcp --dport $PORT -j ACCEPT
echo -e "Port $PORT rule inserted into iptables."
else
echo -e "Port $PORT rule already exists in iptables."
fi
# Save the rule to make it persistent on CentOS/RHEL/CloudLinux
if [ -f /etc/redhat-release ] || [ -f /etc/centos-release ] || [ -f /etc/os-release ]; then
if command -v service >/dev/null 2>&1; then
service iptables save >/dev/null 2>&1 || true
elif [ -f /sbin/iptables-save ]; then
/sbin/iptables-save > /etc/sysconfig/iptables >/dev/null 2>&1 || true
fi
fi
HAS_FIREWALL=true
fi
if [ "$HAS_FIREWALL" = false ]; then
echo -e "${YELLOW}No standard firewall manager or iptables detected. Ensure port $PORT is open manually.${NC}"
fi
# 9. Register & Start Application under PM2 Process Manager
echo -e "${CYAN}[8/8] Deploying process under PM2 Control Panel...${NC}"
cd "$INSTALL_DIR"
# Stop existing PM2 process if already running to prevent duplicates
"$PM2_CMD" delete "$APP_NAME" >/dev/null 2>&1 || true
# Forcefully terminate any duplicate or zombie process occupying the port (excluding ourselves and our parent shell)
echo -e "Ensuring port $PORT is free from any duplicate/zombie processes..."
# Find all processes holding the port using lsof, ss, or netstat
PIDS_ON_PORT=""
if command -v lsof >/dev/null 2>&1; then
PIDS_ON_PORT=$(lsof -t -i:$PORT 2>/dev/null || true)
else
# Fallback to ss or netstat
PIDS_ON_PORT=$(ss -lptn "sport = :$PORT" 2>/dev/null | grep -oP 'pid=\K[0-9]+' || netstat -nlp 2>/dev/null | grep ":$PORT " | awk '{print $7}' | cut -d'/' -f1 || true)
fi
# Filter out our own PID ($$) and our parent PID ($PPID) to prevent suicide
PID_TO_KILL=""
for pid in $PIDS_ON_PORT; do
clean_pid=$(echo "$pid" | grep -oE '[0-9]+' || true)
if [ -n "$clean_pid" ]; then
if [ "$clean_pid" -ne "$$" ] && [ "$clean_pid" -ne "$PPID" ]; then
PID_TO_KILL="$PID_TO_KILL $clean_pid"
fi
fi
done
if [ -n "$PID_TO_KILL" ]; then
echo -e "Found duplicate processes on port $PORT (PIDs: $PID_TO_KILL). Terminating..."
kill -9 $PID_TO_KILL >/dev/null 2>&1 || true
else
echo -e "Port $PORT is clean. No duplicate processes found."
fi
# Start server under PM2 with exponential backoff delay on crash/restart loops
PORT="$PORT" "$PM2_CMD" start server.js --name "$APP_NAME" --max-memory-restart 500M --exp-backoff-restart-delay 1000
# Save PM2 process list to persist across reboots
"$PM2_CMD" save
# Setup PM2 Startup script
echo -e "Generating PM2 system-level startup config..."
"$PM2_CMD" startup systemd | tail -n 1 | bash || true
# Configure systemd to automatically restart the PM2 daemon if it gets killed
echo -e "Patching PM2 systemd service to auto-restart when killed..."
for service in /etc/systemd/system/pm2-*.service; do
if [ -f "$service" ]; then
if ! grep -q "Restart=" "$service"; then
sed -i '/\[Service\]/a Restart=always\nRestartSec=10' "$service"
echo -e "Service $(basename $service) configured to Restart=always when killed."
systemctl daemon-reload || true
service_name=$(basename "$service" .service)
systemctl restart "$service_name" || true
fi
fi
done
echo -e "${CYAN}=================================================================${NC}"
echo -e "${GREEN} PM2 BACKEND DEPLOYED SUCCESSFULLY! ${NC}"
echo -e "${CYAN}=================================================================${NC}"
echo -e "Process Name: ${GREEN}$APP_NAME${NC}"
echo -e "Install Path: ${GREEN}$INSTALL_DIR${NC}"
echo -e "Server Port: ${GREEN}$PORT${NC}"
echo -e ""
echo -e "Manage your application using the following PM2 commands:"
echo -e " - View Live Logs: ${GREEN}pm2 logs $APP_NAME${NC}"
echo -e " - View Process Status: ${GREEN}pm2 status${NC}"
echo -e " - Restart Service: ${GREEN}pm2 restart $APP_NAME${NC}"
echo -e " - Stop Service: ${GREEN}pm2 stop $APP_NAME${NC}"
echo -e "${CYAN}=================================================================${NC}"