248 lines
10 KiB
Bash
Executable file
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=8012 # 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 "$@"
|