← back to NationalPaperHangers
scripts/deploy-kamatera-v2.sh
146 lines
#!/usr/bin/env bash
#
# v2 — fixes the two failures from v1 (SESSION_SECRET missing, DATABASE_URL
# missing). Idempotent and non-destructive: if a value already exists in the
# remote .env, we keep it; we only ADD missing keys.
#
# What v2 adds over v1:
# • Generates a strong SESSION_SECRET if absent
# • Probes Kamatera PG for a national_paper_hangers DB; creates DB + role
# if absent (uses local-trust postgres superuser, no exposed password)
# • Writes a non-clobbering .env merge using a small awk pass on the host
# • Then runs the same nginx + cert + restart sequence as v1
#
# Usage (after Steve says "go"):
# bash ~/Projects/NationalPaperHangers/scripts/deploy-kamatera-v2.sh
set -euo pipefail
REMOTE="${NPH_REMOTE_HOST:-root@45.61.58.125}"
RDIR="${NPH_REMOTE_DIR:-/root/Projects/NationalPaperHangers}"
PORT="${NPH_PORT:-9765}"
DOMAIN="${NPH_DOMAIN:-nationalpaperhangers.com}"
EMAIL="${NPH_LE_EMAIL:-steve@designerwallcoverings.com}"
LOCAL_DIR="$HOME/Projects/NationalPaperHangers"
# Generated locally (never echoed); pushed to Kamatera only if .env doesn't
# already have a SESSION_SECRET.
SESSION_SECRET="$(openssl rand -hex 48)"
# DB password: same pattern — generate once locally, push only if absent.
DB_PASSWORD="$(openssl rand -hex 24)"
say() { printf '\n[deploy] %s\n' "$*"; }
# --- 1. Sync code (idempotent — same as v1) -------------------------------
say "Sync code → $REMOTE:$RDIR ..."
rsync -avz --delete \
--exclude node_modules --exclude .env --exclude .env.local \
--exclude tmp --exclude '*.log' --exclude .git \
"$LOCAL_DIR/" "$REMOTE:$RDIR/" >/dev/null
echo " done"
# --- 2. Provision PG role + database if absent ----------------------------
say "Probing PG for national_paper_hangers DB + role ..."
# Generated DB_PASSWORD pushed via stdin so it never lands in argv.
ssh "$REMOTE" "cd $RDIR && \
ROLE_EXISTS=\$(sudo -u postgres psql -tAc \"SELECT 1 FROM pg_roles WHERE rolname='nph'\" 2>/dev/null); \
DB_EXISTS=\$(sudo -u postgres psql -tAc \"SELECT 1 FROM pg_database WHERE datname='national_paper_hangers'\" 2>/dev/null); \
if [ \"\$ROLE_EXISTS\" != \"1\" ]; then \
read -r PWD; \
sudo -u postgres psql -v ON_ERROR_STOP=1 -c \"CREATE ROLE nph LOGIN PASSWORD '\$PWD';\" >/dev/null; \
echo 'created role nph'; \
else echo 'role nph already exists, keeping existing password'; fi; \
if [ \"\$DB_EXISTS\" != \"1\" ]; then \
sudo -u postgres createdb --owner=nph national_paper_hangers; \
sudo -u postgres psql -d national_paper_hangers -c 'CREATE EXTENSION IF NOT EXISTS pgcrypto;'; \
echo 'created database'; \
else echo 'database already exists'; fi" <<< "$DB_PASSWORD"
# --- 3. Merge .env on Kamatera (non-clobbering) ---------------------------
say "Merging .env on Kamatera (only adds missing keys) ..."
# Build the desired-key block locally; ship via stdin; awk merges in-place.
ENV_BLOCK="$(cat <<EOF
NODE_ENV=production
PORT=$PORT
PUBLIC_URL=https://$DOMAIN
SESSION_SECRET=$SESSION_SECRET
PGHOST=127.0.0.1
PGPORT=5432
PGUSER=nph
PGPASSWORD=$DB_PASSWORD
PGDATABASE=national_paper_hangers
DATABASE_URL=postgres://nph:$DB_PASSWORD@127.0.0.1:5432/national_paper_hangers
EMAIL_FROM=info@$DOMAIN
EMAIL_FROM_NAME=National Paper Hangers
EOF
)"
ssh "$REMOTE" "cd $RDIR && \
touch .env && \
awk -v new=\"\$(cat)\" 'BEGIN{n=split(new,arr,\"\\n\"); for(i=1;i<=n;i++){split(arr[i],kv,\"=\"); if(kv[1]) want[kv[1]]=arr[i]}} { for(k in want){ if(\$0 ~ \"^\"k\"=\"){ delete want[k]; break } } print } END { for(k in want) print want[k] }' .env > .env.new && \
mv .env.new .env && \
chmod 600 .env && \
echo \"keys now in .env: \$(grep -c '^[A-Z]' .env)\"" <<< "$ENV_BLOCK"
# --- 4. Migrations --------------------------------------------------------
say "Running all migrations against fresh DB ..."
ssh "$REMOTE" "cd $RDIR && \
set -a && . .env && set +a && \
for f in db/schema.sql db/migrations/*.sql; do \
echo \" applying \$f\"; \
psql \"\$DATABASE_URL\" -v ON_ERROR_STOP=1 -q -f \"\$f\" 2>&1 | tail -3; \
done"
# --- 5. nginx vhost -------------------------------------------------------
say "Installing nginx vhost for $DOMAIN ..."
ssh "$REMOTE" "cat > /etc/nginx/sites-available/$DOMAIN <<'CONF'
server {
listen 80;
listen [::]:80;
server_name $DOMAIN www.$DOMAIN;
location /.well-known/acme-challenge/ { root /var/www/letsencrypt; }
location /uploads/ {
alias $RDIR/public/uploads/;
access_log off; expires 30d;
add_header Cache-Control \"public, immutable\";
}
location / {
proxy_pass http://127.0.0.1:$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;
client_max_body_size 12m;
}
}
CONF
mkdir -p /var/www/letsencrypt
ln -sf /etc/nginx/sites-available/$DOMAIN /etc/nginx/sites-enabled/$DOMAIN
find /etc/nginx/sites-enabled/ -maxdepth 1 -type f \( -name '*.bak' -o -name '*.old' \) -delete
nginx -t && systemctl reload nginx"
# --- 6. Install node deps + restart pm2 -----------------------------------
say "npm install + pm2 restart ..."
ssh "$REMOTE" "cd $RDIR && npm install --omit=dev --silent 2>&1 | tail -3 && \
(pm2 restart national-paper-hangers --update-env 2>/dev/null || \
pm2 start ecosystem.kamatera.config.js --env production --update-env)"
sleep 3
say "Local probe via 127.0.0.1 ..."
ssh "$REMOTE" "curl -s -o /dev/null -w '127.0.0.1:$PORT/ → %{http_code}\\n' http://127.0.0.1:$PORT/" || true
# --- 7. Issue cert --------------------------------------------------------
say "certbot --nginx for $DOMAIN + www.$DOMAIN ..."
ssh "$REMOTE" "certbot --nginx -d $DOMAIN -d www.$DOMAIN \
--non-interactive --agree-tos -m $EMAIL --redirect --keep-until-expiring"
# --- 8. Smoke test --------------------------------------------------------
say "Live URLs ..."
sleep 2
for path in / /find /installer/atelier-bond-nyc /installer/atelier-bond-nyc/book; do
code=$(curl -s -o /dev/null -w '%{http_code}' "https://$DOMAIN$path" --max-time 12 || echo "TIMEOUT")
printf ' https://%s%s → %s\n' "$DOMAIN" "$path" "$code"
done
say "Done."