video-accessibility/scripts/deploy-dev.sh
Vadym Samoilenko 582f8ad2e8 fix(deploy): change API host port 8003→8010, move image to video-accessibility repo
Port 8003 is occupied by infra-api-1 on optical-dev server.
Artifact Registry repo renamed from nexus to video-accessibility.
cloudbuild.yaml defaults _TAG to 'latest' for manual runs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 22:02:14 +01:00

248 lines
10 KiB
Bash
Executable file

#!/usr/bin/env bash
# =============================================================================
# Deploy script for optical-dev.oliver.solutions
# Run from: /opt/video-accessibility/
#
# Usage:
# First deploy: ./scripts/deploy-dev.sh
# Code-only: ./scripts/deploy-dev.sh --redeploy
# Custom: ./scripts/deploy-dev.sh [--skip-build] [--skip-frontend] [--skip-migrations]
#
# --redeploy shorthand for --skip-build --skip-frontend --skip-migrations
# (just git pull + docker up -d, no rebuilds)
# =============================================================================
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 -f docker-compose.optical-dev.yml --env-file .env.production"
API_INTERNAL_PORT=8010 # host port the api container exposes
VITE_BASE="/video-accessibility"
# Only build images that run on optical-dev; heavy workers run on Cloud Run Jobs
BUILD_SERVICES="api worker"
# ── Flags ─────────────────────────────────────────────────────────────────────
SKIP_BUILD=false
SKIP_FRONTEND=false
SKIP_MIGRATIONS=false
for arg in "$@"; do
case $arg in
--redeploy) SKIP_BUILD=true; SKIP_FRONTEND=true; SKIP_MIGRATIONS=true ;;
--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 <<APACHE
# video-accessibility — auto-generated by deploy-dev.sh
# API proxy (strip /video-accessibility prefix so FastAPI sees /api/...)
ProxyPass /video-accessibility/api/ http://127.0.0.1:${API_INTERNAL_PORT}/api/
ProxyPassReverse /video-accessibility/api/ http://127.0.0.1:${API_INTERNAL_PORT}/api/
# Swagger docs
ProxyPass /video-accessibility/docs http://127.0.0.1:${API_INTERNAL_PORT}/docs
ProxyPassReverse /video-accessibility/docs http://127.0.0.1:${API_INTERNAL_PORT}/docs
ProxyPass /video-accessibility/openapi.json http://127.0.0.1:${API_INTERNAL_PORT}/openapi.json
ProxyPassReverse /video-accessibility/openapi.json http://127.0.0.1:${API_INTERNAL_PORT}/openapi.json
# WebSocket
ProxyPass /video-accessibility/api/v1/ws/ ws://127.0.0.1:${API_INTERNAL_PORT}/api/v1/ws/
ProxyPassReverse /video-accessibility/api/v1/ws/ ws://127.0.0.1:${API_INTERNAL_PORT}/api/v1/ws/
# SPA static files
Alias /video-accessibility ${WEBROOT}
<Directory ${WEBROOT}>
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]
</Directory>
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|</VirtualHost>|${INCLUDE_LINE}\n</VirtualHost>|" "$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 "Redeploy: ./scripts/deploy-dev.sh --redeploy # just pull + restart"
echo "Code+front: ./scripts/deploy-dev.sh --skip-build # rebuild frontend, skip docker build"
echo "Full: ./scripts/deploy-dev.sh # everything from scratch"
}
main "$@"