deploy: auto-pick free ports + render Apache conf from template
The script bailed when 8003 was taken on the dev server. Per spec, it
should never block on a port clash — find a free port and run with it.
How it picks ports:
- Reads OSOP_DB_PORT / OSOP_REDIS_PORT / OSOP_BACKEND_PORT from .env,
falling back to defaults 5435 / 6380 / 8003.
- For each, if the preferred port is taken on the host, scans upward in
a sane range (5435-5499 / 6380-6399 / 8003-8099) for the next free one.
- Persists chosen ports back to .env via an idempotent KEY=VALUE upsert,
so subsequent deploys keep using the same allocation.
- If our compose project is already running, skips the scan and reuses
the current ports (re-deploy in place).
Compose port mappings now reference those env vars with defaults:
127.0.0.1:${OSOP_DB_PORT:-5435}:5432, etc.
Apache config templating:
- deploy/apache-osop.conf.tmpl has __BACKEND_PORT__ placeholder.
- The script renders it to deploy/apache-osop.conf each run with the
chosen backend port substituted in. The rendered file is gitignored
(the template is the source of truth in git).
- If the backend port changed (or the Apache vhost doesn't yet Include
our conf), the script tells the user to reload Apache.
This means a fresh server hits the conflict on 8003 (something else is
listening), the script picks 8004 silently, writes it to .env, renders
apache-osop.conf with 8004, brings the stack up, and tells you to
reload Apache. Re-running the script on the same server keeps 8004.
This commit is contained in:
parent
0734afedac
commit
ed0746267b
4 changed files with 157 additions and 78 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -2,6 +2,11 @@
|
|||
node_modules/
|
||||
dist/
|
||||
|
||||
# Generated by deploy/deploy.sh from apache-osop.conf.tmpl with the
|
||||
# server's actual backend port substituted in. The template is the
|
||||
# source of truth.
|
||||
deploy/apache-osop.conf
|
||||
|
||||
# Compiled Python bytecode
|
||||
*.py[cod]
|
||||
__pycache__/
|
||||
|
|
|
|||
|
|
@ -1,25 +1,26 @@
|
|||
# OLIVER Sales Ops Platform — Apache reverse-proxy block
|
||||
# OLIVER Sales Ops Platform — Apache reverse-proxy block (template)
|
||||
#
|
||||
# This file is the template. The actual `apache-osop.conf` next to it is
|
||||
# generated by `deploy/deploy.sh` on every run, with the chosen backend
|
||||
# port substituted in. apache-osop.conf is gitignored.
|
||||
#
|
||||
# Drop into the merged optical-dev.oliver.solutions vhost via Include:
|
||||
# Include /opt/oliver-sales-ops-platform/deploy/apache-osop.conf
|
||||
#
|
||||
# Mirrors the /gsb/ pattern on the same server:
|
||||
# - Backend FastAPI runs in Docker on 127.0.0.1:8003 (db 5435, redis 6380).
|
||||
# - Frontend is a Vite static build served straight off the filesystem
|
||||
# - Backend FastAPI runs in Docker on 127.0.0.1:__BACKEND_PORT__
|
||||
# - Frontend is a Vite static build, served straight off the filesystem
|
||||
# by Apache from /var/www/html/oliver-sales-ops-platform/.
|
||||
#
|
||||
# Public URL:
|
||||
# https://optical-dev.oliver.solutions/oliver-sales-ops-platform/
|
||||
#
|
||||
# 5-minute timeout matches the /gsb/ block — the matching agent and
|
||||
# deep extraction can run for ~60-180s on large RFPs.
|
||||
|
||||
ProxyTimeout 300
|
||||
TimeOut 300
|
||||
|
||||
# Backend API
|
||||
ProxyPass /oliver-sales-ops-platform/api/ http://127.0.0.1:8003/api/ timeout=300
|
||||
ProxyPassReverse /oliver-sales-ops-platform/api/ http://127.0.0.1:8003/api/
|
||||
ProxyPass /oliver-sales-ops-platform/api/ http://127.0.0.1:__BACKEND_PORT__/api/ timeout=300
|
||||
ProxyPassReverse /oliver-sales-ops-platform/api/ http://127.0.0.1:__BACKEND_PORT__/api/
|
||||
|
||||
# Frontend SPA (built artefacts)
|
||||
Alias /oliver-sales-ops-platform /var/www/html/oliver-sales-ops-platform
|
||||
205
deploy/deploy.sh
205
deploy/deploy.sh
|
|
@ -9,18 +9,23 @@
|
|||
# /var/www/html/oliver-sales-ops-platform/ — built SPA, served by Apache
|
||||
#
|
||||
# What it does:
|
||||
# 1. Sanity check (.env, docker, git on PATH).
|
||||
# 2. Port-conflict check on backend host ports (5435 db, 6380 redis,
|
||||
# 8003 backend). If our compose project is already running, those
|
||||
# ports are ours and re-deploying in place is fine; otherwise we
|
||||
# verify they're free.
|
||||
# 3. git pull (--no-pull to skip).
|
||||
# 4. docker compose build && up -d (--no-build to skip).
|
||||
# 5. Build frontend SPA (Vite) inside a one-shot node container and
|
||||
# sync the dist/ output to /var/www/html/oliver-sales-ops-platform/
|
||||
# 1. Sanity (.env, docker, git on PATH).
|
||||
# 2. Auto-pick free host ports:
|
||||
# - prefers OSOP_DB_PORT (default 5435)
|
||||
# - prefers OSOP_REDIS_PORT (default 6380)
|
||||
# - prefers OSOP_BACKEND_PORT (default 8003)
|
||||
# If any preferred port is taken on the host, scans upward in its
|
||||
# range for the next free port and persists the chosen value to
|
||||
# .env so subsequent deploys keep using it.
|
||||
# 3. Render deploy/apache-osop.conf from .tmpl with the chosen
|
||||
# backend port.
|
||||
# 4. git pull (--no-pull to skip).
|
||||
# 5. docker compose build && up -d (--no-build to skip).
|
||||
# 6. Build frontend SPA (Vite) inside a one-shot node container and
|
||||
# sync dist/ to /var/www/html/oliver-sales-ops-platform/
|
||||
# (--no-frontend to skip).
|
||||
# 6. Poll /api/health until ready, probe the SPA index.
|
||||
# 7. Print URLs + admin email.
|
||||
# 7. Poll /api/health until ready.
|
||||
# 8. Print URLs + admin email + apache-reload reminder.
|
||||
#
|
||||
# Usage:
|
||||
# ./deploy/deploy.sh # full deploy
|
||||
|
|
@ -39,9 +44,10 @@ WEB_ROOT="/var/www/html/oliver-sales-ops-platform"
|
|||
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
log() { printf '\033[1;36m[deploy]\033[0m %s\n' "$*"; }
|
||||
err() { printf '\033[1;31m[deploy]\033[0m %s\n' "$*" >&2; }
|
||||
ok() { printf '\033[1;32m[deploy]\033[0m %s\n' "$*"; }
|
||||
log() { printf '\033[1;36m[deploy]\033[0m %s\n' "$*"; }
|
||||
err() { printf '\033[1;31m[deploy]\033[0m %s\n' "$*" >&2; }
|
||||
ok() { printf '\033[1;32m[deploy]\033[0m %s\n' "$*"; }
|
||||
warn() { printf '\033[1;33m[deploy]\033[0m %s\n' "$*"; }
|
||||
|
||||
DO_PULL=1
|
||||
DO_BUILD=1
|
||||
|
|
@ -79,59 +85,121 @@ command -v git >/dev/null 2>&1 || { err "git not on PATH"; exit 1; }
|
|||
# serves the SPA off the filesystem; the Vite container is local-dev-only.
|
||||
unset COMPOSE_PROFILES
|
||||
|
||||
# 2. Port-conflict check (backend ports only — frontend is filesystem on prod)
|
||||
PORTS=(5435 6380 8003)
|
||||
log "Checking host ports: ${PORTS[*]}"
|
||||
# ---------- helpers ----------
|
||||
|
||||
# True (0) if something else is listening on $1 on the host.
|
||||
port_in_use() {
|
||||
local port=$1
|
||||
local pid=""
|
||||
if command -v lsof >/dev/null 2>&1; then
|
||||
pid=$( { lsof -nP -iTCP:"$port" -sTCP:LISTEN 2>/dev/null || true; } | awk 'NR>1 {print $2}' | head -1 )
|
||||
else
|
||||
pid=$( { ss -ltnp "sport = :$port" 2>/dev/null || true; } | awk -F'pid=' 'NR>1 {print $2}' | cut -d, -f1 | head -1 )
|
||||
fi
|
||||
[[ -n "$pid" ]]
|
||||
}
|
||||
|
||||
# Find the first free port. Tries $preferred first, then scans [start..end].
|
||||
find_free_port() {
|
||||
local preferred=$1
|
||||
local start=$2
|
||||
local end=$3
|
||||
if ! port_in_use "$preferred"; then
|
||||
printf '%s' "$preferred"
|
||||
return 0
|
||||
fi
|
||||
local p
|
||||
for ((p=start; p<=end; p++)); do
|
||||
if ! port_in_use "$p"; then
|
||||
printf '%s' "$p"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Idempotent KEY=VALUE upsert into .env.
|
||||
set_env_var() {
|
||||
local key=$1
|
||||
local value=$2
|
||||
local file="${REPO_ROOT}/.env"
|
||||
if grep -q "^${key}=" "$file" 2>/dev/null; then
|
||||
sed -i.bak "s#^${key}=.*#${key}=${value}#" "$file"
|
||||
rm -f "${file}.bak"
|
||||
else
|
||||
printf '%s=%s\n' "$key" "$value" >> "$file"
|
||||
fi
|
||||
}
|
||||
|
||||
# Echo current value of KEY from .env (empty if absent).
|
||||
get_env_var() {
|
||||
grep -E "^${1}=" "${REPO_ROOT}/.env" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '"' || true
|
||||
}
|
||||
|
||||
# ---------- 2. Pick ports ----------
|
||||
|
||||
DEFAULT_DB_PORT=5435
|
||||
DEFAULT_REDIS_PORT=6380
|
||||
DEFAULT_BACKEND_PORT=8003
|
||||
|
||||
DB_PORT=$(get_env_var OSOP_DB_PORT); DB_PORT=${DB_PORT:-$DEFAULT_DB_PORT}
|
||||
REDIS_PORT=$(get_env_var OSOP_REDIS_PORT); REDIS_PORT=${REDIS_PORT:-$DEFAULT_REDIS_PORT}
|
||||
BACKEND_PORT=$(get_env_var OSOP_BACKEND_PORT); BACKEND_PORT=${BACKEND_PORT:-$DEFAULT_BACKEND_PORT}
|
||||
|
||||
PREV_BACKEND_PORT="$BACKEND_PORT"
|
||||
|
||||
log "Resolving host ports (preferred: db=$DB_PORT redis=$REDIS_PORT backend=$BACKEND_PORT)…"
|
||||
|
||||
RUNNING=$(docker compose ps -q 2>/dev/null | wc -l | tr -d ' ')
|
||||
if [[ "$RUNNING" -gt 0 ]]; then
|
||||
ok "Project '$COMPOSE_PROJECT' already has $RUNNING containers running — ports are ours, re-deploy in place."
|
||||
ok "Project '$COMPOSE_PROJECT' already has $RUNNING containers running — keeping current port assignment."
|
||||
else
|
||||
# NB: lsof / ss exit non-zero when nothing is listening on a port. Under
|
||||
# `set -euo pipefail` that propagated as a fatal error and the script
|
||||
# exited silently right after "Checking host ports". Wrap with `|| true`.
|
||||
port_owner_pid() {
|
||||
local port=$1
|
||||
local pid=""
|
||||
if command -v lsof >/dev/null 2>&1; then
|
||||
pid=$( { lsof -nP -iTCP:"$port" -sTCP:LISTEN 2>/dev/null || true; } | awk 'NR>1 {print $2}' | head -1 )
|
||||
else
|
||||
pid=$( { ss -ltnp "sport = :$port" 2>/dev/null || true; } | awk -F'pid=' 'NR>1 {print $2}' | cut -d, -f1 | head -1 )
|
||||
fi
|
||||
printf '%s' "$pid"
|
||||
}
|
||||
|
||||
CONFLICTS=()
|
||||
for port in "${PORTS[@]}"; do
|
||||
pid=$(port_owner_pid "$port") || true
|
||||
if [[ -n "$pid" ]]; then
|
||||
CONFLICTS+=("$port (pid $pid)")
|
||||
else
|
||||
ok " port $port — free"
|
||||
fi
|
||||
done
|
||||
|
||||
if (( ${#CONFLICTS[@]} > 0 )); then
|
||||
err "Port conflicts detected (something else is listening on the host):"
|
||||
for c in "${CONFLICTS[@]}"; do err " $c"; done
|
||||
err
|
||||
err "Options:"
|
||||
err " 1. Stop the conflicting service."
|
||||
err " 2. Change the host port mapping in docker-compose.yml AND update"
|
||||
err " deploy/apache-osop.conf to match."
|
||||
err " 3. If those ports are owned by docker-proxy from a stale stack,"
|
||||
err " run: docker compose -p $COMPOSE_PROJECT down then re-run."
|
||||
NEW_DB_PORT=$(find_free_port "$DB_PORT" 5435 5499) || NEW_DB_PORT=""
|
||||
NEW_REDIS_PORT=$(find_free_port "$REDIS_PORT" 6380 6399) || NEW_REDIS_PORT=""
|
||||
NEW_BACKEND_PORT=$(find_free_port "$BACKEND_PORT" 8003 8099) || NEW_BACKEND_PORT=""
|
||||
if [[ -z "$NEW_DB_PORT" || -z "$NEW_REDIS_PORT" || -z "$NEW_BACKEND_PORT" ]]; then
|
||||
err "Could not find a free port in the configured ranges."
|
||||
err " db desired=$DB_PORT scanned=5435-5499"
|
||||
err " redis desired=$REDIS_PORT scanned=6380-6399"
|
||||
err " backend desired=$BACKEND_PORT scanned=8003-8099"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
[[ "$NEW_DB_PORT" != "$DB_PORT" ]] && warn "db port $DB_PORT busy → using $NEW_DB_PORT"
|
||||
[[ "$NEW_REDIS_PORT" != "$REDIS_PORT" ]] && warn "redis port $REDIS_PORT busy → using $NEW_REDIS_PORT"
|
||||
[[ "$NEW_BACKEND_PORT" != "$BACKEND_PORT" ]] && warn "backend port $BACKEND_PORT busy → using $NEW_BACKEND_PORT"
|
||||
|
||||
DB_PORT=$NEW_DB_PORT
|
||||
REDIS_PORT=$NEW_REDIS_PORT
|
||||
BACKEND_PORT=$NEW_BACKEND_PORT
|
||||
|
||||
set_env_var OSOP_DB_PORT "$DB_PORT"
|
||||
set_env_var OSOP_REDIS_PORT "$REDIS_PORT"
|
||||
set_env_var OSOP_BACKEND_PORT "$BACKEND_PORT"
|
||||
|
||||
ok "Ports: db=$DB_PORT redis=$REDIS_PORT backend=$BACKEND_PORT (persisted to .env)"
|
||||
fi
|
||||
|
||||
# 3. git pull
|
||||
# ---------- 3. Render apache-osop.conf from template ----------
|
||||
|
||||
APACHE_TMPL="$REPO_ROOT/deploy/apache-osop.conf.tmpl"
|
||||
APACHE_CONF="$REPO_ROOT/deploy/apache-osop.conf"
|
||||
if [[ -f "$APACHE_TMPL" ]]; then
|
||||
sed "s#__BACKEND_PORT__#${BACKEND_PORT}#g" "$APACHE_TMPL" > "$APACHE_CONF"
|
||||
ok "Rendered apache-osop.conf with backend port $BACKEND_PORT"
|
||||
else
|
||||
warn "apache-osop.conf.tmpl missing — leaving deploy/apache-osop.conf untouched."
|
||||
fi
|
||||
|
||||
# ---------- 4. git pull ----------
|
||||
|
||||
if (( DO_PULL )); then
|
||||
log "git pull origin main"
|
||||
git pull --ff-only origin main
|
||||
fi
|
||||
|
||||
# 4. Backend build + up
|
||||
# ---------- 5. Backend build + up ----------
|
||||
|
||||
if (( DO_BUILD )); then
|
||||
log "docker compose build"
|
||||
docker compose build
|
||||
|
|
@ -140,7 +208,8 @@ fi
|
|||
log "docker compose up -d (db + redis + backend)"
|
||||
docker compose up -d
|
||||
|
||||
# 5. Frontend build + sync to web root
|
||||
# ---------- 6. Frontend build + sync ----------
|
||||
|
||||
if (( DO_FRONTEND )); then
|
||||
log "Building Vite SPA in a one-shot node:20 container…"
|
||||
docker run --rm \
|
||||
|
|
@ -163,7 +232,6 @@ if (( DO_FRONTEND )); then
|
|||
fi
|
||||
fi
|
||||
|
||||
# rsync if available (atomic-ish, only changes); else cp -a.
|
||||
if command -v rsync >/dev/null 2>&1; then
|
||||
if [[ -w "$WEB_ROOT" ]]; then
|
||||
rsync -a --delete "$REPO_ROOT/frontend/dist/" "$WEB_ROOT/"
|
||||
|
|
@ -182,10 +250,11 @@ if (( DO_FRONTEND )); then
|
|||
ok "SPA synced to $WEB_ROOT"
|
||||
fi
|
||||
|
||||
# 6. Health poll
|
||||
log "Waiting for backend /api/health (max 60s)…"
|
||||
# ---------- 7. Health poll ----------
|
||||
|
||||
log "Waiting for backend /api/health on :$BACKEND_PORT (max 60s)…"
|
||||
for i in $(seq 1 30); do
|
||||
if curl -fsS http://127.0.0.1:8003/api/health >/dev/null 2>&1; then
|
||||
if curl -fsS "http://127.0.0.1:${BACKEND_PORT}/api/health" >/dev/null 2>&1; then
|
||||
ok "Backend healthy"
|
||||
break
|
||||
fi
|
||||
|
|
@ -197,21 +266,25 @@ for i in $(seq 1 30); do
|
|||
fi
|
||||
done
|
||||
|
||||
# 7. Report
|
||||
ADMIN_EMAIL=$(grep -E '^DEV_AUTH_EMAIL=' .env 2>/dev/null | cut -d= -f2- | tr -d '"' || echo "admin@oliver.agency")
|
||||
PUBLIC_URL=$(grep -E '^APP_PUBLIC_URL=' .env 2>/dev/null | cut -d= -f2- | tr -d '"' || echo "")
|
||||
# ---------- 8. Report ----------
|
||||
|
||||
ADMIN_EMAIL=$(get_env_var DEV_AUTH_EMAIL); ADMIN_EMAIL=${ADMIN_EMAIL:-admin@oliver.agency}
|
||||
PUBLIC_URL=$(get_env_var APP_PUBLIC_URL)
|
||||
ok "Deploy complete."
|
||||
echo
|
||||
echo " Backend (local): http://127.0.0.1:8003/api/health"
|
||||
echo " Backend (local): http://127.0.0.1:${BACKEND_PORT}/api/health"
|
||||
[[ -n "$PUBLIC_URL" ]] && echo " Public URL: ${PUBLIC_URL%/}${URL_PATH}/"
|
||||
[[ -d "$WEB_ROOT" ]] && echo " SPA on disk: $WEB_ROOT"
|
||||
echo " Admin user: $ADMIN_EMAIL (role=admin, no password — DEV_AUTH_BYPASS=true)"
|
||||
echo " Ports: db=$DB_PORT redis=$REDIS_PORT backend=$BACKEND_PORT"
|
||||
echo
|
||||
echo " Apache include line for the merged vhost:"
|
||||
echo " Include $REPO_ROOT/deploy/apache-osop.conf"
|
||||
echo
|
||||
echo " After adding the Include and reloading Apache:"
|
||||
echo " sudo apachectl configtest && sudo systemctl reload apache2"
|
||||
if [[ "$BACKEND_PORT" != "$PREV_BACKEND_PORT" ]] || ! grep -qF "$REPO_ROOT/deploy/apache-osop.conf" /etc/apache2/sites-enabled/*.conf 2>/dev/null; then
|
||||
echo
|
||||
warn "Backend port changed (or first deploy). Reload Apache to pick up the new ProxyPass:"
|
||||
echo " sudo apachectl configtest && sudo systemctl reload apache2"
|
||||
fi
|
||||
echo
|
||||
|
||||
if (( TAIL_LOGS )); then
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ services:
|
|||
volumes:
|
||||
- osop_pgdata:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "127.0.0.1:5435:5432"
|
||||
- "127.0.0.1:${OSOP_DB_PORT:-5435}:5432"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U osop_user -d oliver_sales_ops"]
|
||||
interval: 5s
|
||||
|
|
@ -22,7 +22,7 @@ services:
|
|||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:6380:6379"
|
||||
- "127.0.0.1:${OSOP_REDIS_PORT:-6380}:6379"
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
|
|
@ -55,7 +55,7 @@ services:
|
|||
APP_PUBLIC_URL: ${APP_PUBLIC_URL:-http://localhost:3011}
|
||||
APP_PATH_PREFIX: ${APP_PATH_PREFIX:-/oliver-sales-ops-platform}
|
||||
ports:
|
||||
- "127.0.0.1:8003:8000"
|
||||
- "127.0.0.1:${OSOP_BACKEND_PORT:-8003}:8000"
|
||||
volumes:
|
||||
- ${DATA_DIR:-./data}:/app/data
|
||||
- ./backend/app:/app/app
|
||||
|
|
@ -75,7 +75,7 @@ services:
|
|||
environment:
|
||||
VITE_DEV_AUTH_BYPASS: ${VITE_DEV_AUTH_BYPASS:-}
|
||||
ports:
|
||||
- "127.0.0.1:3011:3000"
|
||||
- "127.0.0.1:${OSOP_FRONTEND_PORT:-3011}:3000"
|
||||
volumes:
|
||||
- ./frontend/src:/app/src
|
||||
- ./frontend/index.html:/app/index.html
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue