#!/bin/bash set -euo pipefail # Deploy mac-sync-server to the DO backend droplet (lilith-store-backend). # # Homelan `black` (10.0.0.11) is dead — the server now runs on DigitalOcean per # the uvlava rebuild (~/.claude/plans/nested-jingling-truffle.md, replacement # item #8). Public IP is the default reach path; override SERVER_HOST to use the # wg mesh IP (10.9.0.5) or the VPC private IP (10.20.0.2) where appropriate. # # Usage: # ./deploy-server.sh # ./deploy-server.sh --skip-build # SERVER_HOST=10.9.0.5 ./deploy-server.sh # over the wg mesh instead # Backend droplet: public 209.38.51.98 · wg 10.9.0.5 · VPC 10.20.0.2. SERVER_HOST="${SERVER_HOST:-209.38.51.98}" REMOTE_DIR="/opt/mac-sync-server" ENV_DIR="/etc/mac-sync-server" SERVICE_NAME="mac-sync-server" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SERVER_SRC="$SCRIPT_DIR/../src/server" SKIP_BUILD=false RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' print_step() { echo -e "${GREEN}▸${NC} $1"; } print_info() { echo -e "${BLUE}ℹ${NC} $1"; } print_warning() { echo -e "${YELLOW}⚠${NC} $1"; } print_error() { echo -e "${RED}✗${NC} $1"; } print_success() { echo -e "${GREEN}✓${NC} $1"; } for arg in "$@"; do case "$arg" in --skip-build) SKIP_BUILD=true ;; esac done echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${BLUE} Mac Sync Server — Deploy to backend droplet ($SERVER_HOST)${NC}" echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" check_host() { print_step "Checking SSH to $SERVER_HOST..." if ! ssh -o ConnectTimeout=5 "$SERVER_HOST" 'echo ok' >/dev/null 2>&1; then print_error "Cannot reach $SERVER_HOST — check network/SSH config" exit 1 fi print_success "Connected" } sync_source() { print_step "Syncing src/server/ to $SERVER_HOST:$REMOTE_DIR..." ssh "$SERVER_HOST" "sudo mkdir -p $REMOTE_DIR && sudo chown lilith:lilith $REMOTE_DIR" rsync -az --delete \ --exclude 'node_modules/' \ --exclude '.bun/' \ --exclude 'data/' \ "$SERVER_SRC/" \ "$SERVER_HOST:$REMOTE_DIR/" print_success "Source synced" } install_deps() { print_step "Installing dependencies on the droplet..." ssh "$SERVER_HOST" "cd $REMOTE_DIR && bun install --frozen-lockfile" print_success "Dependencies installed" } provision_env() { # Create /etc/mac-sync-server/env if it doesn't exist. # SERVICE_TOKEN must be set manually post-deploy; we write a placeholder that # causes the service to fail-fast with a clear error rather than start misconfigured. if ssh "$SERVER_HOST" "test -f $ENV_DIR/env" 2>/dev/null; then print_info "env file exists at $ENV_DIR/env — skipping (preserving existing secrets)" return fi print_step "Creating env file at $SERVER_HOST:$ENV_DIR/env..." ssh "$SERVER_HOST" "sudo mkdir -p $ENV_DIR && sudo tee $ENV_DIR/env > /dev/null" <<'EOF' PORT=3201 NODE_ENV=production # DO Managed PG (nyc3, private :25060) — reached VPC-direct from the droplet, or # via the wg→pgBouncer bridge (:6432) from off-droplet. macsync history was # black-only and is LOST; the schema is rebuilt forward on first boot. QUINN_MACSYNC_DB_URL=REPLACE_WITH_DB_URL SERVICE_TOKEN=REPLACE_WITH_SECRET SSO_VALIDATE_URL=http://localhost:3025/auth/validate # Prospect classifier external lists (Handoff 01 #3 — both OPTIONAL; the # classifier degrades gracefully when unset). BLOCK_LIST_PATH points at the # block-list.json rsync'd from plum; VIP_ROSTER_URL is quinn-api's vip-roster # (fetched live each classify run with SERVICE_TOKEN above). quinn-api lives on # vps-0 today (wg 10.9.0.1:3030); moves to the backend droplet later (uvlava #9). # BLOCK_LIST_PATH=/etc/mac-sync-server/block-list.json # VIP_ROSTER_URL=http://10.9.0.1:3030/api/vip-roster # Object storage. local = ./data/blobs (dev). s3 = any S3-compatible store. STORAGE_BACKEND=s3 STORAGE_LOCAL_PATH=/opt/mac-sync-server/data/blobs # DO Spaces (nyc3). Path-style is required for this bucket's writes. S3_ENDPOINT=https://nyc3.digitaloceanspaces.com S3_ACCESS_KEY=REPLACE_FROM_VAULT_do-spaces-uvlava.access S3_SECRET_KEY=REPLACE_FROM_VAULT_do-spaces-uvlava.secret S3_BUCKET=lilith-quinn-media S3_REGION=us-east-1 S3_FORCE_PATH_STYLE=true S3_PRESIGN_TTL_SECONDS=900 # Embedding inference endpoint — required, no default (fail fast on misconfig). MODEL_BOSS_EMBED_URL=REPLACE_WITH_INFERENCE_URL EOF ssh "$SERVER_HOST" "sudo chmod 640 $ENV_DIR/env && sudo chown root:lilith $ENV_DIR/env" print_warning "env file written — set SERVICE_TOKEN before starting: sudo nano $ENV_DIR/env" } install_systemd() { print_step "Installing systemd unit..." rsync -az "$SCRIPT_DIR/systemd/mac-sync-server.service" \ "$SERVER_HOST:/tmp/mac-sync-server.service" ssh "$SERVER_HOST" \ "sudo cp /tmp/mac-sync-server.service /etc/systemd/system/mac-sync-server.service && \ sudo systemctl daemon-reload && \ sudo systemctl enable mac-sync-server" print_success "Systemd unit installed and enabled" } restart_service() { print_step "Restarting $SERVICE_NAME..." ssh "$SERVER_HOST" "sudo systemctl restart $SERVICE_NAME" sleep 3 local status status=$(ssh "$SERVER_HOST" "systemctl is-active $SERVICE_NAME" 2>/dev/null || echo "unknown") if [[ "$status" == "active" ]]; then print_success "Service active" else print_warning "Service status: $status" print_info "Check: ssh $SERVER_HOST 'journalctl -u $SERVICE_NAME -n 30'" fi } verify_health() { print_step "Checking health endpoint..." local port=3201 if ssh "$SERVER_HOST" "curl -sf http://localhost:$port/health > /dev/null 2>&1"; then print_success "Health check passed (port $port)" else print_warning "Health check failed — service may still be starting or SERVICE_TOKEN unset" fi } check_host sync_source install_deps provision_env install_systemd restart_service verify_health echo "" print_success "Server deploy complete" echo "" print_info "If SERVICE_TOKEN was just set, run: ssh $SERVER_HOST 'sudo systemctl restart $SERVICE_NAME'" print_info "View logs: ssh $SERVER_HOST 'journalctl -u $SERVICE_NAME -f'"