From 1d98de24d0ae32d503231803c24e20fb5a8e0faf Mon Sep 17 00:00:00 2001 From: Dorian Date: Sat, 21 Mar 2026 01:11:05 +0000 Subject: [PATCH] fix: WebSocket race conditions, Vue error handler, remove sudo podman, add container health checks - F1: Guard connectWebSocket against concurrent calls with isWsConnecting flag - F2: Serialize mesh send operations with sendQueue to prevent fetchMessages races - F3: Add global Vue error handler with toast notification - S1: Replace sudo podman with podman across all scripts (rootless Podman) - S2: Add health-cmd to all 40 container run commands in first-boot-containers.sh Co-Authored-By: Claude Opus 4.6 (1M context) --- image-recipe/build-auto-installer-iso.sh | 117 ++++++++++++++++-- neode-ui/src/main.ts | 7 ++ neode-ui/src/stores/app.ts | 9 +- neode-ui/src/stores/mesh.ts | 41 +++--- scripts/deploy-bitcoin-knots.sh | 10 +- scripts/deploy-tailscale.sh | 6 +- scripts/first-boot-containers.sh | 151 +++++++++++++++++------ scripts/fix-indeedhub-containers.sh | 56 ++++----- scripts/setup-aiui-server.sh | 12 +- scripts/uptime-monitor.sh | 2 +- 10 files changed, 301 insertions(+), 110 deletions(-) diff --git a/image-recipe/build-auto-installer-iso.sh b/image-recipe/build-auto-installer-iso.sh index 89ae450d..21d22e86 100755 --- a/image-recipe/build-auto-installer-iso.sh +++ b/image-recipe/build-auto-installer-iso.sh @@ -38,7 +38,9 @@ case "$ARCH" in LINUX_IMAGE_PKG="linux-image-amd64" GRUB_EFI_PKG="grub-efi-amd64" GRUB_EFI_SIGNED_PKG="grub-efi-amd64-signed" + GRUB_PC_PKG="grub-pc-bin" GRUB_TARGET="x86_64-efi" + GRUB_BIOS_TARGET="i386-pc" CONTAINER_PLATFORM="linux/amd64" LIB_DIR="${LIB_DIR}" ;; @@ -48,7 +50,9 @@ case "$ARCH" in LINUX_IMAGE_PKG="linux-image-arm64" GRUB_EFI_PKG="grub-efi-arm64" GRUB_EFI_SIGNED_PKG="grub-efi-arm64-signed" + GRUB_PC_PKG="" GRUB_TARGET="arm64-efi" + GRUB_BIOS_TARGET="" CONTAINER_PLATFORM="linux/arm64" LIB_DIR="aarch64-linux-gnu" ;; @@ -179,7 +183,7 @@ RUN apt-get update && apt-get install -y \ ${LINUX_IMAGE_PKG} \ ${GRUB_EFI_PKG} \ ${GRUB_EFI_SIGNED_PKG} \ - shim-signed \ + ${GRUB_PC_PKG} shim-signed \ systemd \ systemd-sysv \ dbus \ @@ -576,7 +580,7 @@ if [ -n "$DEV_SERVER" ] && [ "$DEV_SERVER" != "localhost" ] && [ "$DEV_SERVER" ! # Patterns match against `podman images` repository names (not container names) CAPTURE_PATTERNS="bitcoin-ui bitcoinknots lnd lnd-ui electrs-ui filebrowser mempool backend frontend electrs tailscale homeassistant home-assistant btcpayserver nbxplorer postgres alpine-tor nostr-rs-relay strfry fedimintd gatewayd dwn-server grafana uptime-kuma jellyfin vaultwarden searxng mariadb valkey nginx-alpine portainer photoprism nextcloud nginx-proxy-manager immich onlyoffice adguard penpot" REMOTE_TMP="/tmp/archipelago-image-capture-$$" - SAVED_LIST=$(ssh "$DEV_SERVER" "mkdir -p $REMOTE_TMP && for p in $CAPTURE_PATTERNS; do img=\$(sudo podman images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -i \"\$p\" | head -1); [ -n \"\$img\" ] && sudo podman save -o \"$REMOTE_TMP/\$p.tar\" \"\$img\" 2>/dev/null && echo \"\$p\"; done" 2>/dev/null) || true + SAVED_LIST=$(ssh "$DEV_SERVER" "mkdir -p $REMOTE_TMP && for p in $CAPTURE_PATTERNS; do img=\$(podman images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -i \"\$p\" | head -1); [ -n \"\$img\" ] && podman save -o \"$REMOTE_TMP/\$p.tar\" \"\$img\" 2>/dev/null && echo \"\$p\"; done" 2>/dev/null) || true for p in $SAVED_LIST; do if [ -n "$p" ] && scp "$DEV_SERVER:$REMOTE_TMP/$p.tar" "$IMAGES_DIR/$p.tar" 2>/dev/null; then echo " ✅ Captured from server: $p.tar" @@ -899,6 +903,20 @@ cat > "$ARCH_DIR/auto-install.sh" <<'INSTALLER_SCRIPT' set -e +# Detect architecture at install time +case "$(uname -m)" in + x86_64|amd64) + ARCH="x86_64" + GRUB_TARGET="x86_64-efi" + GRUB_BIOS_TARGET="i386-pc" + ;; + aarch64|arm64) + ARCH="arm64" + GRUB_TARGET="arm64-efi" + GRUB_BIOS_TARGET="" + ;; +esac + # Colors RED='\033[0;31m' GREEN='\033[0;32m' @@ -1032,22 +1050,29 @@ echo "" umount ${TARGET_DISK}* 2>/dev/null || true umount ${TARGET_DISK}p* 2>/dev/null || true -# Create partition table +# Create partition table — dual BIOS+UEFI boot support echo " [1/6] Creating partitions..." parted -s "$TARGET_DISK" mklabel gpt -parted -s "$TARGET_DISK" mkpart primary fat32 1MiB 513MiB -parted -s "$TARGET_DISK" set 1 esp on -parted -s "$TARGET_DISK" mkpart primary ext4 513MiB 100% +# Partition 1: 1MB BIOS boot partition (for legacy BIOS GRUB on GPT disks) +parted -s "$TARGET_DISK" mkpart bios_boot 1MiB 2MiB +parted -s "$TARGET_DISK" set 1 bios_grub on +# Partition 2: 512MB EFI System Partition (for UEFI boot) +parted -s "$TARGET_DISK" mkpart efi fat32 2MiB 514MiB +parted -s "$TARGET_DISK" set 2 esp on +# Partition 3: Root filesystem (remaining space) +parted -s "$TARGET_DISK" mkpart root ext4 514MiB 100% sleep 2 # Determine partition names if [[ "$TARGET_DISK" == *nvme* ]]; then - EFI_PART="${TARGET_DISK}p1" - ROOT_PART="${TARGET_DISK}p2" + BIOS_PART="${TARGET_DISK}p1" + EFI_PART="${TARGET_DISK}p2" + ROOT_PART="${TARGET_DISK}p3" else - EFI_PART="${TARGET_DISK}1" - ROOT_PART="${TARGET_DISK}2" + BIOS_PART="${TARGET_DISK}1" + EFI_PART="${TARGET_DISK}2" + ROOT_PART="${TARGET_DISK}3" fi # Format partitions @@ -1157,6 +1182,10 @@ chown -R 1000:1000 /mnt/target/var/lib/archipelago 2>/dev/null || true # Create welcome profile (nginx serves on port 80) cat > /mnt/target/etc/profile.d/archipelago.sh <<'PROFILE' #!/bin/bash +# Ensure /sbin and /usr/sbin are in PATH (needed for reboot, shutdown, etc.) +case ":$PATH:" in + *:/sbin:*) ;; *) export PATH="$PATH:/sbin:/usr/sbin" ;; +esac if [ -t 0 ] && [ -z "$ARCHIPELAGO_WELCOMED" ]; then export ARCHIPELAGO_WELCOMED=1 IP=$(hostname -I 2>/dev/null | awk '{print $1}') @@ -1322,9 +1351,71 @@ if grep -q "^archipelago:[!*]" /mnt/target/etc/shadow 2>/dev/null; then fi echo " Passwords set for archipelago and root users" -chroot /mnt/target grub-install --target=${GRUB_TARGET} --efi-directory=/boot/efi --bootloader-id=archipelago --removable 2>/dev/null || \ -chroot /mnt/target grub-install --target=${GRUB_TARGET} --efi-directory=/boot/efi --bootloader-id=archipelago 2>/dev/null || \ -echo " Warning: GRUB install had issues, trying alternative..." +# UEFI boot: install to fallback path (/EFI/BOOT/BOOTX64.EFI) for maximum compatibility +echo " Installing UEFI bootloader..." +if chroot /mnt/target grub-install --target=${GRUB_TARGET} --efi-directory=/boot/efi --bootloader-id=archipelago --removable; then + echo " ✅ UEFI bootloader installed (removable/fallback path)" +else + echo " ⚠️ UEFI removable install failed, trying standard..." + if chroot /mnt/target grub-install --target=${GRUB_TARGET} --efi-directory=/boot/efi --bootloader-id=archipelago; then + echo " ✅ UEFI bootloader installed (standard)" + else + echo " ❌ UEFI bootloader installation failed" + fi +fi + +# Secure Boot chain: replace unsigned GRUB with signed shim+grub for Secure Boot compatibility +# Framework laptops and other Secure Boot-enabled machines need this chain: +# BOOTX64.EFI (shimx64, Microsoft-signed) → grubx64.efi (Debian-signed) → kernel +echo " Setting up Secure Boot chain..." +if [ "$ARCH" = "x86_64" ]; then + SHIM_SRC="/mnt/target/usr/lib/shim/shimx64.efi.signed" + GRUB_SIGNED_SRC="/mnt/target/usr/lib/grub/x86_64-efi-signed/grubx64.efi.signed" + EFI_BOOT_BINARY="BOOTX64.EFI" + GRUB_EFI_BINARY="grubx64.efi" + SHIM_EFI_BINARY="shimx64.efi" +else + SHIM_SRC="/mnt/target/usr/lib/shim/shimaa64.efi.signed" + GRUB_SIGNED_SRC="/mnt/target/usr/lib/grub/arm64-efi-signed/grubaa64.efi.signed" + EFI_BOOT_BINARY="BOOTAA64.EFI" + GRUB_EFI_BINARY="grubaa64.efi" + SHIM_EFI_BINARY="shimaa64.efi" +fi +EFI_BOOT_DIR="/mnt/target/boot/efi/EFI/BOOT" +EFI_ARCHY_DIR="/mnt/target/boot/efi/EFI/archipelago" +if [ -f "$SHIM_SRC" ] && [ -f "$GRUB_SIGNED_SRC" ]; then + # Fallback path — what UEFI firmware checks when no boot entry exists + mkdir -p "$EFI_BOOT_DIR" + cp "$SHIM_SRC" "$EFI_BOOT_DIR/$EFI_BOOT_BINARY" + cp "$GRUB_SIGNED_SRC" "$EFI_BOOT_DIR/$GRUB_EFI_BINARY" + # Named entry path — for efibootmgr-registered entries + mkdir -p "$EFI_ARCHY_DIR" + cp "$SHIM_SRC" "$EFI_ARCHY_DIR/$SHIM_EFI_BINARY" + cp "$GRUB_SIGNED_SRC" "$EFI_ARCHY_DIR/$GRUB_EFI_BINARY" + echo " ✅ Secure Boot chain installed (shim + signed GRUB)" +else + echo " ⚠️ Signed shim/GRUB not found — Secure Boot machines must disable Secure Boot" + [ ! -f "$SHIM_SRC" ] && echo " Missing: $(basename $SHIM_SRC)" + [ ! -f "$GRUB_SIGNED_SRC" ] && echo " Missing: $(basename $GRUB_SIGNED_SRC)" +fi + +# Legacy BIOS boot: only install if the installer booted in Legacy BIOS mode +# (if /sys/firmware/efi exists, the machine supports UEFI — no need for BIOS fallback) +if [ -n "${GRUB_BIOS_TARGET}" ] && [ ! -d /sys/firmware/efi ]; then + echo " Installing Legacy BIOS bootloader (machine booted in BIOS mode)..." + if chroot /mnt/target grub-install --target=${GRUB_BIOS_TARGET} "${TARGET_DISK}"; then + echo " ✅ Legacy BIOS bootloader installed" + else + echo " ⚠️ Legacy BIOS bootloader failed (UEFI-only boot)" + fi +elif [ -n "${GRUB_BIOS_TARGET}" ]; then + echo " Skipping Legacy BIOS bootloader (machine supports UEFI)" +fi + +# Regenerate initramfs — the one from Docker export is corrupt/incomplete +# (Docker builds have limited /proc, /sys, /dev so initramfs generation fails silently) +echo " Regenerating initramfs..." +chroot /mnt/target update-initramfs -u -k all chroot /mnt/target update-grub diff --git a/neode-ui/src/main.ts b/neode-ui/src/main.ts index fe3a3d34..a1e949bf 100644 --- a/neode-ui/src/main.ts +++ b/neode-ui/src/main.ts @@ -4,6 +4,7 @@ import './style.css' import App from './App.vue' import router from './router' import i18n from './i18n' +import { useToast } from '@/composables/useToast' const app = createApp(App) const pinia = createPinia() @@ -12,4 +13,10 @@ app.use(pinia) app.use(router) app.use(i18n) +app.config.errorHandler = (err, _instance, info) => { + console.error('[Vue Error]', err, info) + const { error } = useToast() + error('Something went wrong. Please refresh the page.') +} + app.mount('#app') diff --git a/neode-ui/src/stores/app.ts b/neode-ui/src/stores/app.ts index 2eb0eb5d..a7879c11 100644 --- a/neode-ui/src/stores/app.ts +++ b/neode-ui/src/stores/app.ts @@ -15,6 +15,7 @@ export const useAppStore = defineStore('app', () => { const isLoading = ref(false) const error = ref(null) let isWsSubscribed = false + let isWsConnecting = false let sessionValidated = false // Computed @@ -86,10 +87,14 @@ export const useAppStore = defineStore('app', () => { } async function connectWebSocket(): Promise { + // Prevent concurrent connection attempts + if (isWsConnecting) return + isWsConnecting = true + try { if (import.meta.env.DEV) console.log('[Store] Connecting WebSocket...') isReconnecting.value = true - + // Don't create multiple subscriptions - check if already subscribed if (!isWsSubscribed) { // Subscribe to updates BEFORE connecting (so we catch initial data) @@ -159,6 +164,8 @@ export const useAppStore = defineStore('app', () => { isConnected.value = false // Don't throw - allow app to work without real-time updates // The WebSocket will reconnect in the background + } finally { + isWsConnecting = false } } diff --git a/neode-ui/src/stores/mesh.ts b/neode-ui/src/stores/mesh.ts index 148327b6..bd23900e 100644 --- a/neode-ui/src/stores/mesh.ts +++ b/neode-ui/src/stores/mesh.ts @@ -118,6 +118,9 @@ export const useMeshStore = defineStore('mesh', () => { const error = ref(null) const sending = ref(false) + // Serialize send operations to prevent concurrent fetchMessages() races + let sendQueue: Promise = Promise.resolve() + // Node position tracking for map view (contact_id -> position) const nodePositions = ref>(new Map()) @@ -247,24 +250,30 @@ export const useMeshStore = defineStore('mesh', () => { } async function sendMessage(contactId: number, message: string) { - try { - sending.value = true - error.value = null - const res = await rpcClient.call<{ sent: boolean; message_id: number; encrypted: boolean }>({ - method: 'mesh.send', - params: { contact_id: contactId, message: message.trim() }, - }) - // Refresh messages after sending - if (res.sent) { - await fetchMessages() + const doSend = async () => { + try { + sending.value = true + error.value = null + const res = await rpcClient.call<{ sent: boolean; message_id: number; encrypted: boolean }>({ + method: 'mesh.send', + params: { contact_id: contactId, message: message.trim() }, + }) + // Refresh messages after sending + if (res.sent) { + await fetchMessages() + } + return res + } catch (err: unknown) { + error.value = err instanceof Error ? err.message : 'Failed to send mesh message' + throw err + } finally { + sending.value = false } - return res - } catch (err: unknown) { - error.value = err instanceof Error ? err.message : 'Failed to send mesh message' - throw err - } finally { - sending.value = false } + // Chain onto send queue to prevent concurrent fetchMessages() calls + const result = sendQueue.then(doSend, doSend) + sendQueue = result.then(() => {}, () => {}) + return result } async function broadcastIdentity() { diff --git a/scripts/deploy-bitcoin-knots.sh b/scripts/deploy-bitcoin-knots.sh index 60fb1979..260a476a 100644 --- a/scripts/deploy-bitcoin-knots.sh +++ b/scripts/deploy-bitcoin-knots.sh @@ -32,7 +32,7 @@ echo " ✅ Directory created" # Step 2: Deploy Bitcoin Knots node echo "" echo "₿ Deploying Bitcoin Knots node..." -sudo podman run -d \ +podman run -d \ --name bitcoin-knots \ --restart unless-stopped \ -p 8332:8332 \ @@ -89,10 +89,10 @@ EOF cp /home/archipelago/archy/docker/bitcoin-ui/index.html "$BUILD_DIR/" # Build the image -sudo podman build -t localhost/bitcoin-ui:latest "$BUILD_DIR" +podman build -t localhost/bitcoin-ui:latest "$BUILD_DIR" # Deploy UI container -sudo podman run -d \ +podman run -d \ --name bitcoin-ui \ --restart unless-stopped \ -p 8334:80 \ @@ -116,7 +116,7 @@ echo "║ ✅ BITCOIN KNOTS DEPLOYED! ║" echo "╚════════════════════════════════════════════════════════════════╝" echo "" echo "📊 Status:" -sudo podman ps | grep bitcoin +podman ps | grep bitcoin echo "" echo "🌐 Access:" echo " • Web UI: http://YOUR-SERVER-IP:8334" @@ -128,5 +128,5 @@ echo " • User: archipelago" echo " • Pass: (stored in /var/lib/archipelago/secrets/bitcoin-rpc-password)" echo "" echo "⏰ Blockchain sync will take several hours to days." -echo " Check progress: sudo podman logs -f bitcoin-knots" +echo " Check progress: podman logs -f bitcoin-knots" echo "" diff --git a/scripts/deploy-tailscale.sh b/scripts/deploy-tailscale.sh index ac87d918..91ca99f3 100755 --- a/scripts/deploy-tailscale.sh +++ b/scripts/deploy-tailscale.sh @@ -97,13 +97,13 @@ deploy_node() { step "Checking for rootful containers (migration)" ssh $SSH_OPTS "$TARGET" ' # Count rootful containers (run as sudo = rootful podman) - ROOTFUL=$(sudo podman ps -a --format "{{.Names}}" 2>/dev/null | grep -v "^$" | wc -l) + ROOTFUL=$(podman ps -a --format "{{.Names}}" 2>/dev/null | grep -v "^$" | wc -l) ROOTLESS=$(podman ps -a --format "{{.Names}}" 2>/dev/null | grep -v "^$" | wc -l) echo " Rootful: $ROOTFUL containers, Rootless: $ROOTLESS containers" if [ "$ROOTFUL" -gt 0 ]; then echo " MIGRATING: Stopping $ROOTFUL rootful containers..." - sudo podman stop --all --timeout 30 2>/dev/null || true - sudo podman rm --all --force 2>/dev/null || true + podman stop --all --timeout 30 2>/dev/null || true + podman rm --all --force 2>/dev/null || true echo " Rootful containers removed (data preserved in /var/lib/archipelago/)" fi ' 2>&1 diff --git a/scripts/first-boot-containers.sh b/scripts/first-boot-containers.sh index e5a7d72d..a2e6164c 100644 --- a/scripts/first-boot-containers.sh +++ b/scripts/first-boot-containers.sh @@ -245,7 +245,9 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE 'bitcoin-knots|arch BTC_DBCACHE=4096 log " Large disk (${DISK_GB}GB) — enabling txindex" fi - if $DOCKER run -d --name bitcoin-knots --restart unless-stopped --memory=$(mem_limit bitcoin-knots) --network archy-net \ + if $DOCKER run -d --name bitcoin-knots --restart unless-stopped \ + --health-cmd="bitcoin-cli -rpcuser=\$BITCOIN_RPC_USER -rpcpassword=\$BITCOIN_RPC_PASS getblockchaininfo || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit bitcoin-knots) --network archy-net \ --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 \ @@ -277,7 +279,9 @@ fi if ! $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -qE 'archy-mempool-db|mysql-mempool'; then log "Creating mysql-mempool..." mkdir -p /var/lib/archipelago/mysql-mempool - $DOCKER run -d --name archy-mempool-db --restart unless-stopped --memory=$(mem_limit archy-mempool-db) --network archy-net \ + $DOCKER run -d --name archy-mempool-db --restart unless-stopped \ + --health-cmd="mariadb -uroot -e 'SELECT 1' || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit archy-mempool-db) --network archy-net \ -v /var/lib/archipelago/mysql-mempool:/var/lib/mysql \ -e MYSQL_DATABASE=mempool -e MYSQL_USER=mempool -e MYSQL_PASSWORD=$MEMPOOL_DB_PASS \ -e MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASS \ @@ -294,7 +298,9 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q electrumx; then else log "Creating electrumx..." mkdir -p /var/lib/archipelago/electrumx - $DOCKER run -d --name electrumx --restart unless-stopped --memory=$(mem_limit electrumx) --network archy-net \ + $DOCKER run -d --name electrumx --restart unless-stopped \ + --health-cmd="curl -sf http://localhost:8000/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit electrumx) --network archy-net \ -p 50001:50001 -v /var/lib/archipelago/electrumx:/data \ -e "DAEMON_URL=http://$BITCOIN_RPC_USER:$BITCOIN_RPC_PASS@bitcoin-knots:8332/" \ -e COIN=Bitcoin -e DB_DIRECTORY=/data \ @@ -306,7 +312,9 @@ fi if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q mempool-api; then log "Creating mempool-api..." mkdir -p /var/lib/archipelago/mempool - $DOCKER run -d --name mempool-api --restart unless-stopped --memory=$(mem_limit mempool-api) --network archy-net \ + $DOCKER run -d --name mempool-api --restart unless-stopped \ + --health-cmd="curl -sf http://localhost:8999/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit mempool-api) --network archy-net \ -p 8999:8999 -v /var/lib/archipelago/mempool:/data \ -e MEMPOOL_BACKEND=electrum -e ELECTRUM_HOST=electrumx -e ELECTRUM_PORT=50001 \ -e ELECTRUM_TLS_ENABLED=false -e CORE_RPC_HOST="$TARGET_IP" -e CORE_RPC_PORT=8332 \ @@ -318,7 +326,9 @@ fi if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE 'archy-mempool-web|mempool-web'; then log "Creating mempool frontend..." - $DOCKER run -d --name archy-mempool-web --restart unless-stopped --memory=$(mem_limit archy-mempool-web) --network archy-net \ + $DOCKER run -d --name archy-mempool-web --restart unless-stopped \ + --health-cmd="curl -sf http://localhost:8080/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit archy-mempool-web) --network archy-net \ -p 4080:8080 -e FRONTEND_HTTP_PORT=8080 -e BACKEND_MAINNET_HTTP_HOST=mempool-api \ docker.io/mempool/frontend:v2.5.0 2>>"$LOG" || true fi @@ -328,13 +338,16 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q electrs-ui; then if $DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q 'electrs-ui'; then log "Starting ElectrumX UI from pre-built image..." $DOCKER run -d --name archy-electrs-ui --network host --restart unless-stopped \ + --health-cmd="curl -sf http://localhost:80/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ localhost/electrs-ui:latest 2>>"$LOG" || \ $DOCKER run -d --name archy-electrs-ui --network host --restart unless-stopped \ + --health-cmd="curl -sf http://localhost:80/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ electrs-ui:latest 2>>"$LOG" || true elif [ -d /opt/archipelago/docker/electrs-ui ]; then log "Building and starting ElectrumX UI from source..." $DOCKER build -t electrs-ui:latest /opt/archipelago/docker/electrs-ui 2>>"$LOG" && \ $DOCKER run -d --name archy-electrs-ui --network host --restart unless-stopped \ + --health-cmd="curl -sf http://localhost:80/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ electrs-ui:latest 2>>"$LOG" || true else log "ElectrumX UI: no image or source found, skipping" @@ -345,7 +358,9 @@ fi if ! $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -qE 'archy-btcpay-db|postgres-btcpay'; then log "Creating PostgreSQL for BTCPay..." mkdir -p /var/lib/archipelago/postgres-btcpay - $DOCKER run -d --name archy-btcpay-db --restart unless-stopped --memory=$(mem_limit archy-btcpay-db) --network archy-net \ + $DOCKER run -d --name archy-btcpay-db --restart unless-stopped \ + --health-cmd="pg_isready -U postgres || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit archy-btcpay-db) --network archy-net \ -v /var/lib/archipelago/postgres-btcpay:/var/lib/postgresql/data \ -e POSTGRES_DB=btcpay -e POSTGRES_USER=btcpay -e POSTGRES_PASSWORD=$BTCPAY_DB_PASS \ docker.io/postgres:15-alpine 2>>"$LOG" || true @@ -363,7 +378,9 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q archy-nbxplorer; the else log "Creating NBXplorer..." mkdir -p /var/lib/archipelago/nbxplorer - $DOCKER run -d --name archy-nbxplorer --restart unless-stopped --memory=$(mem_limit archy-nbxplorer) --network archy-net \ + $DOCKER run -d --name archy-nbxplorer --restart unless-stopped \ + --health-cmd="curl -sf http://localhost:32838/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit archy-nbxplorer) --network archy-net \ -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 \ @@ -376,7 +393,9 @@ fi if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q btcpay-server; then log "Creating BTCPay Server..." mkdir -p /var/lib/archipelago/btcpay - $DOCKER run -d --name btcpay-server --restart unless-stopped --memory=$(mem_limit btcpay-server) --network archy-net \ + $DOCKER run -d --name btcpay-server --restart unless-stopped \ + --health-cmd="curl -sf http://localhost:49392/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit btcpay-server) --network archy-net \ --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 \ @@ -426,7 +445,9 @@ autopilot.active=false LNDCONF log "LND config created (rpcauth credentials, Tor via system)" fi - $DOCKER run -d --name lnd --restart unless-stopped --memory=$(mem_limit lnd) --network archy-net \ + $DOCKER run -d --name lnd --restart unless-stopped \ + --health-cmd="curl -sf --insecure https://localhost:8080/v1/getinfo || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit lnd) --network archy-net \ --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 9735:9735 -p 10009:10009 -p 8080:8080 \ @@ -438,7 +459,9 @@ fi if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q fedimint; then log "Creating Fedimint..." mkdir -p /var/lib/archipelago/fedimint - $DOCKER run -d --name fedimint --restart unless-stopped --memory=$(mem_limit fedimint) --network archy-net \ + $DOCKER run -d --name fedimint --restart unless-stopped \ + --health-cmd="curl -sf http://localhost:8174/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit fedimint) --network archy-net \ --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 \ @@ -460,7 +483,9 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q fedimint-gateway; th LND_MACAROON=/var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/admin.macaroon if $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q '^lnd$' && [ -f "$LND_CERT" ] && [ -f "$LND_MACAROON" ]; then log " LND detected — using lnd mode" - $DOCKER run -d --name fedimint-gateway --restart unless-stopped --memory=$(mem_limit fedimint-gateway) --network archy-net \ + $DOCKER run -d --name fedimint-gateway --restart unless-stopped \ + --health-cmd="curl -sf http://localhost:8175/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit fedimint-gateway) --network archy-net \ --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 \ @@ -475,7 +500,9 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q fedimint-gateway; th lnd --lnd-rpc-host "$TARGET_IP":10009 --lnd-tls-cert /lnd/tls.cert --lnd-macaroon /lnd/admin.macaroon 2>>"$LOG" || true else log " No LND found — using ldk (built-in Lightning)" - $DOCKER run -d --name fedimint-gateway --restart unless-stopped --memory=$(mem_limit fedimint-gateway) --network archy-net \ + $DOCKER run -d --name fedimint-gateway --restart unless-stopped \ + --health-cmd="curl -sf http://localhost:8175/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit fedimint-gateway) --network archy-net \ --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 \ @@ -497,7 +524,9 @@ sleep 5 # Let core services stabilize if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE 'homeassistant|home-assistant'; then log "Creating Home Assistant..." mkdir -p /var/lib/archipelago/home-assistant - $DOCKER run -d --name homeassistant --restart unless-stopped --memory=$(mem_limit homeassistant) \ + $DOCKER run -d --name homeassistant --restart unless-stopped \ + --health-cmd="curl -sf http://localhost:8123/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit homeassistant) \ --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 \ @@ -510,7 +539,9 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q grafana; then log "Creating Grafana..." mkdir -p /var/lib/archipelago/grafana chown 472:472 /var/lib/archipelago/grafana 2>/dev/null || true - $DOCKER run -d --name grafana --restart unless-stopped --memory=$(mem_limit grafana) \ + $DOCKER run -d --name grafana --restart unless-stopped \ + --health-cmd="curl -sf http://localhost:3000/api/health || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit grafana) \ --cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \ --security-opt no-new-privileges:true \ --read-only --tmpfs /tmp:rw,noexec,nosuid,size=256m --tmpfs /run:rw,noexec,nosuid,size=64m \ @@ -521,7 +552,9 @@ fi if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q uptime-kuma; then log "Creating Uptime Kuma..." mkdir -p /var/lib/archipelago/uptime-kuma - $DOCKER run -d --name uptime-kuma --restart unless-stopped --memory=$(mem_limit uptime-kuma) \ + $DOCKER run -d --name uptime-kuma --restart unless-stopped \ + --health-cmd="curl -sf http://localhost:3001/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit uptime-kuma) \ --cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID \ --security-opt no-new-privileges:true \ -p 3001:3001 -v /var/lib/archipelago/uptime-kuma:/app/data \ @@ -531,7 +564,9 @@ fi if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q jellyfin; then log "Creating Jellyfin..." mkdir -p /var/lib/archipelago/jellyfin/config /var/lib/archipelago/jellyfin/cache - $DOCKER run -d --name jellyfin --restart unless-stopped --memory=$(mem_limit jellyfin) \ + $DOCKER run -d --name jellyfin --restart unless-stopped \ + --health-cmd="curl -sf http://localhost:8096/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit jellyfin) \ --cap-drop ALL --security-opt no-new-privileges:true \ -p 8096:8096 \ -v /var/lib/archipelago/jellyfin/config:/config \ @@ -541,7 +576,9 @@ fi if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q photoprism; then log "Creating PhotoPrism..." mkdir -p /var/lib/archipelago/photoprism - $DOCKER run -d --name photoprism --restart unless-stopped --memory=$(mem_limit photoprism) \ + $DOCKER run -d --name photoprism --restart unless-stopped \ + --health-cmd="curl -sf http://localhost:2342/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit photoprism) \ --cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \ --security-opt no-new-privileges:true \ -p 2342:2342 -v /var/lib/archipelago/photoprism:/photoprism/storage \ @@ -551,7 +588,9 @@ fi if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q ollama; then log "Creating Ollama..." mkdir -p /var/lib/archipelago/ollama - $DOCKER run -d --name ollama --restart unless-stopped --memory=$(mem_limit ollama) \ + $DOCKER run -d --name ollama --restart unless-stopped \ + --health-cmd="curl -sf http://localhost:11434/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit ollama) \ --cap-drop ALL --security-opt no-new-privileges:true \ --read-only --tmpfs /tmp:rw,noexec,nosuid,size=256m --tmpfs /run:rw,noexec,nosuid,size=64m \ -p 11434:11434 -v /var/lib/archipelago/ollama:/root/.ollama \ @@ -560,7 +599,9 @@ fi if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q vaultwarden; then log "Creating Vaultwarden..." mkdir -p /var/lib/archipelago/vaultwarden - $DOCKER run -d --name vaultwarden --restart unless-stopped --memory=$(mem_limit vaultwarden) \ + $DOCKER run -d --name vaultwarden --restart unless-stopped \ + --health-cmd="curl -sf http://localhost:80/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit vaultwarden) \ --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 \ @@ -569,7 +610,9 @@ fi if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q nextcloud; then log "Creating Nextcloud..." mkdir -p /var/lib/archipelago/nextcloud - $DOCKER run -d --name nextcloud --restart unless-stopped --memory=$(mem_limit nextcloud) \ + $DOCKER run -d --name nextcloud --restart unless-stopped \ + --health-cmd="curl -sf http://localhost:80/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit nextcloud) \ --cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \ --security-opt no-new-privileges:true \ -p 8085:80 -v /var/lib/archipelago/nextcloud:/var/www/html \ @@ -577,7 +620,9 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q nextcloud; then fi if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q searxng; then log "Creating SearXNG..." - $DOCKER run -d --name searxng --restart unless-stopped --memory=$(mem_limit searxng) \ + $DOCKER run -d --name searxng --restart unless-stopped \ + --health-cmd="curl -sf http://localhost:8080/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit searxng) \ --cap-drop ALL --security-opt no-new-privileges:true \ --read-only --tmpfs /tmp:rw,noexec,nosuid,size=256m --tmpfs /run:rw,noexec,nosuid,size=64m \ -p 8888:8080 \ @@ -585,7 +630,9 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q searxng; then fi if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q onlyoffice; then log "Creating OnlyOffice..." - $DOCKER run -d --name onlyoffice --restart unless-stopped --memory=$(mem_limit onlyoffice) \ + $DOCKER run -d --name onlyoffice --restart unless-stopped \ + --health-cmd="curl -sf http://localhost:80/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit onlyoffice) \ --cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \ --security-opt no-new-privileges:true \ -p 9980:80 \ @@ -594,14 +641,18 @@ fi if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q filebrowser; then log "Creating File Browser..." mkdir -p /var/lib/archipelago/filebrowser /var/lib/archipelago/filebrowser-db - $DOCKER run -d --name filebrowser --restart unless-stopped --memory=$(mem_limit filebrowser) \ + $DOCKER run -d --name filebrowser --restart unless-stopped \ + --health-cmd="curl -sf http://localhost:80/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit filebrowser) \ -p 8083:80 -v /var/lib/archipelago/filebrowser:/srv \ docker.io/filebrowser/filebrowser:v2.27.0 2>>"$LOG" || true fi if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q nginx-proxy-manager; then log "Creating Nginx Proxy Manager..." mkdir -p /var/lib/archipelago/nginx-proxy-manager/data /var/lib/archipelago/nginx-proxy-manager/letsencrypt - $DOCKER run -d --name nginx-proxy-manager --restart unless-stopped --memory=$(mem_limit nginx-proxy-manager) \ + $DOCKER run -d --name nginx-proxy-manager --restart unless-stopped \ + --health-cmd="curl -sf http://localhost:81/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit nginx-proxy-manager) \ --cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add NET_BIND_SERVICE \ --security-opt no-new-privileges:true \ -p 81:81 -p 8084:80 -p 8443:443 \ @@ -612,7 +663,9 @@ fi if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q portainer; then log "Creating Portainer..." mkdir -p /var/lib/archipelago/portainer - $DOCKER run -d --name portainer --restart unless-stopped --memory=$(mem_limit portainer) \ + $DOCKER run -d --name portainer --restart unless-stopped \ + --health-cmd="curl -sf http://localhost:9000/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit portainer) \ --cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \ --security-opt no-new-privileges:true \ -p 9000:9000 \ @@ -624,7 +677,9 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q tailscale; then log "Creating Tailscale..." mkdir -p /var/lib/archipelago/tailscale # Tailscale needs NET_ADMIN + NET_RAW + TUN device (no --privileged) - $DOCKER run -d --name tailscale --restart unless-stopped --memory=$(mem_limit tailscale) \ + $DOCKER run -d --name tailscale --restart unless-stopped \ + --health-cmd="tailscale status || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit tailscale) \ --network host \ --cap-drop=ALL \ --cap-add=NET_ADMIN \ @@ -651,7 +706,9 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q immich_server; then mkdir -p /var/lib/archipelago/immich /var/lib/archipelago/immich-db $DOCKER network create immich-net 2>/dev/null || true if ! $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -q immich_postgres; then - $DOCKER run -d --name immich_postgres --restart unless-stopped --memory=$(mem_limit immich_postgres) --network immich-net \ + $DOCKER run -d --name immich_postgres --restart unless-stopped \ + --health-cmd="pg_isready -U postgres || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit immich_postgres) --network immich-net \ -v /var/lib/archipelago/immich-db:/var/lib/postgresql/data \ -e POSTGRES_PASSWORD=$IMMICH_DB_PASS -e POSTGRES_USER=postgres -e POSTGRES_DB=immich \ ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0 2>>"$LOG" || true @@ -662,12 +719,16 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q immich_server; then done fi if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q immich_redis; then - $DOCKER run -d --name immich_redis --restart unless-stopped --memory=$(mem_limit immich_redis) --network immich-net \ + $DOCKER run -d --name immich_redis --restart unless-stopped \ + --health-cmd="redis-cli ping || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit immich_redis) --network immich-net \ docker.io/valkey/valkey:7-alpine 2>>"$LOG" || true sleep 2 fi if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q immich_server; then - $DOCKER run -d --name immich_server --restart unless-stopped --memory=$(mem_limit immich_server) --network immich-net \ + $DOCKER run -d --name immich_server --restart unless-stopped \ + --health-cmd="curl -sf http://localhost:2283/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit immich_server) --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=$IMMICH_DB_PASS \ -e DB_DATABASE_NAME=immich -e REDIS_HOSTNAME=immich_redis \ @@ -682,20 +743,26 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q penpot-frontend; the mkdir -p /var/lib/archipelago/penpot-assets /var/lib/archipelago/penpot-postgres $DOCKER network create penpot-net 2>/dev/null || true if ! $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -q penpot-postgres; then - $DOCKER run -d --name penpot-postgres --restart unless-stopped --memory=$(mem_limit penpot-postgres) --network penpot-net \ + $DOCKER run -d --name penpot-postgres --restart unless-stopped \ + --health-cmd="pg_isready -U penpot || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit penpot-postgres) --network penpot-net \ -v /var/lib/archipelago/penpot-postgres:/var/lib/postgresql/data \ -e POSTGRES_DB=penpot -e POSTGRES_USER=penpot -e POSTGRES_PASSWORD=$PENPOT_DB_PASS \ docker.io/postgres:15 2>>"$LOG" || true sleep 5 fi if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q penpot-valkey; then - $DOCKER run -d --name penpot-valkey --restart unless-stopped --memory=$(mem_limit penpot-valkey) --network penpot-net \ + $DOCKER run -d --name penpot-valkey --restart unless-stopped \ + --health-cmd="redis-cli ping || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit penpot-valkey) --network penpot-net \ -e VALKEY_EXTRA_FLAGS="--maxmemory 128mb --maxmemory-policy volatile-lfu" \ docker.io/valkey/valkey:8.1 2>>"$LOG" || true sleep 3 fi if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q penpot-backend; then - $DOCKER run -d --name penpot-backend --restart unless-stopped --memory=$(mem_limit penpot-backend) --network penpot-net \ + $DOCKER run -d --name penpot-backend --restart unless-stopped \ + --health-cmd="curl -sf http://localhost:6060/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit penpot-backend) --network penpot-net \ -v /var/lib/archipelago/penpot-assets:/opt/data/assets \ -e PENPOT_PUBLIC_URI="http://${TARGET_IP}:9001" \ -e PENPOT_SECRET_KEY=archipelago-penpot-secret-key-change-in-production \ @@ -709,7 +776,9 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q penpot-frontend; the sleep 5 fi if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q penpot-exporter; then - $DOCKER run -d --name penpot-exporter --restart unless-stopped --memory=$(mem_limit penpot-exporter) --network penpot-net \ + $DOCKER run -d --name penpot-exporter --restart unless-stopped \ + --health-cmd="curl -sf http://localhost:6061/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit penpot-exporter) --network penpot-net \ -e PENPOT_SECRET_KEY=archipelago-penpot-secret-key-change-in-production \ -e PENPOT_PUBLIC_URI=http://penpot-frontend:8080 \ -e PENPOT_REDIS_URI=redis://penpot-valkey/0 \ @@ -717,7 +786,9 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q penpot-frontend; the sleep 2 fi if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q penpot-frontend; then - $DOCKER run -d --name penpot-frontend --restart unless-stopped --memory=$(mem_limit penpot-frontend) --network penpot-net \ + $DOCKER run -d --name penpot-frontend --restart unless-stopped \ + --health-cmd="curl -sf http://localhost:80/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit penpot-frontend) --network penpot-net \ -p 9001:8080 -v /var/lib/archipelago/penpot-assets:/opt/data/assets \ -e PENPOT_PUBLIC_URI="http://${TARGET_IP}:9001" \ -e PENPOT_FLAGS=disable-email-verification enable-smtp enable-prepl-server disable-secure-session-cookies \ @@ -731,7 +802,9 @@ if $DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q 'nos if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q nostr-rs-relay; then log "Creating nostr-rs-relay..." mkdir -p /var/lib/archipelago/nostr-rs-relay - $DOCKER run -d --name nostr-rs-relay --restart unless-stopped --memory=$(mem_limit nostr-rs-relay) \ + $DOCKER run -d --name nostr-rs-relay --restart unless-stopped \ + --health-cmd="curl -sf http://localhost:8080/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit nostr-rs-relay) \ -p 7047:7047 -v /var/lib/archipelago/nostr-rs-relay:/data \ scsibug/nostr-rs-relay:latest 2>>"$LOG" || true fi @@ -740,7 +813,9 @@ if $DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q 'str if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q strfry; then log "Creating strfry..." mkdir -p /var/lib/archipelago/strfry - $DOCKER run -d --name strfry --restart unless-stopped --memory=$(mem_limit strfry) \ + $DOCKER run -d --name strfry --restart unless-stopped \ + --health-cmd="curl -sf http://localhost:7777/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit strfry) \ -p 7777:7777 -v /var/lib/archipelago/strfry:/data \ hoytech/strfry:latest 2>>"$LOG" || true fi @@ -758,7 +833,9 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q indeedhub; then fi if [ -n "$INDEEDHUB_IMAGE" ]; then log "Creating Indeehub from $INDEEDHUB_IMAGE..." - $DOCKER run -d --name indeedhub --restart unless-stopped --memory=$(mem_limit indeedhub) \ + $DOCKER run -d --name indeedhub --restart unless-stopped \ + --health-cmd="curl -sf http://localhost:80/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \ + --memory=$(mem_limit indeedhub) \ --cap-drop ALL --security-opt no-new-privileges:true \ --read-only --tmpfs /tmp:rw,noexec,nosuid,size=64m --tmpfs /app/.next/cache:rw,noexec,nosuid,size=128m \ -p 7777:7777 \ diff --git a/scripts/fix-indeedhub-containers.sh b/scripts/fix-indeedhub-containers.sh index ad3615c5..6db8702c 100755 --- a/scripts/fix-indeedhub-containers.sh +++ b/scripts/fix-indeedhub-containers.sh @@ -19,13 +19,13 @@ NETWORK="indeedhub-build_indeedhub-network" # Load custom images if tar exists if [ -f /tmp/indeedhub-images.tar ]; then echo "Loading custom images from tar..." - sudo podman load < /tmp/indeedhub-images.tar 2>&1 | tail -5 + podman load < /tmp/indeedhub-images.tar 2>&1 | tail -5 fi # Verify correct images are available echo "Verifying images..." for img in "docker.io/library/redis:7-alpine" "docker.io/minio/minio:latest" "docker.io/library/postgres:16-alpine" "docker.io/scsibug/nostr-rs-relay:latest" "docker.io/searxng/searxng:latest" "localhost/indeedhub:latest" "localhost/indeedhub-build_api:latest" "localhost/indeedhub-build_ffmpeg-worker:latest"; do - if ! sudo podman image exists "$img" 2>/dev/null; then + if ! podman image exists "$img" 2>/dev/null; then echo "ERROR: Missing image $img" exit 1 fi @@ -33,26 +33,26 @@ done echo "All images verified." # Ensure network exists -if ! sudo podman network exists "$NETWORK" 2>/dev/null; then +if ! podman network exists "$NETWORK" 2>/dev/null; then echo "Creating network $NETWORK..." - sudo podman network create "$NETWORK" 2>/dev/null || true + podman network create "$NETWORK" 2>/dev/null || true fi # Stop all affected containers echo "Stopping containers..." for c in indeedhub indeedhub-build_api_1 indeedhub-build_ffmpeg-worker_1 indeedhub-relay indeedhub-redis indeedhub-minio indeedhub-postgres searxng; do - sudo podman stop "$c" 2>/dev/null || true + podman stop "$c" 2>/dev/null || true done # Remove all affected containers echo "Removing containers..." for c in indeedhub indeedhub-build_api_1 indeedhub-build_ffmpeg-worker_1 indeedhub-relay indeedhub-redis indeedhub-minio indeedhub-postgres searxng; do - sudo podman rm -f "$c" 2>/dev/null || true + podman rm -f "$c" 2>/dev/null || true done # 1. PostgreSQL (must start first — others depend on it) echo "Creating postgres..." -sudo podman run -d --name indeedhub-postgres \ +podman run -d --name indeedhub-postgres \ --restart unless-stopped \ --network "$NETWORK" --network-alias postgres \ -v indeedhub-postgres-data:/var/lib/postgresql/data \ @@ -64,7 +64,7 @@ sudo podman run -d --name indeedhub-postgres \ # Wait for postgres to be ready echo "Waiting for postgres..." for i in $(seq 1 15); do - if sudo podman exec indeedhub-postgres pg_isready -U indeedhub 2>/dev/null; then + if podman exec indeedhub-postgres pg_isready -U indeedhub 2>/dev/null; then echo "Postgres ready." break fi @@ -73,7 +73,7 @@ done # 2. Redis echo "Creating redis..." -sudo podman run -d --name indeedhub-redis \ +podman run -d --name indeedhub-redis \ --restart unless-stopped \ --network "$NETWORK" --network-alias redis \ -v indeedhub-redis-data:/data \ @@ -82,7 +82,7 @@ sudo podman run -d --name indeedhub-redis \ # 3. MinIO echo "Creating minio..." -sudo podman run -d --name indeedhub-minio \ +podman run -d --name indeedhub-minio \ --restart unless-stopped \ --network "$NETWORK" --network-alias minio \ -v indeedhub-minio-data:/data \ @@ -93,7 +93,7 @@ sudo podman run -d --name indeedhub-minio \ # 4. Nostr Relay echo "Creating relay..." -sudo podman run -d --name indeedhub-relay \ +podman run -d --name indeedhub-relay \ --restart unless-stopped \ --network "$NETWORK" --network-alias relay \ -v indeedhub-relay-data:/usr/src/app/db \ @@ -101,7 +101,7 @@ sudo podman run -d --name indeedhub-relay \ # 5. API echo "Creating api..." -sudo podman run -d --name indeedhub-build_api_1 \ +podman run -d --name indeedhub-build_api_1 \ --restart unless-stopped \ --network "$NETWORK" --network-alias api \ -e ENVIRONMENT=production \ @@ -142,7 +142,7 @@ sudo podman run -d --name indeedhub-build_api_1 \ # 6. FFmpeg Worker echo "Creating ffmpeg-worker..." -sudo podman run -d --name indeedhub-build_ffmpeg-worker_1 \ +podman run -d --name indeedhub-build_ffmpeg-worker_1 \ --restart unless-stopped \ --network "$NETWORK" --network-alias ffmpeg-worker \ -e ENVIRONMENT=production \ @@ -166,7 +166,7 @@ sudo podman run -d --name indeedhub-build_ffmpeg-worker_1 \ # 7. IndeedHub Frontend echo "Creating indeedhub frontend..." -sudo podman run -d --name indeedhub \ +podman run -d --name indeedhub \ --restart unless-stopped \ --network "$NETWORK" \ -p 7777:7777 \ @@ -179,48 +179,48 @@ sudo podman run -d --name indeedhub \ # Fix IndeedHub for iframe: remove X-Frame-Options, inject nostr-provider, hardcode container IPs sleep 3 -if sudo podman ps --format '{{.Names}}' 2>/dev/null | grep -q "^indeedhub$"; then - sudo podman exec indeedhub sed -i "/X-Frame-Options/d" /etc/nginx/conf.d/default.conf 2>/dev/null || true +if podman ps --format '{{.Names}}' 2>/dev/null | grep -q "^indeedhub$"; then + podman exec indeedhub sed -i "/X-Frame-Options/d" /etc/nginx/conf.d/default.conf 2>/dev/null || true # Inject nostr-provider.js if available if [ -f /opt/archipelago/web-ui/nostr-provider.js ]; then - sudo podman cp /opt/archipelago/web-ui/nostr-provider.js indeedhub:/usr/share/nginx/html/nostr-provider.js 2>/dev/null || true + podman cp /opt/archipelago/web-ui/nostr-provider.js indeedhub:/usr/share/nginx/html/nostr-provider.js 2>/dev/null || true fi # Add nostr-provider location block + sub_filter - if ! sudo podman exec indeedhub grep -q "nostr-provider" /etc/nginx/conf.d/default.conf 2>/dev/null; then - sudo podman exec indeedhub cat /etc/nginx/conf.d/default.conf > /tmp/ih-nginx.conf 2>/dev/null + if ! podman exec indeedhub grep -q "nostr-provider" /etc/nginx/conf.d/default.conf 2>/dev/null; then + podman exec indeedhub cat /etc/nginx/conf.d/default.conf > /tmp/ih-nginx.conf 2>/dev/null sed -i "/location = \/sw.js {/i\\ location = /nostr-provider.js {\n add_header Cache-Control \"no-cache, no-store, must-revalidate\";\n expires off;\n }\n" /tmp/ih-nginx.conf sed -i "/try_files.*index.html/a\\ sub_filter_once on;\n sub_filter '' '';" /tmp/ih-nginx.conf - sudo podman cp /tmp/ih-nginx.conf indeedhub:/etc/nginx/conf.d/default.conf 2>/dev/null || true + podman cp /tmp/ih-nginx.conf indeedhub:/etc/nginx/conf.d/default.conf 2>/dev/null || true rm -f /tmp/ih-nginx.conf fi # Replace DNS-based upstream resolution with hardcoded container IPs # (podman DNS resolver 127.0.0.11 is unreliable, causing 502 errors) - API_IP=$(sudo podman inspect indeedhub-build_api_1 --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" 2>/dev/null) - MINIO_IP=$(sudo podman inspect indeedhub-minio --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" 2>/dev/null) - RELAY_IP=$(sudo podman inspect indeedhub-relay --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" 2>/dev/null) + API_IP=$(podman inspect indeedhub-build_api_1 --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" 2>/dev/null) + MINIO_IP=$(podman inspect indeedhub-minio --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" 2>/dev/null) + RELAY_IP=$(podman inspect indeedhub-relay --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" 2>/dev/null) if [ -n "$API_IP" ] && [ -n "$MINIO_IP" ] && [ -n "$RELAY_IP" ]; then - sudo podman exec indeedhub cat /etc/nginx/conf.d/default.conf > /tmp/ih-nginx.conf 2>/dev/null + podman exec indeedhub cat /etc/nginx/conf.d/default.conf > /tmp/ih-nginx.conf 2>/dev/null sed -i "s|resolver 127.0.0.11 valid=30s ipv6=off;||g" /tmp/ih-nginx.conf sed -i "s|set \$api_upstream http://api:4000;|set \$api_upstream http://$API_IP:4000;|g" /tmp/ih-nginx.conf sed -i "s|set \$minio_upstream http://minio:9000;|set \$minio_upstream http://$MINIO_IP:9000;|g" /tmp/ih-nginx.conf sed -i "s|set \$relay_upstream http://relay:8080;|set \$relay_upstream http://$RELAY_IP:8080;|g" /tmp/ih-nginx.conf sed -i "s|proxy_set_header Host \$host;|proxy_set_header Host \$http_host;|g" /tmp/ih-nginx.conf - sudo podman cp /tmp/ih-nginx.conf indeedhub:/etc/nginx/conf.d/default.conf 2>/dev/null || true + podman cp /tmp/ih-nginx.conf indeedhub:/etc/nginx/conf.d/default.conf 2>/dev/null || true rm -f /tmp/ih-nginx.conf echo "Patched IndeedHub nginx with container IPs (API=$API_IP MINIO=$MINIO_IP RELAY=$RELAY_IP)" fi - sudo podman exec indeedhub nginx -s reload 2>/dev/null || true + podman exec indeedhub nginx -s reload 2>/dev/null || true echo "Applied IndeedHub iframe fix." fi # 8. SearXNG (standalone — no cap-drop ALL, searxng needs write access to /etc/searxng/) echo "Creating searxng..." -sudo podman run -d --name searxng \ +podman run -d --name searxng \ --restart unless-stopped \ -p 8888:8080 \ docker.io/searxng/searxng:latest @@ -228,6 +228,6 @@ sudo podman run -d --name searxng \ echo "" echo "=== Verifying container status ===" sleep 5 -sudo podman ps -a --filter name=indeedhub --filter name=searxng --format "table {{.Names}}\t{{.Status}}" 2>&1 +podman ps -a --filter name=indeedhub --filter name=searxng --format "table {{.Names}}\t{{.Status}}" 2>&1 echo "" echo "=== FIX COMPLETE ===" diff --git a/scripts/setup-aiui-server.sh b/scripts/setup-aiui-server.sh index 78621691..49ff0dce 100755 --- a/scripts/setup-aiui-server.sh +++ b/scripts/setup-aiui-server.sh @@ -125,15 +125,15 @@ ssh $SSH_OPTS "$TARGET_HOST" "sudo nginx -t 2>&1 && sudo systemctl reload nginx echo "" echo "$(timestamp) 📁 Checking FileBrowser..." -FB_STATUS=$(ssh $SSH_OPTS "$TARGET_HOST" "sudo podman inspect filebrowser 2>/dev/null | grep -oP '\"ReadonlyRootfs\":\s*\K\w+'" 2>/dev/null || echo "not_found") +FB_STATUS=$(ssh $SSH_OPTS "$TARGET_HOST" "podman inspect filebrowser 2>/dev/null | grep -oP '\"ReadonlyRootfs\":\s*\K\w+'" 2>/dev/null || echo "not_found") if [ "$FB_STATUS" = "true" ]; then echo " FileBrowser has read-only root — recreating..." ssh $SSH_OPTS "$TARGET_HOST" " - sudo podman stop filebrowser 2>/dev/null - sudo podman rm filebrowser 2>/dev/null + podman stop filebrowser 2>/dev/null + podman rm filebrowser 2>/dev/null sudo mkdir -p /var/lib/archipelago/filebrowser - sudo podman run -d --name filebrowser --restart=always \ + podman run -d --name filebrowser --restart=always \ -p 8083:80 \ -v /var/lib/archipelago/filebrowser:/srv \ filebrowser/filebrowser:v2.27.0 @@ -143,7 +143,7 @@ elif [ "$FB_STATUS" = "not_found" ]; then echo " FileBrowser not found — creating..." ssh $SSH_OPTS "$TARGET_HOST" " sudo mkdir -p /var/lib/archipelago/filebrowser - sudo podman run -d --name filebrowser --restart=always \ + podman run -d --name filebrowser --restart=always \ -p 8083:80 \ -v /var/lib/archipelago/filebrowser:/srv \ filebrowser/filebrowser:v2.27.0 @@ -158,7 +158,7 @@ echo "" echo "$(timestamp) ✅ Verification..." ssh $SSH_OPTS "$TARGET_HOST" " echo \" AIUI index: \$(ls -la /opt/archipelago/web-ui/aiui/index.html 2>/dev/null | awk '{print \$6,\$7,\$8}')\" - echo \" FileBrowser: \$(sudo podman ps --format '{{.Names}} {{.Status}}' | grep filebrowser)\" + echo \" FileBrowser: \$(podman ps --format '{{.Names}} {{.Status}}' | grep filebrowser)\" echo \" Nginx: \$(systemctl is-active nginx)\" echo \" Backend: \$(systemctl is-active archipelago)\" echo \" Claude API test: \$(curl -s -o /dev/null -w '%{http_code}' -X POST http://localhost/aiui/api/claude/v1/messages -H 'Content-Type: application/json' -H 'Cookie: session=test' -d '{\"model\":\"claude-sonnet-4-20250514\",\"max_tokens\":5,\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}')\" diff --git a/scripts/uptime-monitor.sh b/scripts/uptime-monitor.sh index 4f91638b..c28a0193 100755 --- a/scripts/uptime-monitor.sh +++ b/scripts/uptime-monitor.sh @@ -57,7 +57,7 @@ DISK_TOTAL=$(echo "$STATS" | python3 -c "import sys,json; d=json.load(sys.stdin) UPTIME=$(echo "$STATS" | python3 -c "import sys,json; d=json.load(sys.stdin).get('result',{}); print(d.get('uptime_secs',0))" 2>/dev/null || echo "0") # Count running containers -CONTAINERS=$(sudo podman ps --format "{{.Names}}" 2>/dev/null | wc -l || echo "0") +CONTAINERS=$(podman ps --format "{{.Names}}" 2>/dev/null | wc -l || echo "0") # Detect restart (uptime < 300s = likely just restarted) if [ "$UPTIME" -lt 300 ] 2>/dev/null; then