2026-07-22 13:06:08 -07:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
# Fresh / demo stack bring-up driven almost entirely by .env
|
|
|
|
|
#
|
|
|
|
|
# cp .env.example .env # fill usenet keys + STACK_HOST
|
|
|
|
|
# # drop wireguard/wg0.conf
|
|
|
|
|
# ./scripts/bootstrap-stack.sh
|
|
|
|
|
#
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
|
|
STACK_DIR="${STACK_DIR:-$(cd "$SCRIPT_DIR/.." && pwd)}"
|
|
|
|
|
cd "$STACK_DIR"
|
|
|
|
|
|
|
|
|
|
log() { echo "[bootstrap] $*"; }
|
|
|
|
|
die() { log "ERROR: $*"; exit 1; }
|
|
|
|
|
|
|
|
|
|
[[ -f "$STACK_DIR/.env" ]] || die "missing .env — cp .env.example .env and edit"
|
|
|
|
|
|
|
|
|
|
set -a
|
|
|
|
|
# shellcheck disable=SC1091
|
|
|
|
|
source "$STACK_DIR/.env"
|
|
|
|
|
set +a
|
|
|
|
|
|
|
|
|
|
# Expand nested ${STACK_HOST} style leftovers if someone left them literal
|
|
|
|
|
STACK_HOST="${STACK_HOST:-192.168.8.123}"
|
|
|
|
|
DEMO_USER="${DEMO_USER:-tim}"
|
|
|
|
|
DEMO_PASS="${DEMO_PASS:-asdfasdf}"
|
|
|
|
|
CONFIG_DIR="${CONFIG_DIR:-$STACK_DIR/config}"
|
|
|
|
|
DATA_DIR="${DATA_DIR:-$STACK_DIR/data}"
|
|
|
|
|
BONSAI_MODELS_DIR="${BONSAI_MODELS_DIR:-$STACK_DIR/models/bonsai}"
|
|
|
|
|
AI_WORKSPACE_DIR="${AI_WORKSPACE_DIR:-$STACK_DIR/workspace}"
|
|
|
|
|
DOWNLOAD_MODEL="${DOWNLOAD_MODEL:-true}"
|
|
|
|
|
CONFIGURE_USENET="${CONFIGURE_USENET:-true}"
|
|
|
|
|
HEAL_GLUETUN="${HEAL_GLUETUN:-true}"
|
|
|
|
|
COMPOSE_PULL="${COMPOSE_PULL:-true}"
|
|
|
|
|
START_OPENHANDS="${START_OPENHANDS:-false}"
|
|
|
|
|
BONSAI_BACKEND="${BONSAI_BACKEND:-vulkan}"
|
|
|
|
|
|
|
|
|
|
export STACK_DIR CONFIG_DIR DATA_DIR BONSAI_MODELS_DIR AI_WORKSPACE_DIR
|
|
|
|
|
|
2026-07-22 19:00:59 -07:00
|
|
|
log "STACK_DIR=$STACK_DIR host=$STACK_HOST demo_user=$DEMO_USER profile=${STACK_PROFILE:-unset}"
|
|
|
|
|
|
|
|
|
|
# Optional: re-apply named profile before bootstrap
|
|
|
|
|
if [[ -n "${STACK_PROFILE:-}" && -f "$STACK_DIR/profiles/${STACK_PROFILE}.env" ]]; then
|
|
|
|
|
log "Re-applying profile ${STACK_PROFILE}"
|
|
|
|
|
"$SCRIPT_DIR/apply-profile.sh" "$STACK_PROFILE"
|
|
|
|
|
set -a
|
|
|
|
|
# shellcheck disable=SC1091
|
|
|
|
|
source "$STACK_DIR/.env"
|
|
|
|
|
set +a
|
|
|
|
|
fi
|
2026-07-22 13:06:08 -07:00
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Directories
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
log "Creating data / config trees"
|
|
|
|
|
mkdir -p \
|
|
|
|
|
"$CONFIG_DIR" \
|
|
|
|
|
"$BONSAI_MODELS_DIR" \
|
|
|
|
|
"$AI_WORKSPACE_DIR" \
|
|
|
|
|
"$DATA_DIR"/media/{movies,tv,music} \
|
|
|
|
|
"$DATA_DIR"/torrents/{movies,tv,music} \
|
|
|
|
|
"$DATA_DIR"/usenet/incomplete \
|
|
|
|
|
"$DATA_DIR"/usenet/complete/{movies,tv,music} \
|
|
|
|
|
"$STACK_DIR/wireguard" \
|
|
|
|
|
"$STACK_DIR/secrets"
|
|
|
|
|
|
|
|
|
|
# Prefer main .env for usenet if secrets/.env missing
|
|
|
|
|
if [[ ! -f "$STACK_DIR/secrets/.env" && -f "$STACK_DIR/.env" ]]; then
|
|
|
|
|
log "Using root .env as secrets source for usenet (DEMO_MODE)"
|
|
|
|
|
# configure-usenet already can source SECRETS_FILE; symlink for convenience
|
|
|
|
|
ln -sfn ../.env "$STACK_DIR/secrets/.env" 2>/dev/null || \
|
|
|
|
|
cp "$STACK_DIR/.env" "$STACK_DIR/secrets/.env"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [[ ! -f "$STACK_DIR/wireguard/wg0.conf" ]]; then
|
|
|
|
|
log "WARNING: wireguard/wg0.conf missing — gluetun/qbittorrent will stay unhealthy until you add it"
|
|
|
|
|
else
|
|
|
|
|
# Ensure EndpointHost comment for the watcher
|
|
|
|
|
if [[ -n "${WIREGUARD_ENDPOINT_HOST:-}" ]] && ! grep -q EndpointHost "$STACK_DIR/wireguard/wg0.conf"; then
|
|
|
|
|
sed -i "1a # EndpointHost = ${WIREGUARD_ENDPOINT_HOST}" "$STACK_DIR/wireguard/wg0.conf" || true
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Model download
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
if [[ "${DOWNLOAD_MODEL}" == "true" || "${DOWNLOAD_MODEL}" == "1" ]]; then
|
2026-07-22 19:00:59 -07:00
|
|
|
log "Downloading models for profile (see DOWNLOAD_BONSAI27 / DOWNLOAD_QWEN)…"
|
|
|
|
|
MODEL_DIR="$BONSAI_MODELS_DIR" "$SCRIPT_DIR/download-models.sh"
|
2026-07-22 13:06:08 -07:00
|
|
|
else
|
|
|
|
|
log "DOWNLOAD_MODEL=false — skip model download"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
2026-07-22 19:00:59 -07:00
|
|
|
# Docker compose — always deploy the full stack (no optional profiles)
|
2026-07-22 13:06:08 -07:00
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
if [[ "$COMPOSE_PULL" == "true" || "$COMPOSE_PULL" == "1" ]]; then
|
|
|
|
|
log "docker compose pull"
|
2026-07-22 19:00:59 -07:00
|
|
|
docker compose pull || true
|
2026-07-22 13:06:08 -07:00
|
|
|
fi
|
|
|
|
|
|
2026-07-22 19:00:59 -07:00
|
|
|
log "Building bonsai (BONSAI_BACKEND=$BONSAI_BACKEND NGL=${BONSAI_NGL:-} model=${BONSAI_MAIN_FILE:-})"
|
2026-07-22 13:06:08 -07:00
|
|
|
docker compose build bonsai
|
|
|
|
|
|
2026-07-22 19:00:59 -07:00
|
|
|
log "docker compose up -d (all services)"
|
|
|
|
|
docker compose up -d
|
2026-07-22 13:06:08 -07:00
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Heal gluetun if needed
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
if [[ "$HEAL_GLUETUN" == "true" || "$HEAL_GLUETUN" == "1" ]]; then
|
|
|
|
|
if [[ -x "$SCRIPT_DIR/gluetun-endpoint-watch.sh" ]]; then
|
|
|
|
|
log "Running gluetun endpoint watch (no-op if healthy)"
|
|
|
|
|
STACK_DIR="$STACK_DIR" "$SCRIPT_DIR/gluetun-endpoint-watch.sh" || log "gluetun watch returned non-zero (VPN may still need wg0.conf)"
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Usenet bootstrap
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
if [[ "$CONFIGURE_USENET" == "true" || "$CONFIGURE_USENET" == "1" ]]; then
|
|
|
|
|
log "Waiting for SABnzbd…"
|
|
|
|
|
for i in $(seq 1 60); do
|
|
|
|
|
if curl -fsS "http://127.0.0.1:${SABNZBD_PORT:-8085}/api?mode=version&output=json" >/dev/null 2>&1; then
|
|
|
|
|
break
|
|
|
|
|
fi
|
|
|
|
|
sleep 2
|
|
|
|
|
done
|
|
|
|
|
if [[ -x "$SCRIPT_DIR/configure-usenet.sh" ]]; then
|
|
|
|
|
SECRETS_FILE="${SECRETS_FILE:-$STACK_DIR/secrets/.env}" \
|
|
|
|
|
SAB_URL="http://127.0.0.1:${SABNZBD_PORT:-8085}" \
|
|
|
|
|
"$SCRIPT_DIR/configure-usenet.sh" || log "configure-usenet failed — check SABnzbd wizard / secrets"
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Summary
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
log ""
|
|
|
|
|
log "=== Stack is up (or starting) ==="
|
|
|
|
|
docker compose ps --format 'table {{.Name}}\t{{.Status}}\t{{.Ports}}' 2>/dev/null || docker compose ps
|
|
|
|
|
log ""
|
|
|
|
|
log "Landing: http://${STACK_HOST}/"
|
|
|
|
|
log "Jellyfin: http://${STACK_HOST}:8096"
|
|
|
|
|
log "Open WebUI: http://${STACK_HOST}:3000 (first user = admin; use ${DEMO_USER} / ${DEMO_PASS})"
|
|
|
|
|
log "Forgejo: http://${STACK_HOST}:3002 (install wizard → admin ${DEMO_USER} / ${DEMO_PASS})"
|
|
|
|
|
log "SABnzbd: http://${STACK_HOST}:${SABNZBD_PORT:-8085}"
|
|
|
|
|
log "Prowlarr: http://${STACK_HOST}:9696"
|
|
|
|
|
log "Bonsai: http://${STACK_HOST}:${BONSAI_PORT:-8081}/v1/models"
|
|
|
|
|
log ""
|
|
|
|
|
log "Still one-time UI wiring (APIs vary by version):"
|
|
|
|
|
log " • Prowlarr → NZBgeek + SABnzbd download client → sync to apps"
|
|
|
|
|
log " • Sonarr/Radarr/Lidarr → root folders /data/media/* + SABnzbd categories"
|
|
|
|
|
log " • qBittorrent → categories + password ${DEMO_PASS} after first login"
|
|
|
|
|
log " • Jellyfin → libraries under /data/media/*"
|
|
|
|
|
log ""
|
|
|
|
|
log "Demo login for apps you configure: user=${DEMO_USER} pass=${DEMO_PASS}"
|