Compose derives project name from parent dir by default — every app on this server landed on project "deploy", sharing container names and volume namespaces. Another app on the box just lost 2 days of data from this exact issue. Fix: name: salary-benchmark in docker-compose.prod.yml, plus -p on every docker compose call in deploy-local.sh and deploy.sh. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
142 lines
5.3 KiB
Bash
Executable file
142 lines
5.3 KiB
Bash
Executable file
#!/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}" <<ENV
|
|
# Generated by deploy-local.sh — do not commit
|
|
APP_PORT=${APP_PORT}
|
|
|
|
DB_USER=salary_user
|
|
DB_PASSWORD=${DB_PASSWORD}
|
|
DB_HOST=db
|
|
DB_PORT=5432
|
|
DB_NAME=salary_benchmark
|
|
|
|
JWT_SECRET=${JWT_SECRET}
|
|
JWT_ALGORITHM=HS256
|
|
JWT_EXPIRES_MINUTES=480
|
|
|
|
SERPER_API_KEY=${SERPER_API_KEY}
|
|
FIRECRAWL_API_KEY=${FIRECRAWL_API_KEY}
|
|
COHERE_API_KEY=${COHERE_API_KEY}
|
|
ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
|
ENV
|
|
chmod 600 "${ENVF}"
|
|
info "wrote ${REPO_ROOT}/${ENVF}"
|
|
|
|
# ----------------------------------------------------------------------------
|
|
step "Build & start containers (project=${COMPOSE_PROJECT})"
|
|
cd "${REPO_ROOT}/deploy"
|
|
${DC} build
|
|
${DC} up -d
|
|
${DC} ps
|
|
cd "${REPO_ROOT}"
|
|
|
|
# ----------------------------------------------------------------------------
|
|
step "Install Apache fragment"
|
|
FRAG_DST="${REPO_ROOT}/deploy/apache-salary-benchmark.conf"
|
|
sed "s|__APP_PORT__|${APP_PORT}|g" \
|
|
"${REPO_ROOT}/deploy/apache-salary-benchmark.conf.template" > "${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|</VirtualHost>|${INCLUDE_LINE}\n</VirtualHost>|" "${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)"
|