nkbmusic/server/deploy.sh
metatroncubeswdev d368880601 Add NK Admin web container on 127.0.0.1:18080 with placeholder page
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 10:51:14 -04:00

207 lines
8.5 KiB
Bash

#!/bin/sh
# One-command deploy of the Nanga Kathai backend. Run it again any time to apply
# updates (new migrations, config changes); existing secrets are kept.
#
# sh deploy.sh app.nkbmusic.com you@example.com
#
# Arguments: <domain> [admin-email]
#
# Two modes (NK_PROXY):
# external (default) The server already has a web server (nginx, HestiaCP...).
# Supabase listens on 127.0.0.1:18000 and you point the
# domain's reverse proxy at it. Needs docker + git, no sudo.
# caddy Fresh server with nothing on ports 80/443. Run with sudo:
# installs Docker, a firewall, and Caddy gets HTTPS itself.
#
# Optional environment:
# REPO_URL git URL of this repository: cloned (or pulled) and run from there
# NK_DIR install folder (default ~/nk-backend, /opt/nk-backend with caddy)
# API_GW_HTTP_PORT local port for the reverse proxy (default 18000)
# SMTP_HOST SMTP_PORT SMTP_USER SMTP_PASS SMTP_ADMIN_EMAIL
# email for sign-in codes; asked for if not set
set -eu
DOMAIN="${1:-}"
ADMIN_EMAIL="${2:-}"
export NK_PROXY="${NK_PROXY:-external}"
export API_GW_HTTP_PORT="${API_GW_HTTP_PORT:-18000}"
say() { printf '\n==> %s\n' "$*"; }
die() { printf 'Error: %s\n' "$*" >&2; exit 1; }
[ -n "$DOMAIN" ] || die "usage: sh deploy.sh <domain> [admin-email]"
if [ "$NK_PROXY" = caddy ]; then
[ "$(id -u)" = 0 ] || die "caddy mode installs packages: run with sudo"
NK_DIR="${NK_DIR:-/opt/nk-backend}"
BACKUPS="/opt/nk-backups"
API="https://$DOMAIN"
else
NK_DIR="${NK_DIR:-$HOME/nk-backend}"
BACKUPS="$HOME/nk-backups"
API="http://127.0.0.1:$API_GW_HTTP_PORT"
fi
PROJECT="$NK_DIR/supabase-project"
# --- 1. System (caddy mode only) ---------------------------------------------
if [ "$NK_PROXY" = caddy ]; then
say "Installing system packages"
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq git curl ca-certificates dnsutils ufw cron >/dev/null
command -v docker >/dev/null 2>&1 || curl -fsSL https://get.docker.com | sh >/dev/null
systemctl enable --now docker >/dev/null 2>&1 || true
say "Firewall: allowing SSH, 80 and 443 only"
SSH_PORT="$(ss -tlnpH 2>/dev/null | awk '/sshd/ {sub(/.*:/, "", $4); print $4; exit}')"
for rule in "${SSH_PORT:-22}/tcp" 22/tcp 80/tcp 443/tcp 443/udp; do ufw allow "$rule" >/dev/null; done
ufw --force enable >/dev/null
fi
for cmd in git curl docker; do
command -v "$cmd" >/dev/null 2>&1 || die "'$cmd' is not installed"
done
docker compose version >/dev/null 2>&1 || die "docker compose plugin missing"
docker ps >/dev/null 2>&1 || die "can't use docker: add this user to the docker group"
# --- 2. Code -----------------------------------------------------------------
if [ -n "${REPO_URL:-}" ] && [ "${NK_FROM_CLONE:-}" != 1 ]; then
CODE="${NK_CODE_DIR:-$(dirname "$NK_DIR")/nanga_kathai}"
say "Fetching the code from $REPO_URL into $CODE"
if [ -d "$CODE/.git" ]; then git -C "$CODE" pull --ff-only --quiet
else git clone --quiet "$REPO_URL" "$CODE"; fi
NK_FROM_CLONE=1 exec sh "$CODE/server/deploy.sh" "$@"
fi
HERE="$(cd "$(dirname "$0")" && pwd)"
[ -d "$HERE/../supabase/migrations" ] || die "can't find supabase/migrations next to server/"
# --- 3. Port / DNS checks ----------------------------------------------------
if [ ! -f "$PROJECT/.env" ] && [ "$NK_PROXY" != caddy ] \
&& ss -tln | grep -qE "[:.]$API_GW_HTTP_PORT\b"; then
die "port $API_GW_HTTP_PORT is already in use; set API_GW_HTTP_PORT to a free port"
fi
DNS_IP="$(getent ahostsv4 "$DOMAIN" | awk 'NR==1 {print $1}')"
[ -n "$DNS_IP" ] || echo "Warning: $DOMAIN has no DNS record yet."
# --- 4. Supabase + NK config -------------------------------------------------
if [ ! -f "$PROJECT/.env" ]; then
say "Installing Supabase (first run) into $PROJECT"
sh "$HERE/install.sh" "$DOMAIN" "$NK_DIR"
else
say "Existing install found: refreshing the NK config"
cp -r "$HERE/caddy" "$HERE/email-templates" "$HERE/sql" "$HERE/nginx" "$PROJECT/nk/"
cp "$HERE/docker-compose.nk.yml" "$HERE/docker-compose.nk-caddy.yml" "$PROJECT/"
fi
cd "$PROJECT"
# Admin website: the placeholder until a real build has been published (deploy-admin.sh).
if [ ! -f nk/admin/index.html ]; then
mkdir -p nk/admin
cp "$HERE/admin-placeholder/index.html" nk/admin/
fi
env_get() { grep -E "^$1=" .env | head -n1 | cut -d= -f2-; }
env_set() { # safe for any characters in the value
K="$1" V="$2" awk 'BEGIN { k = ENVIRON["K"]; v = ENVIRON["V"]; done = 0 }
index($0, k "=") == 1 { print k "=" v; done = 1; next } { print }
END { if (!done) print k "=" v }' .env > .env.tmp
cat .env.tmp > .env && rm .env.tmp
}
# --- 5. Email (sign-in codes) ------------------------------------------------
ask() { # ask VAR "question" [default]
eval "cur=\${$1:-}"
if [ -z "$cur" ] && [ -t 0 ]; then
printf '%s%s: ' "$2" "${3:+ [$3]}"; read -r cur
[ -n "$cur" ] || cur="${3:-}"
fi
eval "$1=\$cur"
}
if [ "$(env_get SMTP_HOST)" = "supabase-mail" ] || [ -n "${SMTP_HOST:-}" ]; then
say "Email settings for sign-in codes (Enter on SMTP host to skip for now)"
ask SMTP_HOST "SMTP host"
if [ -n "$SMTP_HOST" ]; then
ask SMTP_PORT "SMTP port" 587
ask SMTP_USER "SMTP user" "no-reply@${DOMAIN#*.}"
ask SMTP_PASS "SMTP password"
ask SMTP_ADMIN_EMAIL "Send codes from" "$SMTP_USER"
env_set SMTP_HOST "$SMTP_HOST"
env_set SMTP_PORT "$SMTP_PORT"
env_set SMTP_USER "$SMTP_USER"
env_set SMTP_PASS "$SMTP_PASS"
env_set SMTP_ADMIN_EMAIL "$SMTP_ADMIN_EMAIL"
env_set SMTP_SENDER_NAME "Nanga Kathai"
else
echo "Skipped: sign-in emails won't send until you run deploy.sh again with SMTP details."
fi
fi
# --- 6. Start ----------------------------------------------------------------
say "Starting the backend (first start takes a few minutes)"
sh run.sh start
# --- 7. Database -------------------------------------------------------------
say "Creating tables, access rules and storage buckets"
sh "$HERE/apply-migrations.sh" "$PROJECT"
# --- 8. API check ------------------------------------------------------------
say "Checking the API at $API"
n=0
until curl -fsS -o /dev/null "$API/auth/v1/health" -H "apikey: $(env_get ANON_KEY)"; do
n=$((n + 1))
[ $n -lt 30 ] || { echo "API not answering (see: sh run.sh logs api-gw)"; break; }
sleep 5
done
# --- 9. Admin account --------------------------------------------------------
if [ -n "$ADMIN_EMAIL" ]; then
say "Making $ADMIN_EMAIL an admin"
SERVICE_KEY="$(env_get SERVICE_ROLE_KEY)"
curl -sS -o /dev/null -X POST "$API/auth/v1/admin/users" \
-H "apikey: $SERVICE_KEY" -H "Authorization: Bearer $SERVICE_KEY" \
-H "Content-Type: application/json" \
-d "{\"email\": \"$ADMIN_EMAIL\", \"email_confirm\": true}" || true # 422 = already exists
docker compose exec -T -e PGPASSWORD="$(env_get POSTGRES_PASSWORD)" db \
psql -h localhost -U postgres -d postgres -qtA -c \
"update public.profiles set role = 'admin'
where id = (select id from auth.users where email = lower('$ADMIN_EMAIL'))
returning 'admin set';" | grep -q 'admin set' \
|| echo "Couldn't set admin yet. Sign in once from the app, then run deploy.sh again."
fi
# --- 10. Backups -------------------------------------------------------------
say "Scheduling nightly backups to $BACKUPS"
mkdir -p "$BACKUPS"
CRON="30 2 * * * sh $HERE/backup.sh $PROJECT $BACKUPS >> $BACKUPS/backup.log 2>&1"
( crontab -l 2>/dev/null | grep -v 'nk-backups' || true; echo "$CRON" ) | crontab -
# --- Done --------------------------------------------------------------------
PROXY_NOTE=""
[ "$NK_PROXY" = caddy ] || PROXY_NOTE="
Reverse proxy (HestiaCP: Web > domain > Proxy, or a custom nginx proxy template):
https://$DOMAIN -> http://127.0.0.1:$API_GW_HTTP_PORT (API + Studio)
https://admin.${DOMAIN#*.} -> http://127.0.0.1:${NK_ADMIN_PORT:-18080} (NK Admin)
"
cat <<EOF
========================================================================
Nanga Kathai backend is running.
$PROXY_NOTE
Admin dashboard : https://$DOMAIN
user : $(env_get DASHBOARD_USERNAME)
password : $(env_get DASHBOARD_PASSWORD)
App env.json (safe to put in the app):
{
"SUPABASE_URL": "https://$DOMAIN",
"SUPABASE_PUBLISHABLE_KEY": "$(env_get SUPABASE_PUBLISHABLE_KEY)",
"MEDIA_BASE_URL": "https://$DOMAIN/storage/v1/object/public/audio"
}
For the song importer only (keep secret, never in the app):
NK_URL=https://$DOMAIN
NK_SERVICE_KEY=$(env_get SERVICE_ROLE_KEY)
Secrets live in $PROJECT/.env. Copy $BACKUPS off the server regularly.
========================================================================
EOF