[object Object]

← back to Homesonspec

deploy-kamatera: rewrite for homesonspec self-host (correct naming, explicit PORT, pgboss grants, LE-cert vhost)

be37128270fef9f5404204683e1ca94b40521fbe · 2026-07-28 07:42:59 -0700 · Steve Abrams

Files touched

Diff

commit be37128270fef9f5404204683e1ca94b40521fbe
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jul 28 07:42:59 2026 -0700

    deploy-kamatera: rewrite for homesonspec self-host (correct naming, explicit PORT, pgboss grants, LE-cert vhost)
---
 deploy-kamatera.sh | 150 ++++++++++++++++++++++++++---------------------------
 1 file changed, 74 insertions(+), 76 deletions(-)

diff --git a/deploy-kamatera.sh b/deploy-kamatera.sh
index 1192e71..9a185a0 100755
--- a/deploy-kamatera.sh
+++ b/deploy-kamatera.sh
@@ -1,116 +1,114 @@
 #!/usr/bin/env bash
-# One-shot Kamatera deploy for SpecHomes. Run from Mac2:  ! bash ~/Projects/spechomes/deploy-kamatera.sh
-# Idempotent — safe to re-run. Serves web on :9975, admin on :9976 behind nginx (HTTP; CF handles edge TLS).
+# One-shot Kamatera self-host deploy for HomesOnSpec.  Run from Mac:  ! bash ~/Projects/homesonspec/deploy-kamatera.sh
+# Idempotent — safe to re-run. Web :9975, admin :9976, workers daemon; all behind nginx.
+#
+# Verified working 2026-07-28 (TK-10005). Notes baked in from the failures that cost us a day:
+#   • project is "homesonspec" (renamed from spechomes) — package scope @homesonspec/*, DB "homesonspec".
+#   • pm2 MUST get an explicit PORT env or Next binds its default 3100/3101 and the :9975 vhost 502s.
+#   • restore is a CLEAN drop+create (--no-owner) then GRANT on BOTH public AND pgboss schemas,
+#     else the pg-boss workers crash-loop on aclcheck_error.
+#   • homesonspec.com is grey-cloud (DNS-only → origin), so the origin serves the real cert:
+#     keep the Let's Encrypt cert on the public :443 block (NOT self-signed + "CF Full").
 set -euo pipefail
 KH=root@45.61.58.125
-REMOTE=/root/Projects/spechomes
+REMOTE=/root/Projects/homesonspec
 WEB_PORT=9975
 ADMIN_PORT=9976
-DBPW_FILE=~/Projects/spechomes/.env.kamatera
+SRC_DB="postgresql://macstudio3@localhost/homesonspec?host=/tmp"
+EK=~/Projects/homesonspec/.env.kamatera
 
-echo "──────── 1/6  Postgres role + DB ────────"
-if ! grep -q '^SPECHOMES_DB_PW=' "$DBPW_FILE" 2>/dev/null; then
-  echo "SPECHOMES_DB_PW=$(openssl rand -hex 16)" >> "$DBPW_FILE"; chmod 600 "$DBPW_FILE"
-fi
-DBPW=$(grep '^SPECHOMES_DB_PW=' "$DBPW_FILE" | tail -1 | cut -d= -f2)
-# Admin Basic-Auth — generated once and stored in the gitignored .env.kamatera
-# (no committed default credential; TK-10 security remediation). Read the value
-# from that file to log into the admin. Rotate by deleting the line + redeploying.
-if ! grep -q '^SPECHOMES_ADMIN_AUTH=' "$DBPW_FILE" 2>/dev/null; then
-  echo "SPECHOMES_ADMIN_AUTH=admin:$(openssl rand -hex 16)" >> "$DBPW_FILE"; chmod 600 "$DBPW_FILE"
-fi
-ADMIN_AUTH=$(grep '^SPECHOMES_ADMIN_AUTH=' "$DBPW_FILE" | tail -1 | cut -d= -f2-)
-ssh $KH "sudo -u postgres psql -v ON_ERROR_STOP=1 -c \"DO \\\$\\\$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='spechomes') THEN CREATE ROLE spechomes LOGIN PASSWORD '$DBPW'; ELSE ALTER ROLE spechomes PASSWORD '$DBPW'; END IF; END \\\$\\\$;\"; \
-  sudo -u postgres createdb -O spechomes spechomes 2>/dev/null || echo '  db exists'; \
-  sudo -u postgres psql -d spechomes -c 'CREATE EXTENSION IF NOT EXISTS pg_trgm;' >/dev/null"
+echo "──────── 1/6  creds (gitignored .env.kamatera) ────────"
+grep -q '^HOMESONSPEC_DB_PW='    "$EK" 2>/dev/null || echo "HOMESONSPEC_DB_PW=$(openssl rand -hex 16)"       >> "$EK"
+grep -q '^HOMESONSPEC_ADMIN_AUTH=' "$EK" 2>/dev/null || echo "HOMESONSPEC_ADMIN_AUTH=admin:$(openssl rand -hex 12)" >> "$EK"
+chmod 600 "$EK"
+DBPW=$(grep '^HOMESONSPEC_DB_PW='      "$EK" | tail -1 | cut -d= -f2)
+ADMIN_AUTH=$(grep '^HOMESONSPEC_ADMIN_AUTH=' "$EK" | tail -1 | cut -d= -f2-)
 
