- docker-compose: DATA_DIR env var controls data volume mount (defaults to ./data for local, /var/www/html/gmal-scope-builder/data on server) - deploy.sh: resolve DATA_DIR from .env, default to persistent web dir - deploy.sh: rm only frontend files from WEB_DIR, preserve data/ subdir - .env.example: document DATA_DIR variable Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
151 lines
7.9 KiB
Bash
Executable file
151 lines
7.9 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# deploy.sh — idempotent deploy script for GMAL Scope Builder
|
|
# Run from the repo root: sudo ./deploy.sh
|
|
# First-time setup: clone repo to /opt/gmal-scope-builder, create .env, then run this script.
|
|
|
|
set -euo pipefail
|
|
|
|
# ── Config ────────────────────────────────────────────────────────────────────
|
|
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
WEB_DIR="/var/www/html/gmal-scope-builder"
|
|
APACHE_CONF="/etc/apache2/sites-enabled/optical-dev.oliver.solutions.conf"
|
|
APACHE_MARKER="gmal-scope-builder" # Used to detect if block already added
|
|
APP_URL_PATH="/gsb"
|
|
BACKEND_PORT="8002"
|
|
HEALTH_URL="http://127.0.0.1:${BACKEND_PORT}/api/health"
|
|
|
|
# ── Colours ───────────────────────────────────────────────────────────────────
|
|
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
|
|
log() { echo -e "${GREEN}[deploy]${NC} $*"; }
|
|
warn() { echo -e "${YELLOW}[ warn ]${NC} $*"; }
|
|
die() { echo -e "${RED}[error ]${NC} $*" >&2; exit 1; }
|
|
|
|
# ── 1. Pre-flight checks ──────────────────────────────────────────────────────
|
|
log "Running pre-flight checks..."
|
|
|
|
[[ -f "$REPO_DIR/.env" ]] || die ".env not found at $REPO_DIR/.env — copy .env.example and fill in values"
|
|
|
|
# Check required env vars
|
|
for var in POSTGRES_PASSWORD ANTHROPIC_API_KEY AZURE_TENANT_ID AZURE_CLIENT_ID; do
|
|
grep -q "^${var}=" "$REPO_DIR/.env" || die "Missing required variable '$var' in .env"
|
|
done
|
|
|
|
command -v docker &>/dev/null || die "Docker is not installed"
|
|
sudo docker compose version &>/dev/null || die "Docker Compose plugin not available"
|
|
|
|
# Resolve DATA_DIR (from .env or default to persistent web dir)
|
|
DATA_DIR=$(grep "^DATA_DIR=" "$REPO_DIR/.env" | cut -d= -f2- | tr -d '"' || true)
|
|
DATA_DIR="${DATA_DIR:-/var/www/html/gmal-scope-builder/data}"
|
|
sudo mkdir -p "$DATA_DIR"
|
|
|
|
# Warn if no GMAL Excel file (non-fatal — data may already be in the DB)
|
|
if ! sudo ls "${DATA_DIR}/"*.xlsx &>/dev/null 2>&1; then
|
|
warn "No .xlsx file found in $DATA_DIR — GMAL ingest will need to be triggered manually after deploy"
|
|
fi
|
|
|
|
log "Pre-flight OK"
|
|
|
|
# ── 2. Pull latest code ───────────────────────────────────────────────────────
|
|
log "Pulling latest code from origin/main..."
|
|
git -C "$REPO_DIR" pull origin main
|
|
|
|
# ── 3. Build and start backend services ──────────────────────────────────────
|
|
log "Building Docker images and starting services (using build cache)..."
|
|
sudo docker compose -f "$REPO_DIR/docker-compose.yml" --env-file "$REPO_DIR/.env" \
|
|
up -d --build --remove-orphans
|
|
|
|
# ── 4. Wait for backend health ────────────────────────────────────────────────
|
|
log "Waiting for backend to become healthy..."
|
|
TIMEOUT=90
|
|
ELAPSED=0
|
|
until curl -sf "$HEALTH_URL" > /dev/null 2>&1; do
|
|
sleep 3
|
|
ELAPSED=$((ELAPSED + 3))
|
|
[[ $ELAPSED -ge $TIMEOUT ]] && {
|
|
warn "Backend logs:"
|
|
sudo docker compose -f "$REPO_DIR/docker-compose.yml" logs --tail=30 backend
|
|
die "Backend did not become healthy within ${TIMEOUT}s"
|
|
}
|
|
log " Still waiting... (${ELAPSED}s)"
|
|
done
|
|
log "Backend healthy at $HEALTH_URL"
|
|
|
|
# ── 5. Database migrations ────────────────────────────────────────────────────
|
|
# create_all() runs automatically inside start.sh on each container start (idempotent).
|
|
# Uncomment the line below once Alembic migrations are set up:
|
|
# sudo docker compose -f "$REPO_DIR/docker-compose.yml" exec -T backend alembic upgrade head
|
|
log "Database schema is managed by start.sh (create_all) — no separate migration step needed"
|
|
|
|
# ── 6. Build frontend ─────────────────────────────────────────────────────────
|
|
log "Building frontend via Node Docker container..."
|
|
sudo docker run --rm \
|
|
-v "$REPO_DIR/frontend:/app" \
|
|
-w /app \
|
|
node:20-alpine \
|
|
sh -c "npm ci --prefer-offline && npm run build"
|
|
|
|
# ── 7. Deploy frontend static files ──────────────────────────────────────────
|
|
log "Deploying frontend to $WEB_DIR..."
|
|
sudo mkdir -p "$WEB_DIR"
|
|
# Remove only frontend files — preserve data/ subdirectory
|
|
sudo find "${WEB_DIR:?}" -maxdepth 1 -mindepth 1 ! -name 'data' -exec rm -rf {} +
|
|
sudo cp -r "$REPO_DIR/frontend/dist/." "$WEB_DIR/"
|
|
sudo chown -R www-data:www-data "$WEB_DIR"
|
|
sudo chown -R root:root "$DATA_DIR" # data/ stays root-owned for Docker
|
|
log "Frontend deployed ($(find "$REPO_DIR/frontend/dist" -type f | wc -l) files)"
|
|
|
|
# ── 8. Apache config (idempotent) ─────────────────────────────────────────────
|
|
if sudo grep -q "$APACHE_MARKER" "$APACHE_CONF" 2>/dev/null; then
|
|
log "Apache block for $APP_URL_PATH already present — skipping"
|
|
else
|
|
log "Adding Apache config block for $APP_URL_PATH ..."
|
|
sudo python3 - << PYEOF
|
|
apache_conf = "$APACHE_CONF"
|
|
block = """
|
|
# ----------------------------------------------------------------
|
|
# GMAL Scope Builder — FastAPI backend at :$BACKEND_PORT
|
|
# ----------------------------------------------------------------
|
|
ProxyPass $APP_URL_PATH/api/ http://127.0.0.1:$BACKEND_PORT/api/
|
|
ProxyPassReverse $APP_URL_PATH/api/ http://127.0.0.1:$BACKEND_PORT/api/
|
|
|
|
# GMAL Scope Builder SPA
|
|
Alias $APP_URL_PATH $WEB_DIR
|
|
|
|
<Directory $WEB_DIR>
|
|
Options -Indexes +FollowSymLinks
|
|
AllowOverride None
|
|
Require all granted
|
|
RewriteEngine On
|
|
RewriteBase $APP_URL_PATH/
|
|
RewriteCond %{REQUEST_FILENAME} !-f
|
|
RewriteCond %{REQUEST_FILENAME} !-d
|
|
RewriteRule ^ index.html [L]
|
|
</Directory>
|
|
"""
|
|
with open(apache_conf) as f:
|
|
content = f.read()
|
|
if "$APACHE_MARKER" not in content:
|
|
content = content.replace("</VirtualHost>", block + "\n</VirtualHost>")
|
|
with open(apache_conf, "w") as f:
|
|
f.write(content)
|
|
print("Apache config updated")
|
|
else:
|
|
print("Apache config already has $APACHE_MARKER block (written by another process)")
|
|
PYEOF
|
|
fi
|
|
|
|
# ── 9. Validate and reload Apache ─────────────────────────────────────────────
|
|
log "Validating Apache config..."
|
|
sudo apache2ctl configtest
|
|
log "Reloading Apache..."
|
|
sudo systemctl reload apache2
|
|
|
|
# ── Done ──────────────────────────────────────────────────────────────────────
|
|
echo ""
|
|
echo -e "${GREEN}╔══════════════════════════════════════════════════════╗${NC}"
|
|
echo -e "${GREEN}║ Deployment complete! ║${NC}"
|
|
echo -e "${GREEN}║ https://optical-dev.oliver.solutions${APP_URL_PATH}/ ║${NC}"
|
|
echo -e "${GREEN}╚══════════════════════════════════════════════════════╝${NC}"
|
|
echo ""
|
|
log "If this is the first deploy, trigger GMAL ingest:"
|
|
log " curl -X POST -H 'Authorization: Bearer <token>' https://optical-dev.oliver.solutions${APP_URL_PATH}/api/gmal/ingest"
|