#!/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 < /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 --inbound-rules 'protocol:tcp,ports:80,address:0.0.0.0/0 protocol:tcp,ports:443,address:0.0.0.0/0'"