-echo "──────── 2/6  Load data (pg_dump Mac2 → Kamatera) ────────"
-pg_dump --no-owner --no-acl -d "postgresql://macstudio3@localhost/spechomes?host=/tmp" \
-  | gzip \
-  | ssh $KH "gunzip | sudo -u postgres psql -q -d spechomes >/dev/null 2>&1; \
-      sudo -u postgres psql -d spechomes -c 'GRANT ALL ON ALL TABLES IN SCHEMA public TO spechomes; GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO spechomes;' >/dev/null; \
-      echo -n '  '; sudo -u postgres psql -d spechomes -tAc \"SELECT 'homes='||count(*) FROM \\\"InventoryHome\\\" WHERE status='PUBLISHED'\""
+echo "──────── 2/6  Postgres role + CLEAN db ────────"
+# Drop+recreate for a faithful mirror (restoring onto an existing DB duplicates rows / fails PKs).
+ssh $KH "sudo -u postgres psql -v ON_ERROR_STOP=1 \
+  -c \"DO \\\$\\\$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='homesonspec') THEN CREATE ROLE homesonspec LOGIN PASSWORD '$DBPW'; ELSE ALTER ROLE homesonspec PASSWORD '$DBPW'; END IF; END \\\$\\\$;\" \
+  -c \"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='homesonspec' AND pid<>pg_backend_pid();\" \
+  -c 'DROP DATABASE IF EXISTS homesonspec;' \
+  -c 'CREATE DATABASE homesonspec OWNER homesonspec;'; \
+  sudo -u postgres psql -d homesonspec -c 'CREATE EXTENSION IF NOT EXISTS pg_trgm;' >/dev/null"
 
-echo "──────── 3/6  Ship code (rsync) ────────"
-ssh $KH "mkdir -p $REMOTE"
-rsync -az --delete \
-  --exclude node_modules --exclude .git --exclude '.env*' --exclude var/snapshots \
-  --exclude '.next' --exclude 'test-results' --exclude 'playwright-report' \
-  ~/Projects/spechomes/ $KH:$REMOTE/
+echo "──────── 3/6  Migrate data (pg_dump Mac → Kamatera) + GRANTS ────────"
+pg_dump --no-owner --no-acl -d "$SRC_DB" | gzip | ssh $KH "gunzip | sudo -u postgres psql -q -d homesonspec >/tmp/hos_restore.log 2>&1; \
+  sudo -u postgres psql -d homesonspec -v ON_ERROR_STOP=1 \
+    -c 'GRANT ALL ON DATABASE homesonspec TO homesonspec;' \
+    -c 'GRANT ALL ON SCHEMA public TO homesonspec;' \
+    -c 'GRANT ALL ON ALL TABLES IN SCHEMA public TO homesonspec;' \
+    -c 'GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO homesonspec;' \
+    -c \"DO \\\$\\\$ BEGIN IF EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name='pgboss') THEN EXECUTE 'GRANT ALL ON SCHEMA pgboss TO homesonspec'; EXECUTE 'GRANT ALL ON ALL TABLES IN SCHEMA pgboss TO homesonspec'; EXECUTE 'GRANT ALL ON ALL SEQUENCES IN SCHEMA pgboss TO homesonspec'; EXECUTE 'GRANT ALL ON ALL FUNCTIONS IN SCHEMA pgboss TO homesonspec'; END IF; END \\\$\\\$;\" >/dev/null; \
+  echo -n '  homes='; sudo -u postgres psql -d homesonspec -tAc 'SELECT count(*) FROM \"InventoryHome\"'"
 
