From c7eaa7a952dc2f19fc982b68b7d5fdc8967f684e Mon Sep 17 00:00:00 2001 From: Vadym Samoilenko Date: Wed, 29 Apr 2026 21:35:19 +0100 Subject: [PATCH] chore: add deploy-dev.sh for optical-dev deployment Sequential image builds (one at a time to avoid OOM), auto Apache fragment, migrations, frontend rsync, smoke test. Flags: --skip-build / --skip-frontend / --skip-migrations Co-Authored-By: Claude Sonnet 4.6 --- scripts/deploy-dev.sh | 238 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100755 scripts/deploy-dev.sh diff --git a/scripts/deploy-dev.sh b/scripts/deploy-dev.sh new file mode 100755 index 0000000..b2292fc --- /dev/null +++ b/scripts/deploy-dev.sh @@ -0,0 +1,238 @@ +#!/usr/bin/env bash +# ============================================================================= +# Deploy script for optical-dev.oliver.solutions +# Run from: /opt/video-accessibility/ +# Usage: ./scripts/deploy-dev.sh [--skip-build] [--skip-frontend] [--skip-migrations] +# ============================================================================= +set -euo pipefail + +# ── Config ──────────────────────────────────────────────────────────────────── +PROJECT_DIR="/opt/video-accessibility" +WEBROOT="/var/www/html/video-accessibility" +APACHE_CONF_DIR="/etc/apache2/sites-available" +APACHE_VHOST="optical-dev.oliver.solutions.conf" +COMPOSE="docker compose -f docker-compose.yml -f docker-compose.prod.yml --env-file .env.production" +API_INTERNAL_PORT=8000 # host port the api container exposes +VITE_BASE="/video-accessibility" + +# Services built sequentially (heaviest last, whisper skipped — download takes forever) +BUILD_SERVICES="api worker ffmpeg-worker tts-worker" + +# ── Flags ───────────────────────────────────────────────────────────────────── +SKIP_BUILD=false +SKIP_FRONTEND=false +SKIP_MIGRATIONS=false +for arg in "$@"; do + case $arg in + --skip-build) SKIP_BUILD=true ;; + --skip-frontend) SKIP_FRONTEND=true ;; + --skip-migrations) SKIP_MIGRATIONS=true ;; + esac +done + +# ── Helpers ─────────────────────────────────────────────────────────────────── +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m' +ok() { echo -e "${GREEN}✓ $*${NC}"; } +info() { echo -e "${BLUE}» $*${NC}"; } +warn() { echo -e "${YELLOW}⚠ $*${NC}"; } +fail() { echo -e "${RED}✗ $*${NC}"; exit 1; } + +# ── Pre-flight ──────────────────────────────────────────────────────────────── +preflight() { + info "Pre-flight checks..." + [[ -f "docker-compose.yml" ]] || fail "Run from /opt/video-accessibility/" + [[ -f ".env.production" ]] || fail ".env.production not found" + [[ -f "secrets/gcp-credentials.json" ]] || fail "secrets/gcp-credentials.json not found" + docker info &>/dev/null || fail "Docker not running" + docker compose version &>/dev/null || fail "docker compose not found" + + # Warn if free RAM < 2 GB + FREE_MB=$(free -m | awk '/^Mem:/ {print $7}') + if (( FREE_MB < 2048 )); then + warn "Low free RAM: ${FREE_MB}MB — build may be slow or OOM" + fi + ok "Pre-flight passed (free RAM: ${FREE_MB}MB)" +} + +# ── Git pull ────────────────────────────────────────────────────────────────── +pull_code() { + info "Pulling latest code from main..." + git pull origin main + ok "Code updated ($(git rev-parse --short HEAD))" +} + +# ── Docker build (sequential, one service at a time) ───────────────────────── +build_images() { + if $SKIP_BUILD; then warn "Skipping Docker build (--skip-build)"; return; fi + + info "Building Docker images sequentially..." + for svc in $BUILD_SERVICES; do + info " Building: $svc" + $COMPOSE build "$svc" + ok " $svc — done" + done + ok "All images built" +} + +# ── Start services ──────────────────────────────────────────────────────────── +start_services() { + info "Starting services..." + $COMPOSE up -d + ok "Containers started" + + # Wait for API to be healthy (up to 60s) + info "Waiting for API to be healthy..." + for i in $(seq 1 30); do + if curl -sf "http://localhost:${API_INTERNAL_PORT}/health" &>/dev/null; then + ok "API is healthy" + return + fi + sleep 2 + done + warn "API health check timed out — check logs: docker compose logs api" +} + +# ── Migrations ──────────────────────────────────────────────────────────────── +run_migrations() { + if $SKIP_MIGRATIONS; then warn "Skipping migrations (--skip-migrations)"; return; fi + + info "Running database migrations..." + $COMPOSE exec -T api python migrate.py up + ok "Migrations complete" +} + +# ── Frontend build & deploy ─────────────────────────────────────────────────── +deploy_frontend() { + if $SKIP_FRONTEND; then warn "Skipping frontend (--skip-frontend)"; return; fi + + info "Building frontend..." + cd frontend + npm ci --prefer-offline + VITE_BASE_PATH="${VITE_BASE}" npm run build + cd .. + ok "Frontend built" + + info "Deploying frontend to Apache webroot..." + sudo mkdir -p "${WEBROOT}" + sudo rsync -a --delete frontend/dist/ "${WEBROOT}/" + sudo chown -R www-data:www-data "${WEBROOT}" + sudo chmod -R 755 "${WEBROOT}" + ok "Frontend deployed to ${WEBROOT}" +} + +# ── Apache fragment ─────────────────────────────────────────────────────────── +ensure_apache_config() { + FRAGMENT="${PROJECT_DIR}/deploy/apache-video-accessibility.conf" + + if [[ ! -f "$FRAGMENT" ]]; then + info "Writing Apache config fragment..." + sudo mkdir -p "${PROJECT_DIR}/deploy" + sudo tee "$FRAGMENT" > /dev/null < + Options -Indexes +FollowSymLinks + AllowOverride None + Require all granted + RewriteEngine On + RewriteBase /video-accessibility/ + RewriteCond %{REQUEST_FILENAME} -f [OR] + RewriteCond %{REQUEST_FILENAME} -d + RewriteRule ^ - [L] + RewriteRule ^ index.html [L] + +APACHE + ok "Apache fragment written to $FRAGMENT" + fi + + VHOST_FILE="${APACHE_CONF_DIR}/${APACHE_VHOST}" + INCLUDE_LINE=" Include ${FRAGMENT}" + + if [[ -f "$VHOST_FILE" ]]; then + if ! sudo grep -qF "$FRAGMENT" "$VHOST_FILE"; then + info "Injecting Include into Apache vhost..." + sudo sed -i "s||${INCLUDE_LINE}\n|" "$VHOST_FILE" + ok "Include injected" + else + ok "Apache Include already present" + fi + + sudo apache2ctl configtest && sudo systemctl reload apache2 + ok "Apache reloaded" + else + warn "Vhost file not found: $VHOST_FILE — add manually:" + warn " Include ${FRAGMENT}" + fi +} + +# ── Smoke test ──────────────────────────────────────────────────────────────── +smoke_test() { + info "Smoke test..." + + # Internal API + if curl -sf "http://localhost:${API_INTERNAL_PORT}/health" | python3 -m json.tool; then + ok "API /health — OK" + else + warn "API /health — failed" + fi + + # Public URL + PUBLIC_URL="https://optical-dev.oliver.solutions${VITE_BASE}" + HTTP_CODE=$(curl -o /dev/null -sw "%{http_code}" "${PUBLIC_URL}/" 2>/dev/null || echo "000") + if [[ "$HTTP_CODE" == "200" ]]; then + ok "Frontend ${PUBLIC_URL}/ — HTTP 200" + else + warn "Frontend ${PUBLIC_URL}/ — HTTP ${HTTP_CODE} (Apache may need config)" + fi + + echo "" + info "Container status:" + $COMPOSE ps +} + +# ── Main ────────────────────────────────────────────────────────────────────── +main() { + echo "" + echo -e "${BLUE}══════════════════════════════════════════${NC}" + echo -e "${BLUE} video-accessibility — optical-dev deploy ${NC}" + echo -e "${BLUE}══════════════════════════════════════════${NC}" + echo "" + + cd "$PROJECT_DIR" + + preflight + pull_code + build_images + start_services + run_migrations + deploy_frontend + ensure_apache_config + smoke_test + + echo "" + ok "Deploy complete!" + echo "" + echo " App: https://optical-dev.oliver.solutions/video-accessibility/" + echo " API: https://optical-dev.oliver.solutions/video-accessibility/api/v1/health" + echo " Docs: https://optical-dev.oliver.solutions/video-accessibility/docs" + echo "" + echo "Logs: docker compose -f docker-compose.yml -f docker-compose.prod.yml logs -f api" + echo "Rerun: ./scripts/deploy-dev.sh --skip-build --skip-frontend" +} + +main "$@"