#!/usr/bin/env bash # Deploy the full stack to ambrosio. # # Usage: infra/scripts/deploy-erosi.sh # Env: DEPLOY_HOST=ambrosio (override if needed) # DEPLOY_PATH=/opt/colectivo # # First run: # 1. rsyncs repo → ambrosio:/opt/colectivo/ # 2. if .env doesn't exist on server, generates secrets and writes it with # placeholder values the operator must fill in (external Keycloak URL, # Keycloak client secret, Traefik network name) # 3. docker compose build + up -d # 4. applies DB migrations # # TLS + routing is handled by Traefik (deployed by NetBird's self-hosted # installer on ambrosio). This script never touches /etc/caddy/* or any host # reverse-proxy config — it only manages the Docker stack. # # Subsequent runs: rsync + rebuild app + rolling restart. set -euo pipefail DEPLOY_HOST="${DEPLOY_HOST:-ambrosio}" DEPLOY_PATH="${DEPLOY_PATH:-/opt/colectivo}" REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" echo "==> Deploying to $DEPLOY_HOST:$DEPLOY_PATH" # ── 1. Ensure target dir exists ──────────────────────────────────────────── ssh "$DEPLOY_HOST" "sudo mkdir -p $DEPLOY_PATH && sudo chown \$(id -u):\$(id -g) $DEPLOY_PATH" # ── 2. rsync repo (excluding node_modules / build artefacts / local env) ── rsync -az --delete \ --exclude '.git' \ --exclude 'node_modules' \ --exclude '.svelte-kit' \ --exclude 'apps/web/build' \ --exclude 'apps/web/.vite' \ --exclude '**/*.log' \ --exclude '**/.env' \ --exclude '**/.env.development' \ --exclude '**/.env.local' \ --exclude 'lighthouse-report.html' \ --exclude 'coverage' \ --exclude 'test-results' \ --exclude 'playwright-report' \ "$REPO_ROOT/" "$DEPLOY_HOST:$DEPLOY_PATH/" # ── 3. Bootstrap secrets on first run; sanity-check placeholders every run ─ ssh "$DEPLOY_HOST" bash -s << 'REMOTE' set -euo pipefail cd /opt/colectivo if [ ! -f .env ]; then echo "--- First deploy: generating secrets" # JWT triplet (anon + service_role) JWT_OUT=$(bash infra/scripts/rotate-jwt.sh) SUPABASE_JWT_SECRET=$(echo "$JWT_OUT" | awk -F= '/^SUPABASE_JWT_SECRET=/{print substr($0, index($0,$2))}') PUBLIC_SUPABASE_ANON_KEY=$(echo "$JWT_OUT" | awk -F= '/^PUBLIC_SUPABASE_ANON_KEY=/{print substr($0, index($0,$2))}') SUPABASE_SERVICE_ROLE_KEY=$(echo "$JWT_OUT" | awk -F= '/^SUPABASE_SERVICE_ROLE_KEY=/{print substr($0, index($0,$2))}') # Postgres + Realtime secrets POSTGRES_PASSWORD=$(openssl rand -hex 24) REALTIME_ENC_KEY=$(openssl rand -hex 8) # 16 chars REALTIME_SECRET_KEY_BASE=$(openssl rand -hex 48) # 96 chars cat > .env <&2 echo " Fill in PUBLIC_KEYCLOAK_URL, KEYCLOAK_CLIENT_SECRET, TRAEFIK_NETWORK and retry." >&2 exit 1 fi REMOTE # ── 4. Build + bring up the stack ───────────────────────────────────────── ssh "$DEPLOY_HOST" bash -s << 'REMOTE' set -euo pipefail cd /opt/colectivo echo "--- Building app image" docker compose --env-file .env -f infra/docker-compose.erosi.yml build app echo "--- Bringing stack up (detached)" docker compose --env-file .env -f infra/docker-compose.erosi.yml up -d echo "--- Waiting for db to be healthy" for i in $(seq 1 60); do if docker compose --env-file .env -f infra/docker-compose.erosi.yml ps db --format json | grep -q '"Health":"healthy"'; then echo "db healthy"; break fi sleep 2 done echo "--- Applying migrations" # Every `docker compose exec -T` call below pipes in ` Deploy complete." echo " App: https://erosi.limonia.net" echo " Keycloak: external — see PUBLIC_KEYCLOAK_URL in $DEPLOY_PATH/.env"