-echo "──────── 4/6  Remote env + install + build ────────"
+echo "──────── 4/6  Ship code + remote env + build ────────"
+ssh $KH "mkdir -p $REMOTE"
+rsync -az --delete --exclude node_modules --exclude .git --exclude '.env*' --exclude var/snapshots \
+  --exclude '.next' --exclude 'test-results' --exclude 'playwright-report' --exclude tmp \
+  ~/Projects/homesonspec/ $KH:$REMOTE/
 ssh $KH "cat > $REMOTE/.env <<EOF
-DATABASE_URL=postgresql://spechomes:$DBPW@localhost/spechomes
+DATABASE_URL=postgresql://homesonspec:$DBPW@localhost/homesonspec
 BASIC_AUTH=$ADMIN_AUTH
 SNAPSHOT_DIR=$REMOTE/var/snapshots
 NODE_ENV=production
 EOF
 ln -sf $REMOTE/.env $REMOTE/apps/web/.env
 ln -sf $REMOTE/.env $REMOTE/apps/admin/.env
-cd $REMOTE
-export PATH=\$PATH:/root/.npm-global/bin:/usr/local/bin
-corepack enable 2>/dev/null || npm i -g pnpm@11 2>/dev/null || true
-pnpm install --config.confirmModulesPurge=false 2>&1 | tail -3
-pnpm --filter @spechomes/database db:generate 2>&1 | tail -2
-pnpm --filter @spechomes/web build 2>&1 | tail -4
-pnpm --filter @spechomes/admin build 2>&1 | tail -4"
+ln -sf $REMOTE/.env $REMOTE/apps/workers/.env
+mkdir -p $REMOTE/var/snapshots
+cd $REMOTE; export PATH=\$PATH:/root/.npm-global/bin:/usr/local/bin; corepack enable 2>/dev/null || true
+pnpm install --config.confirmModulesPurge=false 2>&1 | tail -2
+pnpm --filter @homesonspec/database db:generate 2>&1 | tail -1
+pnpm --filter @homesonspec/web build 2>&1 | tail -3
+pnpm --filter @homesonspec/admin build 2>&1 | tail -3"
 
-echo "──────── 5/6  pm2 (web :$WEB_PORT, admin :$ADMIN_PORT) ────────"
-ssh $KH "cd $REMOTE; export PATH=\$PATH:/root/.npm-global/bin:/usr/local/bin
-set -a; . $REMOTE/.env; set +a   # DATABASE_URL/BASIC_AUTH into env pm2 captures (env-precedence gotcha)
-pm2 delete spechomes-web spechomes-admin 2>/dev/null || true
-PORT=$WEB_PORT pm2 start 'pnpm --filter @spechomes/web start' --name spechomes-web --time
-PORT=$ADMIN_PORT pm2 start 'pnpm --filter @spechomes/admin start' --name spechomes-admin --time
-pm2 save
-sleep 6
+echo "──────── 5/6  pm2 (explicit PORT — the bug that 502'd) ────────"
+ssh $KH "cd $REMOTE; export PATH=\$PATH:/root/.npm-global/bin:/usr/local/bin; set -a; . $REMOTE/.env; set +a
+pm2 delete homesonspec-web homesonspec-admin homesonspec-workers 2>/dev/null || true
+PORT=$WEB_PORT   pm2 start 'pnpm --filter @homesonspec/web start'   --name homesonspec-web   --time
+PORT=$ADMIN_PORT pm2 start 'pnpm --filter @homesonspec/admin start' --name homesonspec-admin --time
+cd $REMOTE/apps/workers; pm2 start src/index.ts --name homesonspec-workers --interpreter node --interpreter-args='--env-file=$REMOTE/.env --import tsx' --time
+pm2 save; sleep 8
 echo -n '  web  '; curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:$WEB_PORT/
 echo -n '  admin(401 expected) '; curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:$ADMIN_PORT/"
 
