macsync/deploy/deploy-server.sh

166 lines
6.4 KiB
Bash
Raw Permalink Normal View History

#!/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'"