242 lines
10 KiB
Bash
242 lines
10 KiB
Bash
#!/bin/bash
|
|
# =============================================================
|
|
# Deploy script — 3M OMG Portal
|
|
#
|
|
# Architecture:
|
|
# Internet → Load Balancer (SSL termination) → 10.220.168.7:80
|
|
# Apache (port 80) → reverse proxy → Node.js (port 3000)
|
|
#
|
|
# Prerequisites (do these manually before running this script):
|
|
# Upload new code to the server (rsync or git pull), then:
|
|
#
|
|
# Usage:
|
|
# sudo bash /opt/3m-portal/deploy.sh # first-time install
|
|
# sudo bash /opt/3m-portal/deploy.sh --update # reload after code update
|
|
# =============================================================
|
|
set -euo pipefail
|
|
|
|
APP_DIR="/opt/3m-portal"
|
|
NODE_PORT=3000
|
|
DOMAIN="3m.automation.oliver.solutions"
|
|
APP_USER="nodeapp"
|
|
APACHE_CONF="/etc/apache2/sites-available/${DOMAIN}.conf"
|
|
|
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
|
|
info() { echo -e "${GREEN}[✓]${NC} $1"; }
|
|
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
|
|
error() { echo -e "${RED}[✗]${NC} $1"; exit 1; }
|
|
section() { echo -e "\n${CYAN}══ $1 ══${NC}"; }
|
|
|
|
UPDATE_ONLY=false
|
|
[[ "${1:-}" == "--update" ]] && UPDATE_ONLY=true
|
|
|
|
# ── 0. Checks ─────────────────────────────────────────────────
|
|
[[ $EUID -ne 0 ]] && error "Run as root: sudo bash deploy.sh"
|
|
[[ ! -f "$APP_DIR/server.js" ]] && error "Code not found at $APP_DIR. Upload the code first."
|
|
[[ ! -d "$APP_DIR/lib" ]] && error "lib/ directory missing — make sure the full codebase is uploaded."
|
|
|
|
# ── 1. Node.js + build tools ──────────────────────────────────
|
|
if ! $UPDATE_ONLY; then
|
|
section "Node.js"
|
|
if ! command -v node &>/dev/null; then
|
|
info "Installing Node.js 20.x..."
|
|
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
|
|
apt-get install -y nodejs
|
|
fi
|
|
info "Node.js $(node -v), npm $(npm -v)"
|
|
|
|
section "Build tools (required for better-sqlite3 native module)"
|
|
apt-get install -y build-essential python3 make g++
|
|
info "Build tools installed"
|
|
fi
|
|
|
|
# ── 2. PM2 ────────────────────────────────────────────────────
|
|
if ! $UPDATE_ONLY; then
|
|
section "PM2"
|
|
if ! command -v pm2 &>/dev/null; then
|
|
info "Installing PM2..."
|
|
npm install -g pm2
|
|
fi
|
|
info "PM2 $(pm2 -v)"
|
|
fi
|
|
|
|
# ── 3. Dedicated app user ─────────────────────────────────────
|
|
if ! $UPDATE_ONLY; then
|
|
section "App user"
|
|
if ! id "$APP_USER" &>/dev/null; then
|
|
useradd --system --shell /bin/false --home "$APP_DIR" "$APP_USER"
|
|
info "Created user: $APP_USER"
|
|
else
|
|
info "User $APP_USER already exists"
|
|
fi
|
|
fi
|
|
|
|
# ── 3a. Data directory ────────────────────────────────────────
|
|
section "Data directory"
|
|
mkdir -p "$APP_DIR/data"
|
|
chown "$APP_USER":"$APP_USER" "$APP_DIR/data"
|
|
chmod 700 "$APP_DIR/data"
|
|
info "Data directory: $APP_DIR/data (owner: $APP_USER, mode: 700)"
|
|
|
|
# ── 4. .env ───────────────────────────────────────────────────
|
|
section ".env"
|
|
ENV_FILE="$APP_DIR/.env"
|
|
if [ ! -f "$ENV_FILE" ]; then
|
|
warn ".env not found — creating template. Fill in all CHANGE_ME values!"
|
|
cat > "$ENV_FILE" << EOF
|
|
SERVICE_USERNAME=portal@oliver.agency
|
|
SERVICE_PASSWORD=CHANGE_ME
|
|
PORT=${NODE_PORT}
|
|
APP_BASE_URL=https://${DOMAIN}
|
|
DATA_DIR=${APP_DIR}/data
|
|
COOKIE_NAME=portal_session
|
|
COOKIE_SECURE=true
|
|
SESSION_TTL_MS=28800000
|
|
INITIAL_ADMINS=[{"email":"CHANGE_ME","one2editUsername":"CHANGE_ME","password":"CHANGE_ME"}]
|
|
MAILGUN_API_KEY=CHANGE_ME
|
|
MAILGUN_DOMAIN=mg.oliver.solutions
|
|
MAILGUN_FROM=noreply@mg.oliver.solutions
|
|
EOF
|
|
warn "──────────────────────────────────────────────────"
|
|
warn "Edit all values now: nano $ENV_FILE"
|
|
warn "──────────────────────────────────────────────────"
|
|
read -rp "Press Enter after updating .env to continue..."
|
|
else
|
|
info ".env exists"
|
|
fi
|
|
|
|
if grep -q "CHANGE_ME" "$ENV_FILE"; then
|
|
error ".env still has CHANGE_ME values. Edit $ENV_FILE first."
|
|
fi
|
|
|
|
# ── 5. Permissions ────────────────────────────────────────────
|
|
section "Permissions"
|
|
# Own all app files as nodeapp, then lock down .env and data
|
|
chown -R "$APP_USER":"$APP_USER" "$APP_DIR"
|
|
chmod 600 "$ENV_FILE"
|
|
# Re-apply data dir restriction (chown -R sets ownership but not mode)
|
|
chmod 700 "$APP_DIR/data"
|
|
info "Owner: $APP_USER, .env: 600, data/: 700"
|
|
|
|
# ── 5a. npm install ───────────────────────────────────────────
|
|
section "npm install"
|
|
cd "$APP_DIR" && HOME="$APP_DIR" sudo -u "$APP_USER" npm install --omit=dev
|
|
info "Dependencies installed (including better-sqlite3 native build)"
|
|
|
|
# ── 6. PM2 process ────────────────────────────────────────────
|
|
section "PM2 process"
|
|
if $UPDATE_ONLY; then
|
|
# Reload picks up new code + fresh .env without dropping existing sessions
|
|
if pm2 describe 3m-portal &>/dev/null; then
|
|
pm2 reload 3m-portal --update-env
|
|
info "PM2 reloaded with updated env"
|
|
else
|
|
warn "PM2 process not found — starting fresh"
|
|
pm2 start "$APP_DIR/server.js" \
|
|
--name 3m-portal \
|
|
--user "$APP_USER" \
|
|
--log /var/log/3m-portal.log \
|
|
--restart-delay 3000 \
|
|
--max-restarts 10
|
|
fi
|
|
else
|
|
pm2 stop 3m-portal 2>/dev/null || true
|
|
pm2 delete 3m-portal 2>/dev/null || true
|
|
|
|
pm2 start "$APP_DIR/server.js" \
|
|
--name 3m-portal \
|
|
--user "$APP_USER" \
|
|
--log /var/log/3m-portal.log \
|
|
--restart-delay 3000 \
|
|
--max-restarts 10
|
|
|
|
env PATH="$PATH:/usr/bin" pm2 startup systemd -u "$APP_USER" --hp "$APP_DIR" \
|
|
| tail -1 | bash || warn "Run 'pm2 startup' manually if autostart is needed"
|
|
fi
|
|
|
|
pm2 save
|
|
info "PM2 state saved"
|
|
|
|
# ── 7. Apache modules ─────────────────────────────────────────
|
|
if ! $UPDATE_ONLY; then
|
|
section "Apache modules"
|
|
a2enmod proxy proxy_http headers rewrite 2>/dev/null || true
|
|
info "Modules enabled (proxy, proxy_http, headers, rewrite)"
|
|
fi
|
|
|
|
# ── 8. Apache virtual host ────────────────────────────────────
|
|
if ! $UPDATE_ONLY; then
|
|
section "Apache vhost"
|
|
|
|
if [ -f "$APACHE_CONF" ]; then
|
|
cp "$APACHE_CONF" "${APACHE_CONF}.bak.$(date +%Y%m%d%H%M%S)"
|
|
info "Backed up existing config"
|
|
fi
|
|
|
|
cat > "$APACHE_CONF" << EOF
|
|
# 3M OMG Portal — Apache reverse proxy to Node.js
|
|
#
|
|
# SSL is terminated at the load balancer.
|
|
# This server receives plain HTTP on port 80 from the LB
|
|
# and proxies it to Node.js on port ${NODE_PORT}.
|
|
|
|
<VirtualHost *:80>
|
|
ServerName ${DOMAIN}
|
|
|
|
ProxyPreserveHost On
|
|
ProxyPass / http://127.0.0.1:${NODE_PORT}/
|
|
ProxyPassReverse / http://127.0.0.1:${NODE_PORT}/
|
|
|
|
RequestHeader set X-Forwarded-Proto "https"
|
|
|
|
Header always set X-Content-Type-Options "nosniff"
|
|
Header always set X-Frame-Options "SAMEORIGIN"
|
|
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
|
|
|
ErrorLog /var/log/apache2/${DOMAIN}-error.log
|
|
CustomLog /var/log/apache2/${DOMAIN}-access.log combined
|
|
</VirtualHost>
|
|
EOF
|
|
|
|
info "Apache config written: $APACHE_CONF"
|
|
|
|
# ── 9. Enable site & reload Apache ────────────────────────────
|
|
section "Apache reload"
|
|
a2ensite "${DOMAIN}.conf" 2>/dev/null || true
|
|
|
|
if apache2ctl configtest 2>&1; then
|
|
systemctl reload apache2
|
|
info "Apache reloaded"
|
|
else
|
|
error "Apache config test failed — check $APACHE_CONF"
|
|
fi
|
|
fi
|
|
|
|
# ── 10. Smoke test ────────────────────────────────────────────
|
|
section "Smoke test"
|
|
sleep 2
|
|
if curl -sf "http://127.0.0.1:${NODE_PORT}/" -o /dev/null; then
|
|
info "Node.js responding on port ${NODE_PORT}"
|
|
else
|
|
error "Node.js not responding. Check logs: pm2 logs 3m-portal"
|
|
fi
|
|
|
|
if ! $UPDATE_ONLY; then
|
|
if curl -sf "http://127.0.0.1/" -o /dev/null; then
|
|
info "Apache proxy responding on port 80"
|
|
else
|
|
warn "Apache not responding on port 80 — check: systemctl status apache2"
|
|
fi
|
|
fi
|
|
|
|
# ── Done ──────────────────────────────────────────────────────
|
|
echo ""
|
|
echo -e "${GREEN}╔══════════════════════════════════════════════════════╗${NC}"
|
|
echo -e "${GREEN}║ Deploy complete! ║${NC}"
|
|
echo -e "${GREEN}║ ║${NC}"
|
|
echo -e "${GREEN}║ Portal: https://${DOMAIN} ║${NC}"
|
|
echo -e "${GREEN}║ Admin: https://${DOMAIN}/admin.html ║${NC}"
|
|
echo -e "${GREEN}║ Logs: pm2 logs 3m-portal ║${NC}"
|
|
echo -e "${GREEN}║ DB: sqlite3 ${APP_DIR}/data/portal.db ║${NC}"
|
|
echo -e "${GREEN}║ Update: upload code → sudo bash deploy.sh --update ║${NC}"
|
|
echo -e "${GREEN}╚══════════════════════════════════════════════════════╝${NC}"
|