-echo "──────── 6/6  nginx vhosts + origin TLS (self-signed; CF SSL=Full encrypts the hop) ────────"
+echo "──────── 6/6  nginx vhost (LE cert on public block; loopback proxy) ────────"
+# Assumes the LE cert already exists (see cert-homesonspec.sh). Keep LE — do NOT swap to self-signed.
 ssh $KH "
-# pre-flight: warn on a wildcard default_server that could hijack direct-IP hits (contrarian #3)
-DSRV=\$(grep -rl 'default_server' /etc/nginx/sites-enabled/ 2>/dev/null | grep -v spechomes || true)
-[ -n \"\$DSRV\" ] && echo \"  ⚠ default_server present in: \$DSRV — spechomes uses IP-bound listen so named requests match, but note it.\" || echo '  ✓ no competing default_server'
-# self-signed origin cert (CF→origin encryption; upgrade to CF Origin CA / LE later)
-mkdir -p /etc/nginx/ssl
-[ -f /etc/nginx/ssl/spechomes.crt ] || openssl req -x509 -nodes -newkey rsa:2048 -days 3650 \
-  -keyout /etc/nginx/ssl/spechomes.key -out /etc/nginx/ssl/spechomes.crt \
-  -subj '/CN=spechomes.com' 2>/dev/null
-cat > /etc/nginx/sites-available/spechomes.com <<'NGINX'
+cat > /etc/nginx/sites-available/homesonspec.com <<'NGINX'
 server {
     listen 45.61.58.125:80;
+    server_name homesonspec.com www.homesonspec.com;
+    location /.well-known/acme-challenge/ { root /var/www/certbot; }
+    location / { return 301 https://\$host\$request_uri; }
+}
+server {
     listen 45.61.58.125:443 ssl;
     server_name homesonspec.com www.homesonspec.com;
-    ssl_certificate     /etc/nginx/ssl/spechomes.crt;
-    ssl_certificate_key /etc/nginx/ssl/spechomes.key;
+    ssl_certificate     /etc/letsencrypt/live/homesonspec.com/fullchain.pem;
+    ssl_certificate_key /etc/letsencrypt/live/homesonspec.com/privkey.pem;
     location / {
         proxy_pass http://127.0.0.1:$WEB_PORT;
-        proxy_set_header Host \$host;
-        proxy_set_header X-Real-IP \$remote_addr;
-        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
-        proxy_set_header X-Forwarded-Proto \$scheme;
+        proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr;
+        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme;
     }
 }
-# admin: origin-only, NOT in public DNS (contrarian #6 — never publicly brute-forceable)
 server {
-    listen 45.61.58.125:80;
     listen 45.61.58.125:443 ssl;
-    server_name admin.spechomes.com;
+    server_name admin.homesonspec.com admin.spechomes.com;
     ssl_certificate     /etc/nginx/ssl/spechomes.crt;
     ssl_certificate_key /etc/nginx/ssl/spechomes.key;
     location / {
         proxy_pass http://127.0.0.1:$ADMIN_PORT;
-        proxy_set_header Host \$host;
-        proxy_set_header X-Real-IP \$remote_addr;
-        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
-        proxy_set_header X-Forwarded-Proto \$scheme;
+        proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr;
+        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme;
     }
 }
 NGINX
-ln -sf /etc/nginx/sites-available/spechomes.com /etc/nginx/sites-enabled/spechomes.com
-nginx -t && systemctl reload nginx
-echo -n '  origin via Host header (HTTP): '; curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: spechomes.com' http://127.0.0.1/
-echo -n '  extension check: '; sudo -u postgres psql -d spechomes -tAc \"SELECT extname FROM pg_extension WHERE extname='pg_trgm'\""
+ln -sf /etc/nginx/sites-available/homesonspec.com /etc/nginx/sites-enabled/homesonspec.com
+nginx -t && systemctl reload nginx"
 
 echo ""
-echo "✅  Kamatera origin is live (HTTP+HTTPS, admin origin-only). Tell Claude 'origin ready' → Cloudflare + GoDaddy."
+echo "✅  homesonspec.com self-hosted on Kamatera (web :$WEB_PORT, admin :$ADMIN_PORT, workers). Admin creds in $EK."

← e118a5b build-loop: max-it — fan out 4 adapters in parallel (per-dom  ·  back to Homesonspec  ·  build-loop: honor injected DATABASE_URL (portable to Kamater 89a47a0 →