diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..94049d0 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================================= +# OliVAS Production Deploy Script +# Idempotent — safe to run for initial deploy and subsequent updates +# ============================================================================= + +REPO_DIR=/opt/olivas +WEB_DIR=/var/www/html/olivas +COMPOSE="docker compose -f docker-compose.yml -f docker-compose.prod.yml" + +# --------------------------------------------------------------------------- +# Color helpers +# --------------------------------------------------------------------------- +info() { printf '\033[1;34m[INFO]\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33m[WARN]\033[0m %s\n' "$*"; } +error() { printf '\033[1;31m[ERROR]\033[0m %s\n' "$*"; } +success() { printf '\033[1;32m[OK]\033[0m %s\n' "$*"; } + +# --------------------------------------------------------------------------- +# 1. Preflight checks +# --------------------------------------------------------------------------- +if [[ $EUID -ne 0 ]]; then + error "This script must be run as root." + exit 1 +fi + +if ! command -v docker &>/dev/null; then + error "docker is not installed." + exit 1 +fi + +if ! docker compose version &>/dev/null; then + error "docker compose plugin is not available." + exit 1 +fi + +# --------------------------------------------------------------------------- +# 2. Pull latest code +# --------------------------------------------------------------------------- +cd "$REPO_DIR" +info "Pulling latest code..." +git pull --ff-only origin main +COMMIT=$(git rev-parse --short HEAD) +success "At commit $COMMIT" + +# --------------------------------------------------------------------------- +# 3. Handle .env +# --------------------------------------------------------------------------- +if [[ ! -f .env ]]; then + cp .env.example .env + warn ".env was created from .env.example." + warn "Edit /opt/olivas/.env with production values, then re-run this script." + exit 0 +fi + +info "Loading .env..." +set -a +# shellcheck disable=SC1091 +source .env +set +a + +# --------------------------------------------------------------------------- +# 4. Build backend Docker image +# --------------------------------------------------------------------------- +info "Building backend image..." +$COMPOSE build backend + +# --------------------------------------------------------------------------- +# 5. Start postgres and wait for it to be healthy +# --------------------------------------------------------------------------- +info "Starting postgres..." +$COMPOSE up -d postgres + +info "Waiting for postgres to be ready..." +for i in $(seq 1 30); do + if $COMPOSE exec postgres pg_isready -U olivas &>/dev/null; then + success "Postgres is ready." + break + fi + if [[ $i -eq 30 ]]; then + error "Postgres did not become ready within 30 seconds." + exit 1 + fi + sleep 1 +done + +# --------------------------------------------------------------------------- +# 6. Run database migrations +# --------------------------------------------------------------------------- +info "Starting backend for migrations..." +$COMPOSE up -d backend + +info "Running database migrations..." +$COMPOSE exec backend alembic -c alembic.ini upgrade head +success "Migrations complete." + +# --------------------------------------------------------------------------- +# 7. Restart backend (pick up new code/env) +# --------------------------------------------------------------------------- +info "Restarting backend..." +$COMPOSE up -d --force-recreate backend +success "Backend restarted." + +# --------------------------------------------------------------------------- +# 8. Build frontend in a temporary Docker container +# --------------------------------------------------------------------------- +info "Building frontend..." +docker run --rm \ + -v "$REPO_DIR/frontend:/app" \ + -w /app \ + -e VITE_AZURE_TENANT_ID \ + -e VITE_AZURE_CLIENT_ID \ + -e VITE_AZURE_REDIRECT_URI \ + node:20-alpine \ + sh -c "npm ci --prefer-offline && npm run build" +success "Frontend built." + +# --------------------------------------------------------------------------- +# 9. Deploy frontend to web directory +# --------------------------------------------------------------------------- +info "Deploying frontend to $WEB_DIR..." +mkdir -p "$WEB_DIR" +rm -rf "${WEB_DIR:?}"/* +cp -r "$REPO_DIR/frontend/dist/"* "$WEB_DIR/" +chown -R www-data:www-data "$WEB_DIR" +success "Frontend deployed." + +# --------------------------------------------------------------------------- +# 10. Health check +# --------------------------------------------------------------------------- +info "Waiting for backend health check..." +sleep 3 +if curl -sf http://localhost:8000/api/health > /dev/null 2>&1; then + success "Backend health check passed." +else + warn "Backend health check failed — it may still be starting up." +fi + +# --------------------------------------------------------------------------- +# 11. Summary +# --------------------------------------------------------------------------- +echo "" +success "Deploy complete" +info "Commit: $COMMIT" +info "Timestamp: $(date -u '+%Y-%m-%d %H:%M:%S UTC')" diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..73518de --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,6 @@ +services: + postgres: + ports: !override [] + restart: unless-stopped + backend: + restart: unless-stopped