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 <noreply@anthropic.com>
This commit is contained in:
parent
49835f9b0c
commit
c7eaa7a952
1 changed files with 238 additions and 0 deletions
238
scripts/deploy-dev.sh
Executable file
238
scripts/deploy-dev.sh
Executable file
|
|
@ -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 <<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 "Rerun: ./scripts/deploy-dev.sh --skip-build --skip-frontend"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Loading…
Add table
Reference in a new issue