← 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 →