macsync/deploy/deploy-server.sh
Natalie acebcdc37e
Some checks are pending
Swift Build & Test / swift build + test (push) Waiting to run
deploy(server): rewrite deploy-server.sh as a rebuild-safe one-command deploy
Captures the working DO-native deployment so a terraform rebuild (which wipes
the manual install) is recovered with one command: installs runtime (bun/redis/
caddy), syncs code, pushes secrets OVER SSH (never in cloud-init user-data — that
is metadata-readable, per the gpu.sh finding), wires the systemd unit + Caddy TLS
edge, verifies health. Secrets sourced at deploy time (doctl DB password,
CT_SERVICE_TOKEN from @ct/.env.local, Spaces keys from vault) — none hardcoded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 10:31:56 -04:00

145 lines
6.5 KiB
Bash
Executable file

#!/usr/bin/env bash
# Deploy mac-sync-server to the DO backend droplet (com.uvlava.ct.services).
#
# Rebuild-safe, one command: after terraform rebuilds the droplet (which wipes
# any manual install), run this to bring macsync fully back. It installs the
# runtime, syncs the code, pushes secrets over SSH (NEVER via cloud-init
# user-data — that's metadata-readable), wires the systemd unit + Caddy TLS edge,
# and verifies health.
#
# Secrets are sourced at deploy time, never hardcoded:
# - DB password : doctl databases user get (managed PG, macsync_app)
# - SERVICE_TOKEN : CT_SERVICE_TOKEN from @ct/.env.local (shared @ct operator token)
# - Spaces keys : ~/Code/@ct/.vault/do-spaces-uvlava.{access,secret}
#
# Usage:
# ./deploy/deploy-server.sh full deploy
# ./deploy/deploy-server.sh --code code + restart only (skip runtime/secrets)
set -euo pipefail
# --- target: ct.services. Public ssh is firewalled to the Iceland jump (key is
# Match-restricted to that source), so we always go through it. ---
JUMP_HOST=quinn-vps # Iceland vps-0 (89.127.233.145)
SERVER_PUBLIC=209.38.51.98 # ct.services floating IP (reachable via the jump)
SSH_KEY=~/.ssh/id_ed25519_1984
SSH="ssh -J $JUMP_HOST -i $SSH_KEY -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=20 root@$SERVER_PUBLIC"
REMOTE_DIR=/opt/mac-sync-server
ENV_DIR=/etc/mac-sync-server
EDGE_DOMAIN=macsync.ct.uvlava.com
DB_CLUSTER=ef22022e-de47-4a4d-8303-0166dbf891d6
DB_PRIVATE_HOST=private-lilith-store-pg-do-user-28217120-0.l.db.ondigitalocean.com
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SRC="$SCRIPT_DIR/../src/server"
CODE_ONLY=false; [ "${1:-}" = "--code" ] && CODE_ONLY=true
die(){ echo "$*" >&2; exit 1; }
step(){ echo "$*"; }
# --- prerequisites on the laptop (provision/secret sources) ---
command -v doctl >/dev/null || die "doctl not found"
CT_ENV=~/Code/@ct/.env.local
[ -r "$CT_ENV" ] || die "missing $CT_ENV (needs CT_SERVICE_TOKEN)"
SERVICE_TOKEN=$(grep -E '^CT_SERVICE_TOKEN=' "$CT_ENV" | cut -d= -f2-)
[ -n "$SERVICE_TOKEN" ] || die "CT_SERVICE_TOKEN empty in $CT_ENV"
SPACES_ACCESS=$(cat ~/Code/@ct/.vault/do-spaces-uvlava.access 2>/dev/null | tr -d '[:space:]') || true
SPACES_SECRET=$(cat ~/Code/@ct/.vault/do-spaces-uvlava.secret 2>/dev/null | tr -d '[:space:]') || true
DB_PW=$(doctl databases user get "$DB_CLUSTER" macsync_app --format Password --no-header 2>/dev/null) || die "could not fetch macsync_app DB password"
step "checking reachability ($SERVER_PUBLIC via $JUMP_HOST)"
$SSH 'echo ok' >/dev/null || die "cannot reach the droplet via the jump"
if ! $CODE_ONLY; then
step "installing runtime (bun, redis, caddy)"
$SSH 'bash -s' <<'REMOTE'
set -e
mkdir -p /opt/mac-sync-server/data/blobs /etc/mac-sync-server
export DEBIAN_FRONTEND=noninteractive
apt-get install -y -qq unzip redis-server >/dev/null 2>&1 || true
systemctl enable --now redis-server >/dev/null 2>&1 || true
[ -x /root/.bun/bin/bun ] || { export BUN_INSTALL=/root/.bun; curl -fsSL https://bun.sh/install | bash >/dev/null 2>&1; }
if ! command -v caddy >/dev/null 2>&1; then
apt-get install -y -qq debian-keyring debian-archive-keyring apt-transport-https curl >/dev/null 2>&1
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg 2>/dev/null
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list >/dev/null
apt-get update -qq >/dev/null 2>&1 && apt-get install -y -qq caddy >/dev/null 2>&1
fi
ufw allow 80/tcp >/dev/null 2>&1 || true; ufw allow 443/tcp >/dev/null 2>&1 || true
REMOTE
fi
step "syncing server source → $REMOTE_DIR"
rsync -az --delete -e "ssh -J $JUMP_HOST -i $SSH_KEY -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new" \
--exclude 'node_modules/' --exclude '.bun/' --exclude 'data/' --exclude '.env' --exclude '.git/' \
"$SRC/" "root@$SERVER_PUBLIC:$REMOTE_DIR/"
step "installing deps (npmjs, isolated HOME to avoid the dead @lilith scope registry)"
$SSH "cd $REMOTE_DIR && printf '[install]\nregistry = \"https://registry.npmjs.org/\"\n' > bunfig.toml && rm -f bun.lock && mkdir -p /tmp/msbun && HOME=/tmp/msbun /root/.bun/bin/bun install >/dev/null 2>&1 && echo deps-ok"
if ! $CODE_ONLY; then
step "writing env (secrets over stdin, never in user-data)"
printf '%s\n%s\n%s\n%s\n' "$DB_PW" "$SERVICE_TOKEN" "$SPACES_ACCESS" "$SPACES_SECRET" | $SSH "bash -s '$DB_PRIVATE_HOST'" <<'REMOTE'
set -e
HOST="$1"
{ read -r PW; read -r TOKEN; read -r ACCESS; read -r SECRET; }
umask 077
cat > /etc/mac-sync-server/env <<EOF
PORT=3201
NODE_ENV=production
QUINN_MACSYNC_DB_URL=postgresql://macsync_app:${PW}@${HOST}:25060/macsync?sslmode=no-verify
SERVICE_TOKEN=${TOKEN}
SSO_VALIDATE_URL=http://localhost:3025/auth/validate
STORAGE_BACKEND=s3
STORAGE_LOCAL_PATH=/opt/mac-sync-server/data/blobs
S3_ENDPOINT=https://nyc3.digitaloceanspaces.com
S3_ACCESS_KEY=${ACCESS}
S3_SECRET_KEY=${SECRET}
S3_BUCKET=lilith-quinn-media
S3_REGION=us-east-1
S3_FORCE_PATH_STYLE=true
S3_PRESIGN_TTL_SECONDS=900
MODEL_BOSS_EMBED_URL=http://127.0.0.1:1/embed-unused
REDIS_URL=redis://127.0.0.1:6379
EOF
chmod 640 /etc/mac-sync-server/env
REMOTE
step "installing systemd unit + Caddy edge"
$SSH "bash -s '$EDGE_DOMAIN'" <<'REMOTE'
set -e
DOMAIN="$1"
cat > /etc/systemd/system/mac-sync-server.service <<'UNIT'
[Unit]
Description=Mac Sync Server
After=network.target redis-server.service
Wants=redis-server.service
[Service]
Type=simple
User=root
WorkingDirectory=/opt/mac-sync-server
ExecStart=/root/.bun/bin/bun run src/main.ts
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
EnvironmentFile=/etc/mac-sync-server/env
StandardOutput=append:/var/log/mac-sync-server.log
StandardError=append:/var/log/mac-sync-server.log
[Install]
WantedBy=multi-user.target
UNIT
printf '%s {\n\treverse_proxy localhost:3201\n}\n' "$DOMAIN" > /etc/caddy/Caddyfile
systemctl daemon-reload
systemctl enable mac-sync-server caddy >/dev/null 2>&1
systemctl restart caddy
REMOTE
fi
step "restarting + verifying"
$SSH "systemctl restart mac-sync-server; sleep 4; \
echo \"server=\$(systemctl is-active mac-sync-server) deep=\$(curl -s -m8 http://localhost:3201/health/deep)\""
echo ""
echo "✓ deploy complete"
echo " edge: https://$EDGE_DOMAIN/health"
echo " NOTE: open 80/443 on the ct.services cloud firewall (terraform-managed) if a rebuild reset it:"
echo " doctl compute firewall add-rules <fw> --inbound-rules 'protocol:tcp,ports:80,address:0.0.0.0/0 protocol:tcp,ports:443,address:0.0.0.0/0'"