#!/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 log "STACK_DIR=$STACK_DIR host=$STACK_HOST demo_user=$DEMO_USER" # --------------------------------------------------------------------------- # 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 if [[ ! -f "$BONSAI_MODELS_DIR/${BONSAI_MAIN_FILE:-Ternary-Bonsai-27B-Q2_0.gguf}" ]]; then log "Downloading Ternary-Bonsai GGUF (~7GB)…" MODEL_DIR="$BONSAI_MODELS_DIR" \ BONSAI_HF_REPO="${BONSAI_HF_REPO:-prism-ml/Ternary-Bonsai-27B-gguf}" \ BONSAI_MAIN_FILE="${BONSAI_MAIN_FILE:-Ternary-Bonsai-27B-Q2_0.gguf}" \ DOWNLOAD_VISION="${DOWNLOAD_VISION:-1}" \ DOWNLOAD_DRAFTER="${DOWNLOAD_DRAFTER:-0}" \ "$SCRIPT_DIR/download-bonsai-model.sh" else log "Model already present — skip download" fi else log "DOWNLOAD_MODEL=false — skip model download" fi # --------------------------------------------------------------------------- # Docker compose # --------------------------------------------------------------------------- PROFILES=() if [[ -n "${COMPOSE_PROFILES:-}" ]]; then # shellcheck disable=SC2206 PROFILES=(--profile ${COMPOSE_PROFILES//,/ --profile }) fi if [[ "$START_OPENHANDS" == "true" || "$START_OPENHANDS" == "1" ]]; then PROFILES+=(--profile coding) fi if [[ "$COMPOSE_PULL" == "true" || "$COMPOSE_PULL" == "1" ]]; then log "docker compose pull" docker compose "${PROFILES[@]}" pull || true fi log "Building bonsai (BONSAI_BACKEND=$BONSAI_BACKEND)" docker compose build bonsai log "docker compose up -d" docker compose "${PROFILES[@]}" up -d # --------------------------------------------------------------------------- # 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}"