#!/usr/bin/env bash # Run this ON the server, from the cloned repo root (e.g. /opt/salary-benchmark). # Assumes you've already created .env with your API keys. Idempotent. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" URL_SUBPATH="${URL_SUBPATH:-/salary-benchmark/}" WEB_DIR="${WEB_DIR:-/var/www/html/salary-benchmark}" VHOST_FILE="${VHOST_FILE:-/etc/apache2/sites-enabled/optical-dev.oliver.solutions.conf}" PORT_SCAN_START="${PORT_SCAN_START:-8100}" PORT_SCAN_END="${PORT_SCAN_END:-8199}" # Pin compose project name. Defaulting to the parent dir name gives every app on # this server the same project ("deploy"), sharing container + volume namespaces # and causing cross-app data loss. Belt-and-braces with `name:` in the yaml. COMPOSE_PROJECT="salary-benchmark" DC="docker compose -p ${COMPOSE_PROJECT} -f docker-compose.prod.yml --env-file .env.prod" step() { printf "\n\033[1;33m==> %s\033[0m\n" "$*"; } info() { printf " %s\n" "$*"; } die() { printf "\033[1;31mERROR: %s\033[0m\n" "$*" >&2; exit 1; } [[ $EUID -eq 0 ]] || die "run as root (for apache + /var/www writes)" command -v docker >/dev/null || die "docker not installed" command -v apache2ctl >/dev/null || die "apache2ctl not found — is Apache installed?" [[ -e "${VHOST_FILE}" ]] || die "vhost file not found: ${VHOST_FILE}" # Follow symlink so we edit the real file (sites-enabled is often a symlink) VHOST_FILE="$(readlink -f "${VHOST_FILE}")" info "vhost: ${VHOST_FILE}" [[ -f "${REPO_ROOT}/.env" ]] || die ".env missing in ${REPO_ROOT} (copy .env.example and fill in API keys)" # ---------------------------------------------------------------------------- step "Pick free port in ${PORT_SCAN_START}-${PORT_SCAN_END}" USED=$(ss -tlnH 2>/dev/null | awk '{print $4}' | awk -F: '{print $NF}' | sort -u) APP_PORT="" for p in $(seq "${PORT_SCAN_START}" "${PORT_SCAN_END}"); do if ! grep -qx "${p}" <<<"${USED}"; then APP_PORT="${p}"; break; fi done [[ -n "${APP_PORT}" ]] || die "no free port in range" info "APP_PORT=${APP_PORT}" # ---------------------------------------------------------------------------- step "Build frontend (VITE_BASE=${URL_SUBPATH}) in a node container" docker run --rm \ -v "${REPO_ROOT}/frontend":/app \ -w /app \ -e VITE_BASE="${URL_SUBPATH}" \ node:20-alpine \ sh -c "npm ci --prefer-offline --no-audit --no-fund && npm run build" info "built ${REPO_ROOT}/frontend/dist/" # ---------------------------------------------------------------------------- step "Deploy frontend to ${WEB_DIR}" mkdir -p "${WEB_DIR}" rsync -a --delete "${REPO_ROOT}/frontend/dist/" "${WEB_DIR}/" # ---------------------------------------------------------------------------- step "Write .env.prod (preserves existing JWT_SECRET and DB_PASSWORD)" cd "${REPO_ROOT}" ENVF="deploy/.env.prod" EXISTING_JWT=""; EXISTING_DBPW="" if [[ -f "${ENVF}" ]]; then EXISTING_JWT=$(grep -E '^JWT_SECRET=' "${ENVF}" | cut -d= -f2- || true) EXISTING_DBPW=$(grep -E '^DB_PASSWORD=' "${ENVF}" | cut -d= -f2- || true) fi JWT_SECRET="${EXISTING_JWT:-$(openssl rand -hex 32)}" DB_PASSWORD="${EXISTING_DBPW:-$(openssl rand -hex 16)}" get_env() { grep -E "^${1}=" .env 2>/dev/null | head -1 | cut -d= -f2- || true; } SERPER_API_KEY=$(get_env SERPER_API_KEY) FIRECRAWL_API_KEY=$(get_env FIRECRAWL_API_KEY) COHERE_API_KEY=$(get_env COHERE_API_KEY) ANTHROPIC_API_KEY=$(get_env ANTHROPIC_API_KEY) cat > "${ENVF}" < "${FRAG_DST}" info "rendered ${FRAG_DST}" INCLUDE_LINE=" Include ${FRAG_DST}" if grep -Fq "${FRAG_DST}" "${VHOST_FILE}"; then info "Include already present in ${VHOST_FILE}" else cp -a "${VHOST_FILE}" "${VHOST_FILE}.bak.$(date +%s)" sed -i "0,/<\/VirtualHost>/s||${INCLUDE_LINE}\n|" "${VHOST_FILE}" info "added Include line to ${VHOST_FILE}" fi apache2ctl configtest systemctl reload apache2 info "Apache reloaded" # ---------------------------------------------------------------------------- step "Verify" sleep 3 HEALTH_URL="https://optical-dev.oliver.solutions${URL_SUBPATH}api/health" if curl -sfk "${HEALTH_URL}" >/dev/null; then info "OK: ${HEALTH_URL}" else info "WARN: health check failed (container may still be starting). Try:" info " (cd ${REPO_ROOT}/deploy && ${DC} logs app --tail 30)" fi step "Done" info "App: https://optical-dev.oliver.solutions${URL_SUBPATH}" info "Login: admin@oliver.agency / Oliver2026!" info "Port: 127.0.0.1:${APP_PORT} (Apache fronts it)"