#!/bin/bash # # Deploy Archipelago code to the HP ProDesk target # # Usage: # ./scripts/deploy-to-target.sh # Sync and rebuild # ./scripts/deploy-to-target.sh --quick # Sync only, no rebuild # ./scripts/deploy-to-target.sh --live # Deploy to live system (default: 192.168.1.228) # ./scripts/deploy-to-target.sh --both # Deploy to 228, then copy to 198 # ./scripts/deploy-to-target.sh --frontend-only # Frontend-only deploy (skip Rust build + container rebuilds) # ./scripts/deploy-to-target.sh --demo # Demo mode: Bitcoin pruning enabled (smaller disk) # ./scripts/deploy-to-target.sh --dry-run --live # Show what would be deployed without executing # set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" # Load deploy config (password etc.) - deploy-config.sh is gitignored [ -f "$SCRIPT_DIR/deploy-config.sh" ] && . "$SCRIPT_DIR/deploy-config.sh" # Configuration TARGET_HOST="${ARCHIPELAGO_TARGET:-archipelago@192.168.1.228}" TARGET_DIR="/home/archipelago/archy" SSH_KEY="${ARCHIPELAGO_SSH_KEY:-$HOME/.ssh/archipelago-deploy}" SSH_OPTS="-o StrictHostKeyChecking=no -o ServerAliveInterval=15 -o ServerAliveCountMax=4 -i $SSH_KEY" DEPLOY_START=$(date +%s) timestamp() { echo "[$(date +%H:%M:%S)]"; } echo "╔════════════════════════════════════════════════════════════════╗" echo "║ Deploying to Archipelago Target ║" echo "╚════════════════════════════════════════════════════════════════╝" echo "" echo "$(timestamp) Target: $TARGET_HOST" echo "" # Parse arguments QUICK=false LIVE=false BOTH=false FRONTEND_ONLY=false DEMO=false DRY_RUN=false for arg in "$@"; do case $arg in --quick) QUICK=true ;; --live) LIVE=true ;; --both) BOTH=true ;; --frontend-only) FRONTEND_ONLY=true; LIVE=true ;; --demo) DEMO=true ;; --dry-run) DRY_RUN=true ;; esac done # Dry run mode: show what would be deployed without executing if [[ "$DRY_RUN" == "true" ]]; then echo "═══ DRY RUN MODE — no changes will be made ═══" echo "" echo "Target: $TARGET_HOST" echo "Project: $PROJECT_DIR" echo "Mode: $( [[ "$BOTH" == "true" ]] && echo "both (.228 + .198)" || \ [[ "$LIVE" == "true" ]] && echo "live (.228)" || \ echo "dev (sync + build)" )" echo "" echo "Files that would be synced:" rsync -avn --exclude '.git' --exclude 'target' --exclude 'node_modules' \ --exclude 'dist' --exclude 'web/dist' --exclude '*.iso' \ "$PROJECT_DIR/" "$TARGET_HOST:$TARGET_DIR/" 2>/dev/null | \ grep -E '^[<>]|^deleting' | head -50 || echo " (rsync check failed — SSH may be unavailable)" echo "" echo "Frontend build: $( [[ "$QUICK" == "true" ]] && echo "SKIP (--quick)" || echo "vue-tsc + vite build" )" echo "Backend build: $( [[ "$FRONTEND_ONLY" == "true" ]] && echo "SKIP (--frontend-only)" || \ [[ "$QUICK" == "true" ]] && echo "SKIP (--quick)" || echo "cargo build --release" )" echo "Live deploy: $( [[ "$LIVE" == "true" || "$BOTH" == "true" ]] && echo "YES — binary + frontend + nginx + systemd" || echo "NO" )" echo "" echo "═══ DRY RUN COMPLETE — nothing was changed ═══" exit 0 fi # Section timing helper section_start() { SECTION_START=$(date +%s); } section_end() { local elapsed=$(($(date +%s) - SECTION_START)) echo " (${elapsed}s)" } # SSH connectivity pre-flight check echo "$(timestamp) Checking SSH connectivity..." if ! ssh $SSH_OPTS -o ConnectTimeout=5 "$TARGET_HOST" "echo ok" >/dev/null 2>&1; then echo " ERROR: Cannot connect to $TARGET_HOST" echo " Check that the server is on and reachable." exit 1 fi echo " Connected." # Install prerequisites if missing (rsync for code sync, python3 for Claude API proxy) echo "$(timestamp) Checking prerequisites..." ssh $SSH_OPTS "$TARGET_HOST" ' NEED_INSTALL="" NEED_INSTALL="" command -v rsync >/dev/null 2>&1 || NEED_INSTALL="$NEED_INSTALL rsync" command -v python3 >/dev/null 2>&1 || NEED_INSTALL="$NEED_INSTALL python3" if ! command -v node >/dev/null 2>&1 || ! command -v npm >/dev/null 2>&1; then echo " Node.js/npm not found — installing..." curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - 2>&1 | tail -3 NEED_INSTALL="$NEED_INSTALL nodejs" fi if [ -n "$NEED_INSTALL" ]; then echo " Installing:$NEED_INSTALL" sudo apt-get update -qq && sudo apt-get install -y -qq $NEED_INSTALL 2>&1 | tail -3 else echo " All prerequisites present" fi ' 2>&1 # Pre-deploy health check (informational — warns but does not block) TARGET_IP_ONLY="$(echo "$TARGET_HOST" | cut -d@ -f2)" PRE_HEALTH=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 5 "http://$TARGET_IP_ONLY/health" 2>/dev/null || echo "000") if [ "$PRE_HEALTH" = "200" ]; then echo " Server health: OK (200)" else echo " ⚠️ Server health: $PRE_HEALTH (may be down or unhealthy — deploying anyway)" fi echo "" # When --both: deploy to 228 first, then copy to 198 if [ "$BOTH" = true ]; then echo "Deploying to both servers (228, then 198)..." "$0" --live echo "" echo "📤 Copying to 192.168.1.198 (no rsync/cargo on that node)..." TARGET_198="archipelago@192.168.1.198" scp $SSH_OPTS archipelago@192.168.1.228:$TARGET_DIR/core/target/release/archipelago /tmp/archipelago-both 2>/dev/null || true scp $SSH_OPTS /tmp/archipelago-both "$TARGET_198:/tmp/archipelago-new" ssh $SSH_OPTS archipelago@192.168.1.228 "cd $TARGET_DIR && tar cf - web/dist/neode-ui 2>/dev/null" | ssh $SSH_OPTS "$TARGET_198" "mkdir -p /tmp/web-deploy && cd /tmp/web-deploy && tar xf -" ssh $SSH_OPTS "$TARGET_198" ' sudo systemctl stop archipelago sudo cp /tmp/archipelago-new /usr/local/bin/archipelago sudo chmod +x /usr/local/bin/archipelago rm -f /tmp/archipelago-new sudo find /opt/archipelago/web-ui -mindepth 1 -maxdepth 1 ! -name "aiui" ! -name "claude-login.html" -exec rm -rf {} + sudo cp -r /tmp/web-deploy/web/dist/neode-ui/* /opt/archipelago/web-ui/ 2>/dev/null || true sudo chown -R 1000:1000 /opt/archipelago/web-ui ' # Deploy AIUI to 198 if available AIUI_DIST="$PROJECT_DIR/../AIUI/packages/app/dist" if [ -d "$AIUI_DIST" ] && [ -f "$AIUI_DIST/index.html" ]; then echo " Deploying AIUI to 198..." ssh $SSH_OPTS "$TARGET_198" "sudo mkdir -p /opt/archipelago/web-ui/aiui && sudo rm -rf /opt/archipelago/web-ui/aiui/*" cd "$AIUI_DIST" && tar cf - . | ssh $SSH_OPTS "$TARGET_198" "sudo tar xf - -C /opt/archipelago/web-ui/aiui/" cd "$PROJECT_DIR" ssh $SSH_OPTS "$TARGET_198" "sudo chown -R 1000:1000 /opt/archipelago/web-ui/aiui" fi # Sync nginx config + snippets + fixes to 198 NGINX_CFG="$PROJECT_DIR/image-recipe/configs/nginx-archipelago.conf" SNIPPETS_DIR="$PROJECT_DIR/image-recipe/configs/snippets" if [ -f "$NGINX_CFG" ]; then echo " Syncing nginx config to 198..." scp $SSH_OPTS "$NGINX_CFG" "$TARGET_198:/tmp/nginx-archipelago.conf" 2>/dev/null || true ssh $SSH_OPTS "$TARGET_198" ' sudo cp /tmp/nginx-archipelago.conf /etc/nginx/sites-available/archipelago sudo rm -f /etc/nginx/conf.d/external-app-proxies.conf sudo sed -i "s|proxy_pass http://127.0.0.1:3141/;|proxy_pass http://127.0.0.1:3142/;|g" /etc/nginx/sites-available/archipelago rm -f /tmp/nginx-archipelago.conf ' 2>/dev/null || true fi # Sync nginx snippets to 198 if [ -d "$SNIPPETS_DIR" ]; then echo " Syncing nginx snippets to 198..." ssh $SSH_OPTS "$TARGET_198" "sudo mkdir -p /etc/nginx/snippets" 2>/dev/null || true for f in "$SNIPPETS_DIR"/*.conf; do [ -f "$f" ] && scp $SSH_OPTS "$f" "$TARGET_198:/tmp/nginx-snippet-$(basename $f)" 2>/dev/null || true done ssh $SSH_OPTS "$TARGET_198" ' for f in /tmp/nginx-snippet-*.conf; do [ -f "$f" ] && sudo mv "$f" "/etc/nginx/snippets/$(basename "$f" | sed "s/^nginx-snippet-//")" done ' 2>/dev/null || true fi ssh $SSH_OPTS "$TARGET_198" 'sudo nginx -t 2>&1 && echo " nginx config OK" || echo " nginx config test failed"' 2>/dev/null || true # Sync systemd service file to 198 SERVICE_FILE="$PROJECT_DIR/image-recipe/configs/archipelago.service" if [ -f "$SERVICE_FILE" ]; then echo " Syncing systemd service to 198..." scp $SSH_OPTS "$SERVICE_FILE" "$TARGET_198:/tmp/archipelago.service" 2>/dev/null || true ssh $SSH_OPTS "$TARGET_198" ' if ! diff -q /tmp/archipelago.service /etc/systemd/system/archipelago.service >/dev/null 2>&1; then sudo cp /tmp/archipelago.service /etc/systemd/system/archipelago.service sudo systemctl daemon-reload echo " Service file updated" else echo " Service file unchanged" fi rm -f /tmp/archipelago.service ' 2>/dev/null || true fi # Dev mode + FileBrowser on 198 ssh $SSH_OPTS "$TARGET_198" ' # Dev mode if ! grep -q "ARCHIPELAGO_DEV_MODE=true" /etc/systemd/system/archipelago.service.d/override.conf 2>/dev/null; then sudo mkdir -p /etc/systemd/system/archipelago.service.d printf "[Service]\nEnvironment=ARCHIPELAGO_DEV_MODE=true\n" | sudo tee /etc/systemd/system/archipelago.service.d/override.conf > /dev/null sudo systemctl daemon-reload fi # FileBrowser fix DOCKER=podman; command -v podman >/dev/null 2>&1 || DOCKER=docker FB=$(sudo $DOCKER ps -a --format "{{.Names}}" 2>/dev/null | grep -x filebrowser || true) if [ -n "$FB" ]; then RO=$(sudo $DOCKER inspect filebrowser 2>/dev/null | grep -oP "\"ReadonlyRootfs\":\s*\K\w+" || echo "false") if [ "$RO" = "true" ]; then sudo $DOCKER stop filebrowser 2>/dev/null; sudo $DOCKER rm filebrowser 2>/dev/null sudo mkdir -p /var/lib/archipelago/filebrowser sudo $DOCKER run -d --name filebrowser --restart=always -p 8083:80 -v /var/lib/archipelago/filebrowser:/srv docker.io/filebrowser/filebrowser:v2.27.0 2>/dev/null fi fi ' 2>/dev/null || true ssh $SSH_OPTS "$TARGET_198" "sudo systemctl start archipelago && sudo systemctl restart nginx" echo " ✅ 192.168.1.198 deployed" rm -f /tmp/archipelago-both exit 0 fi # Sync code section_start echo "$(timestamp) 📦 Syncing code..." rsync -avz --delete \ -e "ssh $SSH_OPTS" \ --exclude 'node_modules' \ --exclude 'target' \ --exclude 'dist' \ --exclude '.git' \ --exclude 'image-recipe/build' \ --exclude 'image-recipe/results' \ "$PROJECT_DIR/" "$TARGET_HOST:$TARGET_DIR/" section_end if [ "$QUICK" = true ]; then echo "" echo "✅ Quick sync complete!" exit 0 fi # Build on target echo "" echo "$(timestamp) 🔨 Building on target..." # Frontend section_start echo "$(timestamp) Building frontend (vue-tsc + vite)..." ssh $SSH_OPTS "$TARGET_HOST" "cd $TARGET_DIR/neode-ui && npm install --silent && npm run build" 2>&1 | sed 's/^/ /' section_end # Backend (if Rust is installed) — skip with --frontend-only if [ "$FRONTEND_ONLY" = true ]; then echo "$(timestamp) Skipping backend build (--frontend-only)" elif ssh $SSH_OPTS "$TARGET_HOST" "source ~/.cargo/env 2>/dev/null && command -v cargo" >/dev/null 2>&1; then section_start echo "$(timestamp) Building backend (Rust release — this takes 1-2 min)..." ssh $SSH_OPTS "$TARGET_HOST" "source ~/.cargo/env && cd $TARGET_DIR/core && cargo build --release 2>&1" | sed 's/^/ /' section_end else echo " ⚠️ Rust not installed on target, skipping backend build" fi if [ "$LIVE" = true ]; then echo "" echo "$(timestamp) 🚀 Deploying to live system..." # Deploy backend (check if binary exists) — skip with --frontend-only if [ "$FRONTEND_ONLY" = true ]; then echo "$(timestamp) Skipping backend deploy (--frontend-only)" elif ssh $SSH_OPTS "$TARGET_HOST" "[ -f $TARGET_DIR/core/target/release/archipelago ]" 2>/dev/null; then echo "$(timestamp) Deploying backend binary..." ssh $SSH_OPTS "$TARGET_HOST" "sudo systemctl stop archipelago" ssh $SSH_OPTS "$TARGET_HOST" "sudo cp $TARGET_DIR/core/target/release/archipelago /usr/local/bin/" fi # Deploy frontend (preserve aiui/ and claude-login.html — they are NOT part of the neode-ui build) echo "$(timestamp) Deploying frontend..." ssh $SSH_OPTS "$TARGET_HOST" "sudo find /opt/archipelago/web-ui -mindepth 1 -maxdepth 1 ! -name 'aiui' ! -name 'claude-login.html' -exec rm -rf {} +" ssh $SSH_OPTS "$TARGET_HOST" "sudo cp -rf $TARGET_DIR/web/dist/neode-ui/* /opt/archipelago/web-ui/" ssh $SSH_OPTS "$TARGET_HOST" "sudo chown -R 1000:1000 /opt/archipelago/web-ui" # Build and deploy AIUI (non-fatal — never delete existing AIUI on failure) AIUI_DIR="$PROJECT_DIR/../AIUI" AIUI_DIST="$AIUI_DIR/packages/app/dist" # Auto-build AIUI if dist is missing or older than source if [ -d "$AIUI_DIR/packages/app/src" ] && ( [ ! -f "$AIUI_DIST/index.html" ] || [ "$(find "$AIUI_DIR/packages/app/src" -newer "$AIUI_DIST/index.html" -print -quit 2>/dev/null)" != "" ] ); then echo "$(timestamp) Building AIUI (source newer than dist or dist missing)..." (cd "$AIUI_DIR" && VITE_BASE_PATH=/aiui/ pnpm build 2>&1 | tail -5) || echo "$(timestamp) ⚠️ AIUI build failed" fi if [ -d "$AIUI_DIST" ] && [ -f "$AIUI_DIST/index.html" ]; then echo "$(timestamp) Deploying AIUI..." if true; then echo "$(timestamp) Deploying AIUI..." ssh $SSH_OPTS "$TARGET_HOST" "sudo mkdir -p /opt/archipelago/web-ui/aiui" ssh $SSH_OPTS "$TARGET_HOST" "sudo rm -rf /opt/archipelago/web-ui/aiui/*" cd "$AIUI_DIST" && tar cf - . | ssh $SSH_OPTS "$TARGET_HOST" "sudo tar xf - -C /opt/archipelago/web-ui/aiui/" cd "$PROJECT_DIR" ssh $SSH_OPTS "$TARGET_HOST" "sudo chown -R 1000:1000 /opt/archipelago/web-ui/aiui && sudo chmod 755 /opt/archipelago/web-ui/aiui && sudo find /opt/archipelago/web-ui/aiui -type d -exec chmod 755 {} \;" else echo "$(timestamp) ⚠️ AIUI build failed — keeping existing AIUI on server" fi else echo "$(timestamp) ⚠️ AIUI not found at $AIUI_DIR, skipping" fi # Sync nginx config from image-recipe (single source of truth) NGINX_CFG="$PROJECT_DIR/image-recipe/configs/nginx-archipelago.conf" SNIPPETS_DIR="$PROJECT_DIR/image-recipe/configs/snippets" if [ -f "$NGINX_CFG" ]; then echo "$(timestamp) Syncing nginx config..." scp $SSH_OPTS "$NGINX_CFG" "$TARGET_HOST:/tmp/nginx-archipelago.conf" 2>/dev/null || true ssh $SSH_OPTS "$TARGET_HOST" ' sudo cp /tmp/nginx-archipelago.conf /etc/nginx/sites-available/archipelago rm -f /tmp/nginx-archipelago.conf ' 2>/dev/null || true fi # Sync nginx snippet files (HTTPS app proxies, PWA headers — included by main config) if [ -d "$SNIPPETS_DIR" ]; then echo "$(timestamp) Syncing nginx snippets..." ssh $SSH_OPTS "$TARGET_HOST" "sudo mkdir -p /etc/nginx/snippets" 2>/dev/null || true for f in "$SNIPPETS_DIR"/*.conf; do [ -f "$f" ] && scp $SSH_OPTS "$f" "$TARGET_HOST:/tmp/nginx-snippet-$(basename $f)" 2>/dev/null || true done ssh $SSH_OPTS "$TARGET_HOST" ' for f in /tmp/nginx-snippet-*.conf; do [ -f "$f" ] && sudo mv "$f" "/etc/nginx/snippets/$(basename "$f" | sed "s/^nginx-snippet-//")" done ' 2>/dev/null || true fi # Remove old port-based external app proxies config ssh $SSH_OPTS "$TARGET_HOST" 'sudo rm -f /etc/nginx/conf.d/external-app-proxies.conf' 2>/dev/null || true # Fix nginx Claude API proxy port (template uses 3141, proxy runs on 3142) ssh $SSH_OPTS "$TARGET_HOST" 'sudo sed -i "s|proxy_pass http://127.0.0.1:3141/;|proxy_pass http://127.0.0.1:3142/;|g" /etc/nginx/sites-available/archipelago' 2>/dev/null || true # Validate nginx config after all changes ssh $SSH_OPTS "$TARGET_HOST" 'sudo nginx -t 2>&1 && echo " nginx config OK" || echo " ⚠️ nginx config test failed"' 2>/dev/null || true # Sync systemd service file (single source of truth: image-recipe/configs/) SERVICE_FILE="$PROJECT_DIR/image-recipe/configs/archipelago.service" if [ -f "$SERVICE_FILE" ]; then echo "$(timestamp) Syncing systemd service file..." scp $SSH_OPTS "$SERVICE_FILE" "$TARGET_HOST:/tmp/archipelago.service" 2>/dev/null || true ssh $SSH_OPTS "$TARGET_HOST" ' if ! diff -q /tmp/archipelago.service /etc/systemd/system/archipelago.service >/dev/null 2>&1; then sudo cp /tmp/archipelago.service /etc/systemd/system/archipelago.service sudo systemctl daemon-reload echo " Service file updated" else echo " Service file unchanged" fi rm -f /tmp/archipelago.service ' 2>/dev/null || true fi # Deploy Claude API proxy (auto-install if missing) echo "$(timestamp) Setting up Claude API proxy..." ssh $SSH_OPTS "$TARGET_HOST" ' echo " Updating Claude API proxy on port 3142..." # Check for API key in existing service or setup-aiui-server.sh EXISTING_KEY=$(grep -oP "ANTHROPIC_API_KEY=\K.*" /etc/systemd/system/claude-api-proxy.service 2>/dev/null || true) if [ -z "$EXISTING_KEY" ]; then echo " ⚠️ No ANTHROPIC_API_KEY found — run setup-aiui-server.sh first to configure" else # Proxy script sudo tee /opt/archipelago/claude-api-proxy.py > /dev/null << '\''PYEOF'\'' #!/usr/bin/env python3 import http.server, json, ssl, sys, os, urllib.request, urllib.error API_KEY = os.environ.get("ANTHROPIC_API_KEY", "") PORT = 3142 class Handler(http.server.BaseHTTPRequestHandler): def do_POST(self): if self.path == "/health": self.send_response(200); self.send_header("Content-Type","application/json"); self.end_headers() self.wfile.write(b"{\"status\":\"ok\"}"); return cl = int(self.headers.get("Content-Length", 0)) body = self.rfile.read(cl) try: data = json.loads(body) except: data = {} if "max_tokens" not in data: data["max_tokens"] = 8096 for f in ["webSearch","web_search"]: data.pop(f, None) # Normalize model IDs — map short/dotted names to full API model IDs MODEL_MAP = { "claude-haiku-4.5": "claude-haiku-4-5-20251001", "claude-haiku-4-5": "claude-haiku-4-5-20251001", "claude-sonnet-4": "claude-sonnet-4-20250514", "claude-sonnet-4.5": "claude-sonnet-4-5-20250514", "claude-sonnet-4-5": "claude-sonnet-4-5-20250514", "claude-opus-4": "claude-opus-4-20250514", } m = data.get("model", "") if m in MODEL_MAP: data["model"] = MODEL_MAP[m] body = json.dumps(data).encode() headers = {"Content-Type":"application/json","x-api-key":API_KEY,"anthropic-version":"2023-06-01","anthropic-dangerous-direct-browser-access":"true"} for h in ["anthropic-version","anthropic-beta"]: if self.headers.get(h): headers[h] = self.headers[h] req = urllib.request.Request("https://api.anthropic.com"+self.path, data=body, headers=headers, method="POST") try: ctx = ssl.create_default_context() resp = urllib.request.urlopen(req, context=ctx, timeout=300) self.send_response(resp.status) is_stream = "text/event-stream" in (resp.headers.get("Content-Type","") or "") for k,v in resp.headers.items(): if k.lower() not in ("transfer-encoding","connection"): self.send_header(k,v) if is_stream: self.send_header("Transfer-Encoding","chunked") self.end_headers() if is_stream: while True: chunk = resp.read(4096) if not chunk: break self.wfile.write(b"%x\r\n" % len(chunk)); self.wfile.write(chunk); self.wfile.write(b"\r\n"); self.wfile.flush() self.wfile.write(b"0\r\n\r\n"); self.wfile.flush() else: self.wfile.write(resp.read()) except urllib.error.HTTPError as e: self.send_response(e.code); self.send_header("Content-Type","application/json"); self.end_headers(); self.wfile.write(e.read()) except Exception as e: self.send_response(502); self.send_header("Content-Type","application/json"); self.end_headers(); self.wfile.write(json.dumps({"error":str(e)}).encode()) def do_GET(self): if self.path == "/health": self.send_response(200); self.send_header("Content-Type","application/json"); self.end_headers(); self.wfile.write(b"{\"status\":\"ok\"}") else: self.send_response(404); self.end_headers() def log_message(self, fmt, *args): pass if not API_KEY: print("ERROR: ANTHROPIC_API_KEY not set"); sys.exit(1) server = http.server.HTTPServer(("127.0.0.1", PORT), Handler) print(f"Claude API proxy on port {PORT}") server.serve_forever() PYEOF sudo systemctl daemon-reload sudo systemctl enable claude-api-proxy sudo systemctl restart claude-api-proxy sleep 1 echo " Claude API proxy: $(systemctl is-active claude-api-proxy)" fi ' 2>/dev/null || true # Dev mode for Tailscale HTTP access (cookies need Secure flag disabled over plain HTTP) echo "$(timestamp) Checking dev mode..." ssh $SSH_OPTS "$TARGET_HOST" ' if [ -f /etc/systemd/system/archipelago.service.d/override.conf ] && grep -q "ARCHIPELAGO_DEV_MODE=true" /etc/systemd/system/archipelago.service.d/override.conf 2>/dev/null; then echo " Dev mode already enabled" else echo " Enabling dev mode (for Tailscale HTTP cookie support)..." sudo mkdir -p /etc/systemd/system/archipelago.service.d printf "[Service]\nEnvironment=ARCHIPELAGO_DEV_MODE=true\n" | sudo tee /etc/systemd/system/archipelago.service.d/override.conf > /dev/null sudo systemctl daemon-reload echo " Dev mode enabled" fi ' 2>/dev/null || true # Create data directories for DWN, content sharing, federation, identities echo "$(timestamp) Ensuring data directories for DWN, content, federation..." ssh $SSH_OPTS "$TARGET_HOST" ' sudo mkdir -p /var/lib/archipelago/dwn/messages sudo mkdir -p /var/lib/archipelago/dwn/protocols sudo mkdir -p /var/lib/archipelago/content/files sudo mkdir -p /var/lib/archipelago/federation sudo mkdir -p /var/lib/archipelago/identities sudo mkdir -p /var/lib/archipelago/tor-config sudo chown -R archipelago:archipelago /var/lib/archipelago/dwn /var/lib/archipelago/content /var/lib/archipelago/federation /var/lib/archipelago/identities /var/lib/archipelago/tor-config 2>/dev/null || true echo " Data directories OK" ' 2>/dev/null || true # Deploy nostr-provider.js for NIP-07 iframe signing (window.nostr support) echo "$(timestamp) Deploying nostr-provider.js..." scp $SSH_OPTS "$PROJECT_DIR/neode-ui/public/nostr-provider.js" "$TARGET_HOST:/tmp/nostr-provider.js" 2>/dev/null && \ ssh $SSH_OPTS "$TARGET_HOST" 'sudo cp /tmp/nostr-provider.js /opt/archipelago/web-ui/nostr-provider.js && echo " nostr-provider.js deployed"' 2>/dev/null || echo " (nostr-provider.js not found, skipping)" # Sync nginx config (includes all app proxies, NIP-07 sub_filter, AIUI proxy, external URL proxies) echo "$(timestamp) Syncing nginx config..." scp $SSH_OPTS "$PROJECT_DIR/image-recipe/configs/nginx-archipelago.conf" "$TARGET_HOST:/tmp/nginx-archipelago.conf" 2>/dev/null && \ ssh $SSH_OPTS "$TARGET_HOST" ' sudo cp /tmp/nginx-archipelago.conf /etc/nginx/sites-available/archipelago # Also sync HTTPS snippets if they exist sudo mkdir -p /etc/nginx/snippets echo " Nginx config synced" ' 2>/dev/null || echo " (nginx config sync skipped)" # Sync HTTPS app proxies snippet if it exists if [ -f "$PROJECT_DIR/image-recipe/configs/snippets/archipelago-https-app-proxies.conf" ]; then scp $SSH_OPTS "$PROJECT_DIR/image-recipe/configs/snippets/archipelago-https-app-proxies.conf" "$TARGET_HOST:/tmp/https-app-proxies.conf" 2>/dev/null && \ ssh $SSH_OPTS "$TARGET_HOST" 'sudo cp /tmp/https-app-proxies.conf /etc/nginx/snippets/archipelago-https-app-proxies.conf' 2>/dev/null || true fi # Fix FileBrowser — recreate if read-only root, create if missing echo "$(timestamp) Checking FileBrowser..." ssh $SSH_OPTS "$TARGET_HOST" ' DOCKER=podman command -v podman >/dev/null 2>&1 || DOCKER=docker FB_EXISTS=$(sudo $DOCKER ps -a --format "{{.Names}}" 2>/dev/null | grep -x filebrowser || true) if [ -n "$FB_EXISTS" ]; then RO=$(sudo $DOCKER inspect filebrowser 2>/dev/null | grep -oP "\"ReadonlyRootfs\":\s*\K\w+" || echo "false") if [ "$RO" = "true" ]; then echo " FileBrowser has read-only root — recreating..." sudo $DOCKER stop filebrowser 2>/dev/null sudo $DOCKER rm filebrowser 2>/dev/null sudo mkdir -p /var/lib/archipelago/filebrowser sudo $DOCKER run -d --name filebrowser --restart=always -p 8083:80 -v /var/lib/archipelago/filebrowser:/srv docker.io/filebrowser/filebrowser:v2.27.0 2>&1 | tail -1 echo " FileBrowser recreated" else echo " FileBrowser OK" fi else echo " Creating FileBrowser..." sudo mkdir -p /var/lib/archipelago/filebrowser sudo $DOCKER run -d --name filebrowser --restart=always -p 8083:80 -v /var/lib/archipelago/filebrowser:/srv docker.io/filebrowser/filebrowser:v2.27.0 2>&1 | tail -1 echo " FileBrowser created" fi ' 2>/dev/null || true # Restart services echo "$(timestamp) Restarting services..." ssh $SSH_OPTS "$TARGET_HOST" "sudo systemctl start archipelago && sudo systemctl restart nginx" # Set up HTTPS for PWA installability (browsers require secure context) echo "$(timestamp) Setting up HTTPS for PWA install..." ssh $SSH_OPTS "$TARGET_HOST" "sudo bash $TARGET_DIR/scripts/setup-https-dev.sh" 2>&1 | sed 's/^/ /' || true if [ "$FRONTEND_ONLY" = true ]; then echo "$(timestamp) Skipping container rebuilds (--frontend-only)" else # Rebuild and recreate LND UI container (port 8081 so Launch from UI and http://host:8081 both work) echo "$(timestamp) Rebuilding LND UI..." if ssh $SSH_OPTS "$TARGET_HOST" "cd $TARGET_DIR/docker/lnd-ui && (command -v podman >/dev/null 2>&1 && sudo podman build --no-cache -t lnd-ui:latest . || sudo docker build --no-cache -t lnd-ui:latest .)" 2>&1 | tail -12 | sed 's/^/ /'; then echo " Recreating LND UI container (port 8081)..." ssh $SSH_OPTS "$TARGET_HOST" ' DOCKER=podman command -v podman >/dev/null 2>&1 || DOCKER=docker for c in $(sudo $DOCKER ps -a --format "{{.Names}}" 2>/dev/null | grep -i lnd-ui); do [ -n "$c" ] && sudo $DOCKER stop "$c" 2>/dev/null; sudo $DOCKER rm -f "$c" 2>/dev/null done sudo $DOCKER run -d --name archy-lnd-ui -p 8081:80 --restart unless-stopped lnd-ui:latest ' 2>&1 | sed 's/^/ /' || true fi # Rebuild and recreate Electrs UI container (port 50002) echo "$(timestamp) Rebuilding Electrs UI..." if ssh $SSH_OPTS "$TARGET_HOST" "cd $TARGET_DIR/docker/electrs-ui && (command -v podman >/dev/null 2>&1 && sudo podman build --no-cache -t electrs-ui:latest . || sudo docker build --no-cache -t electrs-ui:latest .)" 2>&1 | tail -12 | sed 's/^/ /'; then echo " Recreating Electrs UI container (port 50002, host network)..." ssh $SSH_OPTS "$TARGET_HOST" ' DOCKER=podman command -v podman >/dev/null 2>&1 || DOCKER=docker for c in $(sudo $DOCKER ps -a --format "{{.Names}}" 2>/dev/null | grep -i electrs-ui); do [ -n "$c" ] && sudo $DOCKER stop "$c" 2>/dev/null; sudo $DOCKER rm -f "$c" 2>/dev/null done sudo $DOCKER run -d --name archy-electrs-ui --network host --restart unless-stopped electrs-ui:latest ' 2>&1 | sed 's/^/ /' || true fi # Bitcoin Knots: required for Mempool, Electrs, BTCPay, Fedimint TARGET_IP="$(echo "$TARGET_HOST" | cut -d@ -f2)" echo "$(timestamp) Ensuring Bitcoin Knots..." ssh $SSH_OPTS "$TARGET_HOST" " DOCKER=podman command -v podman >/dev/null 2>&1 || DOCKER=docker sudo \$DOCKER network create archy-net 2>/dev/null || true NET_OPT='--network archy-net' if ! sudo \$DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE 'bitcoin-knots|archy-bitcoin-knots'; then echo ' Creating Bitcoin Knots (mainnet, archipelago RPC)...' sudo mkdir -p /var/lib/archipelago/bitcoin # Demo mode: prune=550 saves ~194GB disk, but disables txindex (incompatible with electrs) if [ "$DEMO" = "true" ]; then BTC_EXTRA_ARGS="-prune=550" BTC_DBCACHE=512 else BTC_EXTRA_ARGS="-txindex=1" BTC_DBCACHE=4096 fi sudo \$DOCKER run -d --name bitcoin-knots --restart unless-stopped \$NET_OPT \ --cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \ --security-opt no-new-privileges:true \ -p 8332:8332 -p 8333:8333 \ -v /var/lib/archipelago/bitcoin:/home/bitcoin/.bitcoin \ docker.io/bitcoinknots/bitcoin:latest \ -server=1 \$BTC_EXTRA_ARGS \ -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 \ -rpcuser=archipelago -rpcpassword=archipelago123 \ -dbcache=\$BTC_DBCACHE echo ' Bitcoin Knots started (sync may take hours)' else sudo \$DOCKER network connect archy-net bitcoin-knots 2>/dev/null || true fi " 2>&1 | sed 's/^/ /' || true # Fix Mempool: clean duplicates, ensure full stack - mysql, backend (8999), frontend (4080) echo "$(timestamp) Fixing Mempool stack..." ssh $SSH_OPTS "$TARGET_HOST" " DOCKER=podman command -v podman >/dev/null 2>&1 || DOCKER=docker TARGET_IP='$TARGET_IP' NET_OPT='--network archy-net' # Clean any duplicate/old mempool containers (user may have two versions) # EXCLUDE mempool-electrs - indexing takes days, do not recreate on every deploy for c in mempool mempool-api mempool-web archy-mempool-api archy-mempool-web; do sudo \$DOCKER stop \$c 2>/dev/null sudo \$DOCKER rm -f \$c 2>/dev/null done # Create mysql-mempool if missing if ! sudo \$DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -qE 'mysql-mempool|archy-mempool-db'; then echo ' Creating mysql-mempool...' sudo mkdir -p /var/lib/archipelago/mysql-mempool sudo \$DOCKER run -d --name archy-mempool-db --restart unless-stopped \$NET_OPT \ -v /var/lib/archipelago/mysql-mempool:/var/lib/mysql \ -e MYSQL_DATABASE=mempool \ -e MYSQL_USER=mempool \ -e MYSQL_PASSWORD=mempoolpass \ -e MYSQL_ROOT_PASSWORD=rootpass \ docker.io/mariadb:10.11 sleep 3 fi MYSQL_CNT=\$(sudo \$DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -E 'mysql-mempool|archy-mempool-db' | head -1) MYSQL_CNT=\${MYSQL_CNT:-archy-mempool-db} # Ensure DB is on archy-net so mempool-api can resolve it sudo \$DOCKER network connect archy-net \$MYSQL_CNT 2>/dev/null || true # Create mempool-electrs ONLY if missing - do NOT recreate (indexing takes days, data is 800GB+) # One-time migration: if on bridge (wrong network), recreate with archy-net so it can reach bitcoin-knots if sudo \$DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -q mempool-electrs; then MEMPOOL_ELECTRS_NET=\$(sudo \$DOCKER inspect mempool-electrs --format '{{range \$k, \$v := .NetworkSettings.Networks}}{{\$k}}{{end}}' 2>/dev/null || true) if [ \"\$MEMPOOL_ELECTRS_NET\" = \"bridge\" ] || [ \"\$MEMPOOL_ELECTRS_NET\" = \"\" ]; then echo ' Migrating mempool-electrs to archy-net (preserving 800GB+ index)...' sudo \$DOCKER stop mempool-electrs 2>/dev/null sudo \$DOCKER rm mempool-electrs 2>/dev/null fi fi if ! sudo \$DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q mempool-electrs; then if sudo \$DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -q mempool-electrs; then echo ' Starting existing mempool-electrs (preserving index)...' sudo \$DOCKER start mempool-electrs 2>/dev/null || true else echo ' Creating mempool-electrs (indexer - may take days to sync, do not recreate)...' sudo mkdir -p /var/lib/archipelago/mempool-electrs # Use archy-net + bitcoin-knots for reliable Bitcoin connectivity (not host IP from bridge) sudo \$DOCKER run -d --name mempool-electrs --restart unless-stopped \$NET_OPT \ -p 50001:50001 \ -v /var/lib/archipelago/mempool-electrs:/data \ docker.io/mempool/electrs:latest \ --daemon-rpc-addr bitcoin-knots:8332 \ --cookie archipelago:archipelago123 \ --jsonrpc-import \ --electrum-rpc-addr 0.0.0.0:50001 \ --db-dir /data \ --lightmode fi fi # Create/recreate mempool-api (backend on 8999) - required for mempool to work for c in \$(sudo \$DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -E 'mempool-api|archy-mempool-api'); do echo ' Recreating mempool-api (backend)...' sudo \$DOCKER stop \"\$c\" 2>/dev/null sudo \$DOCKER rm -f \"\$c\" 2>/dev/null done if ! sudo \$DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q mempool-api; then echo ' Creating mempool-api (backend)...' sudo mkdir -p /var/lib/archipelago/mempool sudo \$DOCKER run -d --name mempool-api --restart unless-stopped \$NET_OPT \ -p 8999:8999 \ -v /var/lib/archipelago/mempool:/data \ -e MEMPOOL_BACKEND=electrum \ -e ELECTRUM_HOST=mempool-electrs \ -e ELECTRUM_PORT=50001 \ -e ELECTRUM_TLS_ENABLED=false \ -e CORE_RPC_HOST=\$TARGET_IP \ -e CORE_RPC_PORT=8332 \ -e CORE_RPC_USERNAME=archipelago \ -e CORE_RPC_PASSWORD=archipelago123 \ -e DATABASE_ENABLED=true \ -e DATABASE_HOST=\$MYSQL_CNT \ -e DATABASE_DATABASE=mempool \ -e DATABASE_USERNAME=mempool \ -e DATABASE_PASSWORD=mempoolpass \ docker.io/mempool/backend:v2.5.0 fi # Recreate mempool frontend - handle both 'mempool' and 'mempool-web' (frontend was on wrong port 8999) for c in \$(sudo \$DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -E '^mempool\$|mempool-web|archy-mempool-web'); do echo ' Recreating mempool frontend on 4080...' sudo \$DOCKER stop \"\$c\" 2>/dev/null sudo \$DOCKER rm -f \"\$c\" 2>/dev/null break done if ! sudo \$DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q archy-mempool-web; then echo ' Creating mempool frontend on 4080...' sudo \$DOCKER run -d --name archy-mempool-web --restart unless-stopped \$NET_OPT \ -p 4080:8080 \ -e FRONTEND_HTTP_PORT=8080 \ -e BACKEND_MAINNET_HTTP_HOST=mempool-api \ docker.io/mempool/frontend:v2.5.0 fi " 2>&1 | sed 's/^/ /' || true # Fix BTCPay Server: requires PostgreSQL + NBXplorer (BTCPay needs NBXplorer for block indexing) echo "$(timestamp) Fixing BTCPay Server stack..." TARGET_IP="$(echo "$TARGET_HOST" | cut -d@ -f2)" ssh $SSH_OPTS "$TARGET_HOST" " DOCKER=podman command -v podman >/dev/null 2>&1 || DOCKER=docker TARGET_IP='$TARGET_IP' sudo \$DOCKER network create archy-net 2>/dev/null || true NET_OPT='--network archy-net' # Ensure bitcoin-knots is on archy-net for NBXplorer/BTCPay to reach it sudo \$DOCKER network connect archy-net bitcoin-knots 2>/dev/null || true # Create PostgreSQL for BTCPay if missing if ! sudo \$DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -qE 'archy-btcpay-db|postgres-btcpay'; then echo ' Creating archy-btcpay-db (PostgreSQL)...' sudo mkdir -p /var/lib/archipelago/postgres-btcpay sudo \$DOCKER run -d --name archy-btcpay-db --restart unless-stopped \$NET_OPT \ -v /var/lib/archipelago/postgres-btcpay:/var/lib/postgresql/data \ -e POSTGRES_DB=btcpay \ -e POSTGRES_USER=btcpay \ -e POSTGRES_PASSWORD=btcpaypass \ docker.io/postgres:15-alpine sleep 3 fi # Create NBXplorer database in PostgreSQL (NBXplorer needs its own DB) sudo \$DOCKER exec archy-btcpay-db psql -U postgres -tc \"SELECT 1 FROM pg_database WHERE datname='nbxplorer'\" 2>/dev/null | grep -q 1 || \ sudo \$DOCKER exec -e PGPASSWORD=btcpaypass archy-btcpay-db psql -U postgres -c \"CREATE DATABASE nbxplorer;\" 2>/dev/null || true # Create NBXplorer (required by BTCPay - indexes blocks for payment tracking) if ! sudo \$DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q archy-nbxplorer; then if sudo \$DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -q archy-nbxplorer; then sudo \$DOCKER start archy-nbxplorer 2>/dev/null || true else echo ' Creating archy-nbxplorer...' sudo mkdir -p /var/lib/archipelago/nbxplorer sudo \$DOCKER run -d --name archy-nbxplorer --restart unless-stopped \$NET_OPT \ -p 32838:32838 \ -v /var/lib/archipelago/nbxplorer:/data \ -e NBXPLORER_DATADIR=/data \ -e NBXPLORER_NETWORK=mainnet \ -e NBXPLORER_CHAINS=btc \ -e NBXPLORER_BIND=0.0.0.0:32838 \ -e NBXPLORER_BTCRPCURL=http://bitcoin-knots:8332 \ -e NBXPLORER_BTCRPCUSER=archipelago \ -e NBXPLORER_BTCRPCPASSWORD=archipelago123 \ -e NBXPLORER_POSTGRES='User ID=btcpay;Password=btcpaypass;Host=archy-btcpay-db;Port=5432;Database=nbxplorer;Include Error Detail=true' \ docker.io/nicolasdorier/nbxplorer:2.6.0 sleep 5 fi fi # Recreate btcpay-server with PostgreSQL, NBXplorer URL, and Bitcoin RPC for c in btcpay-server archy-btcpay; do if sudo \$DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -qx \"\$c\"; then echo ' Recreating btcpay-server with NBXplorer...' sudo \$DOCKER stop \"\$c\" 2>/dev/null sudo \$DOCKER rm -f \"\$c\" 2>/dev/null fi done if ! sudo \$DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q btcpay-server; then echo ' Creating btcpay-server on 23000...' sudo mkdir -p /var/lib/archipelago/btcpay sudo \$DOCKER run -d --name btcpay-server --restart unless-stopped \$NET_OPT \ --cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \ --security-opt no-new-privileges:true \ -p 23000:49392 \ -v /var/lib/archipelago/btcpay:/datadir \ -e ASPNETCORE_URLS=http://0.0.0.0:49392 \ -e BTCPAY_PROTOCOL=http \ -e BTCPAY_HOST=\$TARGET_IP:23000 \ -e BTCPAY_CHAINS=btc \ -e BTCPAY_BTCEXPLORERURL=http://archy-nbxplorer:32838 \ -e BTCPAY_BTCRPCURL=http://bitcoin-knots:8332 \ -e BTCPAY_BTCRPCUSER=archipelago \ -e BTCPAY_BTCRPCPASSWORD=archipelago123 \ -e BTCPAY_POSTGRES='User ID=btcpay;Password=btcpaypass;Host=archy-btcpay-db;Port=5432;Database=btcpay;Include Error Detail=true' \ docker.io/btcpayserver/btcpayserver:1.13.5 fi " 2>&1 | sed 's/^/ /' || true # Ensure Immich stack (postgres + redis + server) - creates if missing echo "$(timestamp) Ensuring Immich stack..." ssh $SSH_OPTS "$TARGET_HOST" " DOCKER=podman command -v podman >/dev/null 2>&1 || DOCKER=docker # Remove old single-container 'immich' if present (wrong port mapping, conflicts with immich_server) if sudo \$DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -qx immich; then echo ' Removing old immich container (use immich_server)...' sudo \$DOCKER stop immich 2>/dev/null sudo \$DOCKER rm -f immich 2>/dev/null sudo \$DOCKER start immich_server 2>/dev/null || true fi if ! sudo \$DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q immich_server; then echo ' Creating Immich stack...' sudo mkdir -p /var/lib/archipelago/immich /var/lib/archipelago/immich-db sudo \$DOCKER network create immich-net 2>/dev/null || true if ! sudo \$DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -q immich_postgres; then sudo \$DOCKER run -d --name immich_postgres --restart unless-stopped --network immich-net \ -v /var/lib/archipelago/immich-db:/var/lib/postgresql/data \ -e POSTGRES_PASSWORD=immichpass -e POSTGRES_USER=postgres -e POSTGRES_DB=immich \ ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0 2>/dev/null || true sleep 5 fi if ! sudo \$DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q immich_redis; then sudo \$DOCKER run -d --name immich_redis --restart unless-stopped --network immich-net \ docker.io/valkey/valkey:7-alpine 2>/dev/null || true sleep 2 fi if ! sudo \$DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q immich_server; then sudo \$DOCKER run -d --name immich_server --restart unless-stopped --network immich-net \ -p 2283:2283 -v /var/lib/archipelago/immich:/usr/src/app/upload \ -e DB_HOSTNAME=immich_postgres -e DB_USERNAME=postgres -e DB_PASSWORD=immichpass \ -e DB_DATABASE_NAME=immich -e REDIS_HOSTNAME=immich_redis \ -e UPLOAD_LOCATION=/usr/src/app/upload \ ghcr.io/immich-app/immich-server:release 2>/dev/null || true fi echo ' Immich stack created (may take 1-2 min to become ready)' else echo ' Immich already running' fi " 2>&1 | sed 's/^/ /' || true # Tor: global hidden services - each service gets its own .onion address echo "$(timestamp) Setting up Tor..." TARGET_IP="$(echo "$TARGET_HOST" | cut -d@ -f2)" ssh $SSH_OPTS "$TARGET_HOST" " DOCKER=podman command -v podman >/dev/null 2>&1 || DOCKER=docker TARGET_IP='$TARGET_IP' sudo mkdir -p /var/lib/archipelago/tor # Ensure services.json exists with default services SERVICES_JSON=/var/lib/archipelago/tor/services.json if [ ! -f "\$SERVICES_JSON" ]; then sudo python3 -c ' import json services = [ {"name": "archipelago", "local_port": 80, "enabled": True}, {"name": "bitcoin", "local_port": 8333, "enabled": True}, {"name": "electrs", "local_port": 50001, "enabled": True}, {"name": "lnd", "local_port": 9735, "enabled": True}, {"name": "btcpay", "local_port": 23000, "enabled": True}, {"name": "mempool", "local_port": 4080, "enabled": True}, {"name": "fedimint", "local_port": 8175, "enabled": True} ] with open("/var/lib/archipelago/tor/services.json", "w") as f: json.dump({"services": services}, f, indent=2) print("services.json created") ' fi # Generate torrc — use /var/lib/tor/ for hidden services (AppArmor-safe) sudo python3 -c ' import json lines = ["SocksPort 9050", "ControlPort 0", ""] try: with open("/var/lib/archipelago/tor/services.json") as f: cfg = json.load(f) for svc in cfg.get("services", []): if svc.get("enabled", True): n = svc["name"] p = svc["local_port"] lines.append("HiddenServiceDir /var/lib/tor/hidden_service_%s" % n) lines.append("HiddenServicePort %d 127.0.0.1:%d" % (p, p)) lines.append("") except Exception: for n, p in [("archipelago",80),("bitcoin",8333),("electrs",50001),("lnd",9735),("btcpay",23000),("mempool",4080),("fedimint",8175)]: lines.append("HiddenServiceDir /var/lib/tor/hidden_service_%s" % n) lines.append("HiddenServicePort %d 127.0.0.1:%d" % (p, p)) lines.append("") with open("/etc/tor/torrc", "w") as f: f.write("\n".join(lines) + "\n") print("torrc generated with %d services" % (len(lines) // 3)) ' # Remove any old Tor container (system Tor is preferred) for c in \$(sudo \$DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -E 'archy-tor|^tor\$'); do sudo \$DOCKER stop \"\$c\" 2>/dev/null sudo \$DOCKER rm -f \"\$c\" 2>/dev/null done # Use system Tor (preferred — no AppArmor issues with default paths) if command -v tor >/dev/null 2>&1; then sudo systemctl enable tor 2>/dev/null sudo systemctl restart tor@default 2>/dev/null echo ' Using system Tor daemon' else echo ' Installing system Tor...' sudo apt-get update -qq && sudo apt-get install -y -qq tor 2>/dev/null || true if command -v tor >/dev/null 2>&1; then sudo systemctl enable tor 2>/dev/null sudo systemctl restart tor@default 2>/dev/null echo ' System Tor installed and started' else echo ' WARNING: Could not install Tor' fi fi " 2>&1 | sed 's/^/ /' || true # Tor diagnostic: check if hostname files exist (may take 30-60s after Tor starts) echo " Checking Tor hostname files..." ssh $SSH_OPTS "$TARGET_HOST" " # Check all hidden_service_* dirs for hostname files (check both paths) for dir in /var/lib/tor/hidden_service_*/ /var/lib/archipelago/tor/hidden_service_*/; do [ -d \"\$dir\" ] || continue svc=\$(basename \"\$dir\" | sed 's/hidden_service_//') f=\"\${dir}hostname\" if [ -f \"\$f\" ]; then echo \" ✓ \$svc: \$(cat \$f)\" else echo \" ✗ \$svc: hostname not yet generated (Tor may need 30-60s)\" fi done " 2>&1 | sed 's/^/ /' || true # Recreate Fedimint with FM_API_URL for Guardian UI (fixes "Api URL must be configured") section_start echo "$(timestamp) Fixing Fedimint API URL..." TARGET_IP="$(echo "$TARGET_HOST" | cut -d@ -f2)" TIMEOUT_CMD="" command -v timeout >/dev/null 2>&1 && TIMEOUT_CMD="timeout 90" command -v gtimeout >/dev/null 2>&1 && TIMEOUT_CMD="gtimeout 90" ($TIMEOUT_CMD ssh $SSH_OPTS "$TARGET_HOST" " DOCKER=podman command -v podman >/dev/null 2>&1 || DOCKER=docker for c in \$(sudo \$DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -E '^fedimint\$'); do echo ' Recreating fedimint with FM_API_URL...' sudo \$DOCKER stop \"\$c\" 2>/dev/null sudo \$DOCKER rm -f \"\$c\" 2>/dev/null sudo \$DOCKER run -d --name fedimint --restart unless-stopped \ --cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \ --security-opt no-new-privileges:true \ -p 8173:8173 -p 8174:8174 -p 8175:8175 \ -v /var/lib/archipelago/fedimint:/data \ -e FM_DATA_DIR=/data \ -e FM_BITCOIND_USERNAME=archipelago \ -e FM_BITCOIND_PASSWORD=archipelago123 \ -e FM_BITCOIN_NETWORK=bitcoin \ -e FM_BIND_P2P=0.0.0.0:8173 \ -e FM_BIND_API=0.0.0.0:8174 \ -e FM_BIND_UI=0.0.0.0:8175 \ -e FM_P2P_URL=fedimint://$TARGET_IP:8173 \ -e FM_API_URL=ws://$TARGET_IP:8174 \ -e FM_BITCOIND_URL=http://$TARGET_IP:8332 \ docker.io/fedimint/fedimintd:v0.10.0 break done # Ensure Fedimint Gateway companion container # Auto-detect LND: if running with credentials, use lnd mode; otherwise use ldk (built-in) sudo \$DOCKER rm -f fedimint-gateway 2>/dev/null || true echo ' Creating fedimint-gateway...' sudo mkdir -p /var/lib/archipelago/fedimint-gateway LND_CERT=/var/lib/archipelago/lnd/tls.cert LND_MACAROON=/var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/admin.macaroon GW_COMMON=\"-p 8176:8176 -v /var/lib/archipelago/fedimint-gateway:/data docker.io/fedimint/gatewayd:v0.10.0 gatewayd --data-dir /data --listen 0.0.0.0:8176 --bcrypt-password-hash '\$2y\$10\$t9YjjxkiktrlYvjajB/zgOMDnSNVg4HqrbDqh47u7Jf42whNdxNqC' --network bitcoin --bitcoind-url http://$TARGET_IP:8332 --bitcoind-username archipelago --bitcoind-password archipelago123\" if sudo \$DOCKER ps --format '{{.Names}}' | grep -q '^lnd\$' && sudo test -f \$LND_CERT && sudo test -f \$LND_MACAROON; then echo ' LND detected — using lnd mode' sudo \$DOCKER run -d --name fedimint-gateway --restart unless-stopped \ --cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \ --security-opt no-new-privileges:true \ -p 8176:8176 \ -v /var/lib/archipelago/fedimint-gateway:/data \ -v /var/lib/archipelago/lnd/tls.cert:/lnd/tls.cert:ro \ -v /var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/admin.macaroon:/lnd/admin.macaroon:ro \ docker.io/fedimint/gatewayd:v0.10.0 \ gatewayd --data-dir /data --listen 0.0.0.0:8176 \ --bcrypt-password-hash '\$2y\$10\$t9YjjxkiktrlYvjajB/zgOMDnSNVg4HqrbDqh47u7Jf42whNdxNqC' \ --network bitcoin --bitcoind-url http://$TARGET_IP:8332 \ --bitcoind-username archipelago --bitcoind-password archipelago123 \ lnd --lnd-rpc-host $TARGET_IP:10009 --lnd-tls-cert /lnd/tls.cert --lnd-macaroon /lnd/admin.macaroon else echo ' No LND found — using ldk (built-in Lightning)' sudo \$DOCKER run -d --name fedimint-gateway --restart unless-stopped \ --cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \ --security-opt no-new-privileges:true \ -p 8176:8176 -p 9737:9737 \ -v /var/lib/archipelago/fedimint-gateway:/data \ docker.io/fedimint/gatewayd:v0.10.0 \ gatewayd --data-dir /data --listen 0.0.0.0:8176 \ --bcrypt-password-hash '\$2y\$10\$t9YjjxkiktrlYvjajB/zgOMDnSNVg4HqrbDqh47u7Jf42whNdxNqC' \ --network bitcoin --bitcoind-url http://$TARGET_IP:8332 \ --bitcoind-username archipelago --bitcoind-password archipelago123 \ ldk --ldk-lightning-port 9737 --ldk-alias archipelago-gateway fi " 2>&1 | sed 's/^/ /') || echo " (Fedimint fix timed out or skipped - run manually if needed)" section_end # LND: Lightning Network Daemon (requires bitcoin-knots on archy-net) echo "$(timestamp) Ensuring LND..." ssh $SSH_OPTS "$TARGET_HOST" ' DOCKER=podman command -v podman >/dev/null 2>&1 || DOCKER=docker if ! sudo $DOCKER ps --format "{{.Names}}" 2>/dev/null | grep -qx lnd; then if sudo $DOCKER ps -a --format "{{.Names}}" 2>/dev/null | grep -qx lnd; then sudo $DOCKER start lnd 2>/dev/null || true echo " LND started (existing)" else echo " Creating LND..." sudo mkdir -p /var/lib/archipelago/lnd if [ ! -f /var/lib/archipelago/lnd/lnd.conf ]; then cat > /tmp/lnd.conf <&1 | sed 's/^/ /' || true # Home Assistant echo "$(timestamp) Ensuring Home Assistant..." ssh $SSH_OPTS "$TARGET_HOST" ' DOCKER=podman command -v podman >/dev/null 2>&1 || DOCKER=docker if ! sudo $DOCKER ps --format "{{.Names}}" 2>/dev/null | grep -qx homeassistant; then if sudo $DOCKER ps -a --format "{{.Names}}" 2>/dev/null | grep -qx homeassistant; then sudo $DOCKER start homeassistant 2>/dev/null || true else echo " Creating Home Assistant..." sudo mkdir -p /var/lib/archipelago/home-assistant sudo $DOCKER run -d --name homeassistant --restart unless-stopped \ --cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \ --security-opt no-new-privileges:true \ -p 8123:8123 -v /var/lib/archipelago/home-assistant:/config \ -e TZ=UTC \ docker.io/homeassistant/home-assistant:2024.1 fi else echo " Home Assistant already running" fi ' 2>&1 | sed 's/^/ /' || true # Grafana echo "$(timestamp) Ensuring Grafana..." ssh $SSH_OPTS "$TARGET_HOST" ' DOCKER=podman command -v podman >/dev/null 2>&1 || DOCKER=docker if ! sudo $DOCKER ps --format "{{.Names}}" 2>/dev/null | grep -qx grafana; then if sudo $DOCKER ps -a --format "{{.Names}}" 2>/dev/null | grep -qx grafana; then sudo $DOCKER start grafana 2>/dev/null || true else echo " Creating Grafana..." sudo mkdir -p /var/lib/archipelago/grafana sudo chown 472:472 /var/lib/archipelago/grafana 2>/dev/null || true sudo $DOCKER run -d --name grafana --restart unless-stopped \ --cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \ --security-opt no-new-privileges:true \ -p 3000:3000 -v /var/lib/archipelago/grafana:/var/lib/grafana \ -e GF_PATHS_DATA=/var/lib/grafana -e GF_USERS_ALLOW_SIGN_UP=false \ docker.io/grafana/grafana:10.2.0 fi else echo " Grafana already running" fi ' 2>&1 | sed 's/^/ /' || true # Jellyfin echo "$(timestamp) Ensuring Jellyfin..." ssh $SSH_OPTS "$TARGET_HOST" ' DOCKER=podman command -v podman >/dev/null 2>&1 || DOCKER=docker if ! sudo $DOCKER ps --format "{{.Names}}" 2>/dev/null | grep -qx jellyfin; then if sudo $DOCKER ps -a --format "{{.Names}}" 2>/dev/null | grep -qx jellyfin; then sudo $DOCKER start jellyfin 2>/dev/null || true else echo " Creating Jellyfin..." sudo mkdir -p /var/lib/archipelago/jellyfin/config /var/lib/archipelago/jellyfin/cache sudo $DOCKER run -d --name jellyfin --restart unless-stopped \ --cap-drop ALL --security-opt no-new-privileges:true \ -p 8096:8096 \ -v /var/lib/archipelago/jellyfin/config:/config \ -v /var/lib/archipelago/jellyfin/cache:/cache \ docker.io/jellyfin/jellyfin:10.8.13 fi else echo " Jellyfin already running" fi ' 2>&1 | sed 's/^/ /' || true # Vaultwarden echo "$(timestamp) Ensuring Vaultwarden..." ssh $SSH_OPTS "$TARGET_HOST" ' DOCKER=podman command -v podman >/dev/null 2>&1 || DOCKER=docker if ! sudo $DOCKER ps --format "{{.Names}}" 2>/dev/null | grep -qx vaultwarden; then if sudo $DOCKER ps -a --format "{{.Names}}" 2>/dev/null | grep -qx vaultwarden; then sudo $DOCKER start vaultwarden 2>/dev/null || true else echo " Creating Vaultwarden..." sudo mkdir -p /var/lib/archipelago/vaultwarden sudo $DOCKER run -d --name vaultwarden --restart unless-stopped \ --cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add NET_BIND_SERVICE \ --security-opt no-new-privileges:true \ -p 8082:80 -v /var/lib/archipelago/vaultwarden:/data \ docker.io/vaultwarden/server:1.30.0-alpine fi else echo " Vaultwarden already running" fi ' 2>&1 | sed 's/^/ /' || true # SearXNG (privacy search engine — used by AIUI web search) echo "$(timestamp) Ensuring SearXNG..." ssh $SSH_OPTS "$TARGET_HOST" ' DOCKER=podman command -v podman >/dev/null 2>&1 || DOCKER=docker if ! sudo $DOCKER ps --format "{{.Names}}" 2>/dev/null | grep -qx searxng; then if sudo $DOCKER ps -a --format "{{.Names}}" 2>/dev/null | grep -qx searxng; then sudo $DOCKER start searxng 2>/dev/null || true else echo " Creating SearXNG..." sudo $DOCKER run -d --name searxng --restart unless-stopped \ --cap-drop ALL --security-opt no-new-privileges:true \ -p 8888:8080 \ docker.io/searxng/searxng:latest fi else echo " SearXNG already running" fi ' 2>&1 | sed 's/^/ /' || true # Ollama (local LLM inference — used by AIUI) echo "$(timestamp) Ensuring Ollama..." ssh $SSH_OPTS "$TARGET_HOST" ' DOCKER=podman command -v podman >/dev/null 2>&1 || DOCKER=docker if ! sudo $DOCKER ps --format "{{.Names}}" 2>/dev/null | grep -qx ollama; then if sudo $DOCKER ps -a --format "{{.Names}}" 2>/dev/null | grep -qx ollama; then sudo $DOCKER start ollama 2>/dev/null || true else echo " Creating Ollama..." sudo mkdir -p /var/lib/archipelago/ollama sudo $DOCKER run -d --name ollama --restart unless-stopped \ --cap-drop ALL --security-opt no-new-privileges:true \ -p 11434:11434 -v /var/lib/archipelago/ollama:/root/.ollama \ docker.io/ollama/ollama:latest fi else echo " Ollama already running" fi ' 2>&1 | sed 's/^/ /' || true fi # end FRONTEND_ONLY guard # Post-deploy health check — wait up to 60s for server to come healthy echo "" echo "$(timestamp) 🩺 Post-deploy health check..." HEALTH_OK=false for i in $(seq 1 12); do POST_HEALTH=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 5 "http://$TARGET_IP_ONLY/health" 2>/dev/null || echo "000") if [ "$POST_HEALTH" = "200" ]; then echo " Health: OK (200) after $((i * 5))s" HEALTH_OK=true break fi echo " Health: $POST_HEALTH (waiting... ${i}/12)" sleep 5 done if [ "$HEALTH_OK" = false ]; then echo " ⚠️ Server did not become healthy within 60s (last: $POST_HEALTH)" echo " Rollback: ssh $TARGET_HOST and check 'sudo journalctl -u archipelago -n 50'" fi DEPLOY_END=$(date +%s) DEPLOY_ELAPSED=$((DEPLOY_END - DEPLOY_START)) echo "" echo "$(timestamp) ✅ Deployed to live system! (${DEPLOY_ELAPSED}s total)" echo " Backend: $(ssh $SSH_OPTS "$TARGET_HOST" 'sudo systemctl is-active archipelago')" echo " Web UI: http://$TARGET_IP_ONLY" echo " PWA install: https://$TARGET_IP_ONLY (use HTTPS, accept cert once, then Install app)" else echo "" echo "✅ Build complete!" echo "" echo "To test frontend dev server:" echo " ssh $TARGET_HOST" echo " cd ~/archy/neode-ui && npm run dev -- --host 0.0.0.0" echo " Then open: http://$(echo $TARGET_HOST | cut -d@ -f2):5173" echo "" echo "To deploy to live system:" echo " ./scripts/deploy-to-target.sh --live" fi