#!/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: [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 [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 (admin/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 <