diff --git a/image-recipe/archipelago-scripts/archipelago-menu.sh b/image-recipe/archipelago-scripts/archipelago-menu.sh index 28039133..7ae57d22 100755 --- a/image-recipe/archipelago-scripts/archipelago-menu.sh +++ b/image-recipe/archipelago-scripts/archipelago-menu.sh @@ -240,7 +240,7 @@ setup_btcpay() { echo "" echo " 🐳 Pulling BTCPay Server image..." - podman pull docker.io/btcpayserver/btcpayserver:latest + podman pull "${BTCPAY_IMAGE:-docker.io/btcpayserver/btcpayserver:1.14.5}" # Create data directory mkdir -p ~/.btcpay diff --git a/image-recipe/build-auto-installer-iso.sh b/image-recipe/build-auto-installer-iso.sh index 21d22e86..3bbf9ade 100755 --- a/image-recipe/build-auto-installer-iso.sh +++ b/image-recipe/build-auto-installer-iso.sh @@ -24,6 +24,10 @@ set -e +# Source pinned image versions (single source of truth) +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +[ -f "$SCRIPT_DIR/../scripts/image-versions.sh" ] && . "$SCRIPT_DIR/../scripts/image-versions.sh" + # Configuration DEV_SERVER="${DEV_SERVER:-archipelago@192.168.1.228}" BUILD_FROM_SOURCE="${BUILD_FROM_SOURCE:-0}" @@ -597,37 +601,37 @@ fi # bitcoin-ui and lnd-ui are custom and normally captured from server or built separately. # Alpha: core Bitcoin/Lightning stack + essential apps. Others pulled on-demand from Marketplace. CONTAINER_IMAGES=" -docker.io/bitcoinknots/bitcoin:latest bitcoin-knots.tar -docker.io/lightninglabs/lnd:v0.18.4-beta lnd.tar -docker.io/homeassistant/home-assistant:2024.1 homeassistant.tar -docker.io/btcpayserver/btcpayserver:1.13.5 btcpayserver.tar -docker.io/nicolasdorier/nbxplorer:2.6.0 nbxplorer.tar -docker.io/library/postgres:15-alpine postgres-btcpay.tar -docker.io/mempool/frontend:v2.5.0 mempool-frontend.tar -docker.io/mempool/backend:v2.5.0 mempool-backend.tar -docker.io/lukechilds/electrumx:v1.18.0 electrumx.tar -docker.io/library/mariadb:10.11 mariadb-mempool.tar -docker.io/fedimint/fedimintd:v0.10.0 fedimint.tar -docker.io/fedimint/gatewayd:v0.10.0 fedimint-gateway.tar -docker.io/filebrowser/filebrowser:v2.27.0 filebrowser.tar -docker.io/andrius/alpine-tor:latest alpine-tor.tar -docker.io/library/nginx:alpine nginx-alpine.tar +${BITCOIN_KNOTS_IMAGE:-docker.io/bitcoinknots/bitcoin:v28.1} bitcoin-knots.tar +${LND_IMAGE:-docker.io/lightninglabs/lnd:v0.18.5-beta} lnd.tar +${HOMEASSISTANT_IMAGE:-ghcr.io/home-assistant/home-assistant:2024.12} homeassistant.tar +${BTCPAY_IMAGE:-docker.io/btcpayserver/btcpayserver:1.14.5} btcpayserver.tar +${NBXPLORER_IMAGE:-docker.io/nicolasdorier/nbxplorer:2.5.13} nbxplorer.tar +${POSTGRES_IMAGE:-docker.io/library/postgres:16} postgres-btcpay.tar +${MEMPOOL_API_IMAGE:-docker.io/mempool/frontend:v3.0.0} mempool-frontend.tar +${MEMPOOL_WEB_IMAGE:-docker.io/mempool/frontend:v3.0.0} mempool-backend.tar +${ELECTRUMX_IMAGE:-docker.io/lukechilds/electrumx:v1.16.0} electrumx.tar +${MARIADB_IMAGE:-docker.io/library/mariadb:11.4} mariadb-mempool.tar +${FEDIMINT_IMAGE:-docker.io/fedimint/fedimintd:v0.5.1} fedimint.tar +${FEDIMINT_GATEWAY_IMAGE:-docker.io/fedimint/gatewayd:v0.5.1} fedimint-gateway.tar +${FILEBROWSER_IMAGE:-docker.io/filebrowser/filebrowser:v2} filebrowser.tar +${ALPINE_TOR_IMAGE:-docker.io/andrius/alpine-tor:0.4.8.13} alpine-tor.tar +${NGINX_ALPINE_IMAGE:-docker.io/library/nginx:alpine} nginx-alpine.tar ghcr.io/tbd54566975/dwn-server:main dwn-server.tar -docker.io/grafana/grafana:10.2.0 grafana.tar -docker.io/louislam/uptime-kuma:1 uptime-kuma.tar -docker.io/vaultwarden/server:1.30.0-alpine vaultwarden.tar -docker.io/searxng/searxng:latest searxng.tar -docker.io/portainer/portainer-ce:2.19.4 portainer.tar -docker.io/tailscale/tailscale:stable tailscale.tar -docker.io/jellyfin/jellyfin:10.8.13 jellyfin.tar -docker.io/photoprism/photoprism:latest photoprism.tar -docker.io/library/nextcloud:28-apache nextcloud.tar -docker.io/jc21/nginx-proxy-manager:latest nginx-proxy-manager.tar -ghcr.io/immich-app/immich-server:release immich-server.tar +${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.4.0} grafana.tar +${UPTIME_KUMA_IMAGE:-docker.io/louislam/uptime-kuma:1} uptime-kuma.tar +${VAULTWARDEN_IMAGE:-docker.io/vaultwarden/server:1.32.5} vaultwarden.tar +${SEARXNG_IMAGE:-docker.io/searxng/searxng:2024.11.17} searxng.tar +${PORTAINER_IMAGE:-docker.io/portainer/portainer-ce:2.21.5} portainer.tar +${TAILSCALE_IMAGE:-docker.io/tailscale/tailscale:v1.78.3} tailscale.tar +${JELLYFIN_IMAGE:-docker.io/jellyfin/jellyfin:10.10.3} jellyfin.tar +${PHOTOPRISM_IMAGE:-docker.io/photoprism/photoprism:240915} photoprism.tar +${NEXTCLOUD_IMAGE:-docker.io/library/nextcloud:30} nextcloud.tar +${NPM_IMAGE:-docker.io/jc21/nginx-proxy-manager:2} nginx-proxy-manager.tar +${IMMICH_IMAGE:-ghcr.io/immich-app/immich-server:v1.123.0} immich-server.tar docker.io/library/postgres:14-alpine postgres-immich.tar -docker.io/library/redis:7-alpine redis-immich.tar -docker.io/onlyoffice/documentserver:8.0 onlyoffice.tar -docker.io/adguard/adguardhome:latest adguardhome.tar +${INDEEDHUB_REDIS_IMAGE:-docker.io/library/redis:7-alpine} redis-immich.tar +${ONLYOFFICE_IMAGE:-docker.io/onlyoffice/documentserver:8.2} onlyoffice.tar +${ADGUARDHOME_IMAGE:-docker.io/adguard/adguardhome:v0.107.55} adguardhome.tar " # Pull and save each image (force target arch) only if not already present @@ -806,7 +810,7 @@ else sudo $DOCKER run -d --name archy-tor --restart unless-stopped --network host \ -v "$TOR_DIR:$TOR_DIR" \ --entrypoint tor \ - docker.io/andrius/alpine-tor:latest \ + ${ALPINE_TOR_IMAGE:-docker.io/andrius/alpine-tor:0.4.8.13} \ -f /etc/tor/torrc >> "$LOG" 2>&1 echo "$(date): Tor container started" >> "$LOG" fi diff --git a/neode-ui/src/composables/useAudioPlayer.ts b/neode-ui/src/composables/useAudioPlayer.ts index ce91f5ac..70f5ea03 100644 --- a/neode-ui/src/composables/useAudioPlayer.ts +++ b/neode-ui/src/composables/useAudioPlayer.ts @@ -7,46 +7,52 @@ const playing = ref(false) const currentTime = ref(0) const duration = ref(0) const error = ref(null) +let initialized = false + +/** Create the Audio element and attach listeners once */ +function init() { + if (initialized) return + initialized = true + audio.value = new Audio() + audio.value.addEventListener('timeupdate', () => { + currentTime.value = audio.value?.currentTime ?? 0 + }) + audio.value.addEventListener('loadedmetadata', () => { + duration.value = audio.value?.duration ?? 0 + error.value = null + }) + audio.value.addEventListener('ended', () => { + playing.value = false + }) + audio.value.addEventListener('pause', () => { + playing.value = false + }) + audio.value.addEventListener('play', () => { + playing.value = true + error.value = null + }) + audio.value.addEventListener('error', () => { + playing.value = false + error.value = 'Could not play audio. File Browser may not be running.' + }) +} function play(src: string, name: string) { - if (!audio.value) { - audio.value = new Audio() - audio.value.addEventListener('timeupdate', () => { - currentTime.value = audio.value?.currentTime ?? 0 - }) - audio.value.addEventListener('loadedmetadata', () => { - duration.value = audio.value?.duration ?? 0 - error.value = null - }) - audio.value.addEventListener('ended', () => { - playing.value = false - }) - audio.value.addEventListener('pause', () => { - playing.value = false - }) - audio.value.addEventListener('play', () => { - playing.value = true - error.value = null - }) - audio.value.addEventListener('error', () => { - playing.value = false - error.value = 'Could not play audio. File Browser may not be running.' - }) - } + init() error.value = null if (currentSrc.value === src && playing.value) { - audio.value.pause() + audio.value!.pause() return } if (currentSrc.value !== src) { - audio.value.src = src + audio.value!.src = src currentSrc.value = src currentName.value = name } - audio.value.play() + audio.value!.play() } function pause() { diff --git a/neode-ui/src/composables/useMessageToast.ts b/neode-ui/src/composables/useMessageToast.ts index c4f3c64e..cc5de069 100644 --- a/neode-ui/src/composables/useMessageToast.ts +++ b/neode-ui/src/composables/useMessageToast.ts @@ -54,10 +54,21 @@ export function useMessageToast() { } } + function isAuthenticated(): boolean { + return localStorage.getItem('neode-auth') === 'true' + } + function startPolling() { if (pollTimer) return + if (!isAuthenticated()) return loadReceivedMessages() - pollTimer = setInterval(loadReceivedMessages, MESSAGE_POLL_INTERVAL) + pollTimer = setInterval(() => { + if (!isAuthenticated()) { + stopPolling() + return + } + loadReceivedMessages() + }, MESSAGE_POLL_INTERVAL) } function stopPolling() { diff --git a/neode-ui/src/stores/app.ts b/neode-ui/src/stores/app.ts index a7879c11..4a10b100 100644 --- a/neode-ui/src/stores/app.ts +++ b/neode-ui/src/stores/app.ts @@ -149,7 +149,18 @@ export const useAppStore = defineStore('app', () => { await wsClient.connect() if (import.meta.env.DEV) console.log('[Store] WebSocket connected') - + + // Fetch fresh state after reconnect to avoid stale patch application + try { + const freshState = await rpcClient.call<{ data: DataModel }>({ method: 'server.get-state' }) + if (freshState?.data) { + data.value = freshState.data + } + } catch { + // Non-fatal: WebSocket patches will still work + if (import.meta.env.DEV) console.warn('[Store] Failed to fetch fresh state after reconnect') + } + // Connection state will be updated via the callback if (wsClient.isConnected()) { isConnected.value = true diff --git a/neode-ui/src/stores/appLauncher.ts b/neode-ui/src/stores/appLauncher.ts index 8eb18444..8541091f 100644 --- a/neode-ui/src/stores/appLauncher.ts +++ b/neode-ui/src/stores/appLauncher.ts @@ -162,6 +162,9 @@ export const useAppLauncherStore = defineStore('appLauncher', () => { isOpen.value = false url.value = '' title.value = '' + // Explicitly remove NIP-07 listener as safety net — if user navigates away + // without close() triggering the isOpen watcher, the listener would leak + window.removeEventListener('message', handleNostrRequest) if (toRestore && typeof toRestore.focus === 'function') { requestAnimationFrame(() => { toRestore.focus() diff --git a/scripts/container-doctor.sh b/scripts/container-doctor.sh index fc80c193..eb0387a3 100755 --- a/scripts/container-doctor.sh +++ b/scripts/container-doctor.sh @@ -20,6 +20,10 @@ set -o pipefail +# Source pinned image versions (single source of truth) +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +[ -f "$SCRIPT_DIR/image-versions.sh" ] && . "$SCRIPT_DIR/image-versions.sh" + FIXES_APPLIED=0 CHECKS_PASSED=0 FIX_NAMES=() @@ -194,7 +198,7 @@ fix_searxng() { -v searxng-cache:/var/cache/searxng:rw \ -p "${host_port}:8080" \ --memory=512m \ - docker.io/searxng/searxng:latest 2>&1 || true + "${SEARXNG_IMAGE:-docker.io/searxng/searxng:2024.11.17}" 2>&1 || true log "SearXNG recreated (no readonly, no cap-drop ALL)" return 0 diff --git a/scripts/deploy-bitcoin-knots.sh b/scripts/deploy-bitcoin-knots.sh index 260a476a..8dabca7c 100644 --- a/scripts/deploy-bitcoin-knots.sh +++ b/scripts/deploy-bitcoin-knots.sh @@ -9,6 +9,10 @@ set -e +# Source pinned image versions (single source of truth) +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +[ -f "$SCRIPT_DIR/image-versions.sh" ] && . "$SCRIPT_DIR/image-versions.sh" + # Read per-installation Bitcoin RPC credentials SECRETS_DIR="/var/lib/archipelago/secrets" sudo mkdir -p "$SECRETS_DIR" && sudo chmod 700 "$SECRETS_DIR" @@ -48,7 +52,7 @@ podman run -d \ --label "com.archipelago.icon=/assets/img/app-icons/bitcoin-knots.webp" \ --label "com.archipelago.port=8332" \ --label "com.archipelago.repo=https://github.com/bitcoinknots/bitcoin" \ - docker.io/bitcoinknots/bitcoin:latest \ + "${BITCOIN_KNOTS_IMAGE:-docker.io/bitcoinknots/bitcoin:v28.1}" \ -server=1 \ -txindex=1 \ -rpcallowip=127.0.0.1/32 -rpcallowip=10.88.0.0/16 \ @@ -89,7 +93,7 @@ EOF cp /home/archipelago/archy/docker/bitcoin-ui/index.html "$BUILD_DIR/" # Build the image -podman build -t localhost/bitcoin-ui:latest "$BUILD_DIR" +podman build -t localhost/bitcoin-ui:local "$BUILD_DIR" # Deploy UI container podman run -d \ @@ -98,7 +102,7 @@ podman run -d \ -p 8334:80 \ --label "com.archipelago.app=bitcoin-ui" \ --label "com.archipelago.parent=bitcoin-knots" \ - localhost/bitcoin-ui:latest + localhost/bitcoin-ui:local echo " ✅ Bitcoin UI deployed on port 8334" diff --git a/scripts/deploy-tailscale.sh b/scripts/deploy-tailscale.sh index 91ca99f3..30eb7d24 100755 --- a/scripts/deploy-tailscale.sh +++ b/scripts/deploy-tailscale.sh @@ -21,6 +21,9 @@ TARGET_DIR="/home/archipelago/archy" # Load deploy config (gitignored) [ -f "$SCRIPT_DIR/deploy-config.sh" ] && . "$SCRIPT_DIR/deploy-config.sh" +# Source pinned image versions (single source of truth) +[ -f "$SCRIPT_DIR/image-versions.sh" ] && . "$SCRIPT_DIR/image-versions.sh" + SSH_KEY="${ARCHIPELAGO_SSH_KEY:-$HOME/.ssh/archipelago-deploy}" SSH_OPTS="-o StrictHostKeyChecking=no -o ServerAliveInterval=15 -o ServerAliveCountMax=4 -o ConnectTimeout=10 -i $SSH_KEY" BUILD_SOURCE="archipelago@192.168.1.228" @@ -155,7 +158,7 @@ deploy_node() { HAS_IMG=$(ssh $SSH_OPTS "$BUILD_SOURCE" "podman images --format '{{.Repository}}' 2>/dev/null | grep -q '$ui_img' && echo yes || echo no" 2>/dev/null) if [ "$HAS_IMG" = "yes" ]; then echo " $ui_img..." - ssh $SSH_OPTS "$BUILD_SOURCE" "podman save 'localhost/${ui_img}:latest'" > "/tmp/${ui_img}.tar" + ssh $SSH_OPTS "$BUILD_SOURCE" "podman save 'localhost/${ui_img}:local'" > "/tmp/${ui_img}.tar" ssh $SSH_OPTS "$TARGET" "podman load" < "/tmp/${ui_img}.tar" 2>&1 | tail -1 rm -f "/tmp/${ui_img}.tar" fi @@ -471,7 +474,7 @@ deploy_node() { --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 \ + ${BITCOIN_KNOTS_IMAGE:-docker.io/bitcoinknots/bitcoin:v28.1} \ -server=1 \$BTC_EXTRA_ARGS \ -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 \ -rpcuser=$BITCOIN_RPC_USER -rpcpassword=$BITCOIN_RPC_PASS \ @@ -764,7 +767,7 @@ LNDCONF else \$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 + -p 8888:8080 ${SEARXNG_IMAGE:-docker.io/searxng/searxng:2024.11.17} fi fi # FileBrowser @@ -800,7 +803,7 @@ LNDCONF --security-opt no-new-privileges:true \ -p 2342:2342 -v /var/lib/archipelago/photoprism:/photoprism/storage \ -e PHOTOPRISM_ADMIN_PASSWORD=archipelago -e PHOTOPRISM_DEFAULT_LOCALE=en \ - docker.io/photoprism/photoprism:latest + ${PHOTOPRISM_IMAGE:-docker.io/photoprism/photoprism:240915} fi fi # OnlyOffice @@ -826,7 +829,7 @@ LNDCONF -p 81:81 -p 8084:80 -p 8443:443 \ -v /var/lib/archipelago/nginx-proxy-manager/data:/data \ -v /var/lib/archipelago/nginx-proxy-manager/letsencrypt:/etc/letsencrypt \ - docker.io/jc21/nginx-proxy-manager:latest + ${NPM_IMAGE:-docker.io/jc21/nginx-proxy-manager:2} fi fi # Portainer @@ -872,7 +875,7 @@ LNDCONF -e PENPOT_OBJECTS_STORAGE_BACKEND=fs \ -e PENPOT_OBJECTS_STORAGE_FS_DIRECTORY=/opt/data/assets \ -e PENPOT_FLAGS='disable-email-verification enable-smtp enable-prepl-server disable-secure-session-cookies' \ - docker.io/penpotapp/backend:latest + ${PENPOT_BACKEND_IMAGE:-docker.io/penpotapp/backend:2.4.2} sleep 5 fi if ! \$DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q penpot-exporter; then @@ -880,7 +883,7 @@ LNDCONF -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 \ - docker.io/penpotapp/exporter:latest + ${PENPOT_EXPORTER_IMAGE:-docker.io/penpotapp/exporter:2.4.2} sleep 2 fi if ! \$DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q penpot-frontend; then @@ -888,7 +891,7 @@ LNDCONF -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' \ - docker.io/penpotapp/frontend:latest + ${PENPOT_FRONTEND_IMAGE:-docker.io/penpotapp/frontend:2.4.2} fi fi @@ -906,9 +909,9 @@ LNDCONF esac if [ -d \"$TARGET_DIR/docker/\$ui\" ]; then echo \" Building \$ui...\" - if \$DOCKER build --no-cache -t \"\$ui:latest\" \"$TARGET_DIR/docker/\$ui\" 2>/dev/null; then + if \$DOCKER build --no-cache -t \"\$ui:local\" \"$TARGET_DIR/docker/\$ui\" 2>/dev/null; then \$DOCKER stop \"\$CONTAINER_NAME\" 2>/dev/null; \$DOCKER rm -f \"\$CONTAINER_NAME\" 2>/dev/null - \$DOCKER run -d --name \"\$CONTAINER_NAME\" \$PORT_ARG --restart unless-stopped \$NET_ARG \"\$ui:latest\" + \$DOCKER run -d --name \"\$CONTAINER_NAME\" \$PORT_ARG --restart unless-stopped \$NET_ARG \"\$ui:local\" echo \" \$ui created\" fi elif \$DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q \"\$ui\"; then diff --git a/scripts/deploy-to-target.sh b/scripts/deploy-to-target.sh index 71a8ea5e..e3c03f56 100755 --- a/scripts/deploy-to-target.sh +++ b/scripts/deploy-to-target.sh @@ -22,6 +22,9 @@ 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" +# Source pinned image versions (single source of truth) +[ -f "$SCRIPT_DIR/image-versions.sh" ] && . "$SCRIPT_DIR/image-versions.sh" + # Configuration TARGET_HOST="${ARCHIPELAGO_TARGET:-archipelago@192.168.1.228}" TARGET_DIR="/home/archipelago/archy" @@ -839,7 +842,7 @@ MANIFEST_EOF else # Rebuild and recreate LND UI container (port 8081 so Launch from UI and http://host:8081 both work) progress "Rebuilding LND UI" - if ssh $SSH_OPTS "$TARGET_HOST" "cd $TARGET_DIR/docker/lnd-ui && (command -v podman >/dev/null 2>&1 && podman build --no-cache -t lnd-ui:latest . || docker build --no-cache -t lnd-ui:latest .)" 2>&1 | tail -12 | sed 's/^/ /'; then + if ssh $SSH_OPTS "$TARGET_HOST" "cd $TARGET_DIR/docker/lnd-ui && (command -v podman >/dev/null 2>&1 && podman build --no-cache -t lnd-ui:local . || docker build --no-cache -t lnd-ui:local .)" 2>&1 | tail -12 | sed 's/^/ /'; then echo " Recreating LND UI container (port 8081)..." ssh $SSH_OPTS "$TARGET_HOST" ' DOCKER=podman @@ -847,13 +850,13 @@ MANIFEST_EOF for c in $($DOCKER ps -a --format "{{.Names}}" 2>/dev/null | grep -i lnd-ui); do [ -n "$c" ] && $DOCKER stop "$c" 2>/dev/null; $DOCKER rm -f "$c" 2>/dev/null done - $DOCKER run -d --name archy-lnd-ui -p 8081:80 --restart unless-stopped lnd-ui:latest + $DOCKER run -d --name archy-lnd-ui -p 8081:80 --restart unless-stopped lnd-ui:local ' 2>&1 | sed 's/^/ /' || true fi # Rebuild and recreate ElectrumX UI container (port 50002) progress "Rebuilding ElectrumX UI" - if ssh $SSH_OPTS "$TARGET_HOST" "cd $TARGET_DIR/docker/electrs-ui && (command -v podman >/dev/null 2>&1 && podman build --no-cache -t electrs-ui:latest . || docker build --no-cache -t electrs-ui:latest .)" 2>&1 | tail -12 | sed 's/^/ /'; then + if ssh $SSH_OPTS "$TARGET_HOST" "cd $TARGET_DIR/docker/electrs-ui && (command -v podman >/dev/null 2>&1 && podman build --no-cache -t electrs-ui:local . || docker build --no-cache -t electrs-ui:local .)" 2>&1 | tail -12 | sed 's/^/ /'; then echo " Recreating ElectrumX UI container (port 50002, host network)..." ssh $SSH_OPTS "$TARGET_HOST" ' DOCKER=podman @@ -861,7 +864,7 @@ MANIFEST_EOF for c in $($DOCKER ps -a --format "{{.Names}}" 2>/dev/null | grep -i electrs-ui); do [ -n "$c" ] && $DOCKER stop "$c" 2>/dev/null; $DOCKER rm -f "$c" 2>/dev/null done - $DOCKER run -d --name archy-electrs-ui --network host --restart unless-stopped electrs-ui:latest + $DOCKER run -d --name archy-electrs-ui --network host --restart unless-stopped electrs-ui:local ' 2>&1 | sed 's/^/ /' || true fi @@ -877,7 +880,7 @@ MANIFEST_EOF sed -i "s|__BITCOIN_RPC_AUTH__|${AUTH_B64}|g" '"$TARGET_DIR"'/docker/bitcoin-ui/nginx.conf fi ' 2>/dev/null || true - if ssh $SSH_OPTS "$TARGET_HOST" "cd $TARGET_DIR/docker/bitcoin-ui && (command -v podman >/dev/null 2>&1 && podman build --no-cache -t bitcoin-ui:latest . || docker build --no-cache -t bitcoin-ui:latest .)" 2>&1 | tail -12 | sed 's/^/ /'; then + if ssh $SSH_OPTS "$TARGET_HOST" "cd $TARGET_DIR/docker/bitcoin-ui && (command -v podman >/dev/null 2>&1 && podman build --no-cache -t bitcoin-ui:local . || docker build --no-cache -t bitcoin-ui:local .)" 2>&1 | tail -12 | sed 's/^/ /'; then echo " Recreating Bitcoin UI container (port 8334, host network)..." ssh $SSH_OPTS "$TARGET_HOST" ' DOCKER=podman @@ -885,7 +888,7 @@ MANIFEST_EOF for c in $($DOCKER ps -a --format "{{.Names}}" 2>/dev/null | grep -i bitcoin-ui); do [ -n "$c" ] && $DOCKER stop "$c" 2>/dev/null; $DOCKER rm -f "$c" 2>/dev/null done - $DOCKER run -d --name archy-bitcoin-ui --network host --restart unless-stopped bitcoin-ui:latest + $DOCKER run -d --name archy-bitcoin-ui --network host --restart unless-stopped bitcoin-ui:local ' 2>&1 | sed 's/^/ /' || true fi @@ -965,7 +968,7 @@ MANIFEST_EOF --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 \ + ${BITCOIN_KNOTS_IMAGE:-docker.io/bitcoinknots/bitcoin:v28.1} \ -server=1 \$BTC_EXTRA_ARGS \ -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 \ -dbcache=\$BTC_DBCACHE @@ -1563,7 +1566,7 @@ LNDCONF $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 + ${SEARXNG_IMAGE:-docker.io/searxng/searxng:2024.11.17} fi else echo " SearXNG already running" diff --git a/scripts/first-boot-containers.sh b/scripts/first-boot-containers.sh index a2e6164c..416687fe 100644 --- a/scripts/first-boot-containers.sh +++ b/scripts/first-boot-containers.sh @@ -11,6 +11,9 @@ LOG="/var/log/archipelago-first-boot.log" DOCKER=podman command -v podman >/dev/null 2>&1 || DOCKER=docker +# Source pinned image versions (single source of truth) +source /opt/archipelago/image-versions.sh 2>/dev/null || true + # Must run as root for podman [ "$(id -u)" -eq 0 ] || { echo "Must run as root" >&2; exit 1; } @@ -252,7 +255,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE 'bitcoin-knots|arch --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 \ + "${BITCOIN_KNOTS_IMAGE:-docker.io/bitcoinknots/bitcoin:v28.1}" \ -server=1 $BTC_EXTRA_ARGS \ -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 \ -proxy=host.containers.internal:9050 -listen=1 -bind=0.0.0.0:8333 \ @@ -339,16 +342,16 @@ if ! $DOCKER ps --format '{{.Names}}' 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" || \ + localhost/electrs-ui:local 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 + electrs-ui:local 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 build -t electrs-ui:local /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 + electrs-ui:local 2>>"$LOG" || true else log "ElectrumX UI: no image or source found, skipping" fi @@ -583,7 +586,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q photoprism; then --security-opt no-new-privileges:true \ -p 2342:2342 -v /var/lib/archipelago/photoprism:/photoprism/storage \ -e PHOTOPRISM_ADMIN_PASSWORD=archipelago -e PHOTOPRISM_DEFAULT_LOCALE=en \ - docker.io/photoprism/photoprism:latest 2>>"$LOG" || true + "${PHOTOPRISM_IMAGE:-docker.io/photoprism/photoprism:240915}" 2>>"$LOG" || true fi if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q ollama; then log "Creating Ollama..." @@ -594,7 +597,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q ollama; then --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 \ - docker.io/ollama/ollama:latest 2>>"$LOG" || true + "${OLLAMA_IMAGE:-docker.io/ollama/ollama:0.5.4}" 2>>"$LOG" || true fi if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q vaultwarden; then log "Creating Vaultwarden..." @@ -626,7 +629,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q searxng; then --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 \ - docker.io/searxng/searxng:latest 2>>"$LOG" || true + "${SEARXNG_IMAGE:-docker.io/searxng/searxng:2024.11.17}" 2>>"$LOG" || true fi if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q onlyoffice; then log "Creating OnlyOffice..." @@ -658,7 +661,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q nginx-proxy-manager; -p 81:81 -p 8084:80 -p 8443:443 \ -v /var/lib/archipelago/nginx-proxy-manager/data:/data \ -v /var/lib/archipelago/nginx-proxy-manager/letsencrypt:/etc/letsencrypt \ - docker.io/jc21/nginx-proxy-manager:latest 2>>"$LOG" || true + "${NPM_IMAGE:-docker.io/jc21/nginx-proxy-manager:2}" 2>>"$LOG" || true fi if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q portainer; then log "Creating Portainer..." @@ -772,7 +775,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q penpot-frontend; the -e PENPOT_OBJECTS_STORAGE_BACKEND=fs \ -e PENPOT_OBJECTS_STORAGE_FS_DIRECTORY=/opt/data/assets \ -e PENPOT_FLAGS=disable-email-verification enable-smtp enable-prepl-server disable-secure-session-cookies \ - docker.io/penpotapp/backend:latest 2>>"$LOG" || true + "${PENPOT_BACKEND_IMAGE:-docker.io/penpotapp/backend:2.4.2}" 2>>"$LOG" || true sleep 5 fi if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q penpot-exporter; then @@ -782,7 +785,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q penpot-frontend; the -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 \ - docker.io/penpotapp/exporter:latest 2>>"$LOG" || true + "${PENPOT_EXPORTER_IMAGE:-docker.io/penpotapp/exporter:2.4.2}" 2>>"$LOG" || true sleep 2 fi if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q penpot-frontend; then @@ -792,7 +795,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q penpot-frontend; the -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 \ - docker.io/penpotapp/frontend:latest 2>>"$LOG" || true + "${PENPOT_FRONTEND_IMAGE:-docker.io/penpotapp/frontend:2.4.2}" 2>>"$LOG" || true fi fi @@ -806,7 +809,7 @@ if $DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q 'nos --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 + "${NOSTR_RS_RELAY_IMAGE:-docker.io/scsibug/nostr-rs-relay:0.9.0}" 2>>"$LOG" || true fi fi if $DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q 'strfry'; then @@ -817,7 +820,7 @@ if $DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q 'str --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 + "${STRFRY_IMAGE:-docker.io/pluja/strfry:latest}" 2>>"$LOG" || true fi fi @@ -826,10 +829,10 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q indeedhub; then INDEEDHUB_IMAGE="" # Try local image first (pre-built or loaded from ISO) if $DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q 'localhost/indeedhub'; then - INDEEDHUB_IMAGE="localhost/indeedhub:latest" + INDEEDHUB_IMAGE="localhost/indeedhub:local" # Try registry image - elif $DOCKER pull git.tx1138.com/lfg2025/indeedhub:latest 2>>"$LOG"; then - INDEEDHUB_IMAGE="git.tx1138.com/lfg2025/indeedhub:latest" + elif $DOCKER pull git.tx1138.com/lfg2025/indeedhub:local 2>>"$LOG"; then + INDEEDHUB_IMAGE="git.tx1138.com/lfg2025/indeedhub:local" fi if [ -n "$INDEEDHUB_IMAGE" ]; then log "Creating Indeehub from $INDEEDHUB_IMAGE..." @@ -871,13 +874,13 @@ for ui in bitcoin-ui lnd-ui; do $DOCKER run -d --name "$CONTAINER_NAME" $PORT_ARG --restart unless-stopped --memory=$(mem_limit "$CONTAINER_NAME") $NET_ARG "$IMG" 2>>"$LOG" || true elif [ -d "/opt/archipelago/docker/$ui" ]; then log "Building $ui from source (/opt/archipelago/docker/$ui)..." - if $DOCKER build -t "$ui:latest" "/opt/archipelago/docker/$ui" 2>>"$LOG"; then - $DOCKER run -d --name "$CONTAINER_NAME" $PORT_ARG --restart unless-stopped --memory=$(mem_limit "$CONTAINER_NAME") $NET_ARG "$ui:latest" 2>>"$LOG" || true + if $DOCKER build -t "$ui:local" "/opt/archipelago/docker/$ui" 2>>"$LOG"; then + $DOCKER run -d --name "$CONTAINER_NAME" $PORT_ARG --restart unless-stopped --memory=$(mem_limit "$CONTAINER_NAME") $NET_ARG "$ui:local" 2>>"$LOG" || true fi elif [ -d "/home/archipelago/archy/docker/$ui" ]; then log "Building $ui from source (/home/archipelago/archy/docker/$ui)..." - if $DOCKER build -t "$ui:latest" "/home/archipelago/archy/docker/$ui" 2>>"$LOG"; then - $DOCKER run -d --name "$CONTAINER_NAME" $PORT_ARG --restart unless-stopped --memory=$(mem_limit "$CONTAINER_NAME") $NET_ARG "$ui:latest" 2>>"$LOG" || true + if $DOCKER build -t "$ui:local" "/home/archipelago/archy/docker/$ui" 2>>"$LOG"; then + $DOCKER run -d --name "$CONTAINER_NAME" $PORT_ARG --restart unless-stopped --memory=$(mem_limit "$CONTAINER_NAME") $NET_ARG "$ui:local" 2>>"$LOG" || true fi else log "$ui: no image or source found, skipping" diff --git a/scripts/fix-indeedhub-containers.sh b/scripts/fix-indeedhub-containers.sh index 6db8702c..44d55656 100755 --- a/scripts/fix-indeedhub-containers.sh +++ b/scripts/fix-indeedhub-containers.sh @@ -1,6 +1,10 @@ #!/bin/bash set -e +# Source pinned image versions (single source of truth) +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +[ -f "$SCRIPT_DIR/image-versions.sh" ] && . "$SCRIPT_DIR/image-versions.sh" + # Fix corrupted IndeedHub containers + SearXNG # All images were exported as the same (wrong) image during multi-node deploy. # This script: stops broken containers, removes them, recreates with correct images. @@ -24,7 +28,7 @@ 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 +for img in "${INDEEDHUB_REDIS_IMAGE:-docker.io/library/redis:7-alpine}" "${MINIO_IMAGE:-docker.io/minio/minio:RELEASE.2024-11-07T00-52-20Z}" "${INDEEDHUB_POSTGRES_IMAGE:-docker.io/library/postgres:16-alpine}" "${NOSTR_RS_RELAY_IMAGE:-docker.io/scsibug/nostr-rs-relay:0.9.0}" "${SEARXNG_IMAGE:-docker.io/searxng/searxng:2024.11.17}" "localhost/indeedhub:local" "localhost/indeedhub-build_api:local" "localhost/indeedhub-build_ffmpeg-worker:local"; do if ! podman image exists "$img" 2>/dev/null; then echo "ERROR: Missing image $img" exit 1 @@ -88,7 +92,7 @@ podman run -d --name indeedhub-minio \ -v indeedhub-minio-data:/data \ -e MINIO_ROOT_USER=indeeadmin \ -e MINIO_ROOT_PASSWORD=indeeadmin2026 \ - docker.io/minio/minio:latest \ + "${MINIO_IMAGE:-docker.io/minio/minio:RELEASE.2024-11-07T00-52-20Z}" \ server /data --console-address ":9001" # 4. Nostr Relay @@ -97,7 +101,7 @@ podman run -d --name indeedhub-relay \ --restart unless-stopped \ --network "$NETWORK" --network-alias relay \ -v indeedhub-relay-data:/usr/src/app/db \ - docker.io/scsibug/nostr-rs-relay:latest + "${NOSTR_RS_RELAY_IMAGE:-docker.io/scsibug/nostr-rs-relay:0.9.0}" # 5. API echo "Creating api..." @@ -137,7 +141,7 @@ podman run -d --name indeedhub-build_api_1 \ --health-timeout 30s \ --health-retries 5 \ --health-start-period 60s \ - localhost/indeedhub-build_api:latest \ + localhost/indeedhub-build_api:local \ sh -c "echo 'Running database migrations...' && npx typeorm migration:run -d dist/database/ormconfig.js && echo 'Migrations complete.' && npm run start:prod" # 6. FFmpeg Worker @@ -162,7 +166,7 @@ podman run -d --name indeedhub-build_ffmpeg-worker_1 \ -e S3_PUBLIC_BUCKET_NAME=indeedhub-public \ -e S3_PUBLIC_BUCKET_URL=/storage \ -e AES_MASTER_SECRET=0123456789abcdef0123456789abcdef \ - localhost/indeedhub-build_ffmpeg-worker:latest + localhost/indeedhub-build_ffmpeg-worker:local # 7. IndeedHub Frontend echo "Creating indeedhub frontend..." @@ -175,7 +179,7 @@ podman run -d --name indeedhub \ --label "com.archipelago.version=0.1.0" \ --label "com.archipelago.category=media" \ --label "com.archipelago.port=7777" \ - localhost/indeedhub:latest + localhost/indeedhub:local # Fix IndeedHub for iframe: remove X-Frame-Options, inject nostr-provider, hardcode container IPs sleep 3 @@ -223,7 +227,7 @@ echo "Creating searxng..." podman run -d --name searxng \ --restart unless-stopped \ -p 8888:8080 \ - docker.io/searxng/searxng:latest + "${SEARXNG_IMAGE:-docker.io/searxng/searxng:2024.11.17}" echo "" echo "=== Verifying container status ===" diff --git a/scripts/image-versions.sh b/scripts/image-versions.sh new file mode 100644 index 00000000..090f3af5 --- /dev/null +++ b/scripts/image-versions.sh @@ -0,0 +1,67 @@ +#!/bin/bash +# Container image versions — single source of truth +# Source this file from all scripts that create containers +# +# Usage: source /opt/archipelago/image-versions.sh 2>/dev/null || true +# source "$(dirname "$0")/image-versions.sh" 2>/dev/null || true + +# Bitcoin stack +BITCOIN_KNOTS_IMAGE="docker.io/bitcoinknots/bitcoin:v28.1" +LND_IMAGE="docker.io/lightninglabs/lnd:v0.18.5-beta" +ELECTRUMX_IMAGE="docker.io/lukechilds/electrumx:v1.16.0" + +# Mempool stack +MEMPOOL_API_IMAGE="docker.io/mempool/frontend:v3.0.0" +MEMPOOL_WEB_IMAGE="docker.io/mempool/frontend:v3.0.0" +MARIADB_IMAGE="docker.io/library/mariadb:11.4" + +# BTCPay +BTCPAY_IMAGE="docker.io/btcpayserver/btcpayserver:1.14.5" +NBXPLORER_IMAGE="docker.io/nicolasdorier/nbxplorer:2.5.13" +POSTGRES_IMAGE="docker.io/library/postgres:16" + +# Apps +HOMEASSISTANT_IMAGE="ghcr.io/home-assistant/home-assistant:2024.12" +GRAFANA_IMAGE="docker.io/grafana/grafana:11.4.0" +UPTIME_KUMA_IMAGE="docker.io/louislam/uptime-kuma:1" +JELLYFIN_IMAGE="docker.io/jellyfin/jellyfin:10.10.3" +PHOTOPRISM_IMAGE="docker.io/photoprism/photoprism:240915" +OLLAMA_IMAGE="docker.io/ollama/ollama:0.5.4" +VAULTWARDEN_IMAGE="docker.io/vaultwarden/server:1.32.5" +NEXTCLOUD_IMAGE="docker.io/library/nextcloud:30" +SEARXNG_IMAGE="docker.io/searxng/searxng:2024.11.17" +ONLYOFFICE_IMAGE="docker.io/onlyoffice/documentserver:8.2" +FILEBROWSER_IMAGE="docker.io/filebrowser/filebrowser:v2" +NPM_IMAGE="docker.io/jc21/nginx-proxy-manager:2" +PORTAINER_IMAGE="docker.io/portainer/portainer-ce:2.21.5" + +# Networking +TAILSCALE_IMAGE="docker.io/tailscale/tailscale:v1.78.3" +ALPINE_TOR_IMAGE="docker.io/andrius/alpine-tor:0.4.8.13" +ADGUARDHOME_IMAGE="docker.io/adguard/adguardhome:v0.107.55" + +# Fedimint +FEDIMINT_IMAGE="docker.io/fedimint/fedimintd:v0.5.1" +FEDIMINT_GATEWAY_IMAGE="docker.io/fedimint/gatewayd:v0.5.1" + +# Media +IMMICH_IMAGE="ghcr.io/immich-app/immich-server:v1.123.0" +REDIS_IMAGE="docker.io/library/redis:7" + +# Penpot +PENPOT_BACKEND_IMAGE="docker.io/penpotapp/backend:2.4.2" +PENPOT_FRONTEND_IMAGE="docker.io/penpotapp/frontend:2.4.2" +PENPOT_EXPORTER_IMAGE="docker.io/penpotapp/exporter:2.4.2" +VALKEY_IMAGE="docker.io/valkey/valkey:8" + +# Nostr +NOSTR_RS_RELAY_IMAGE="docker.io/scsibug/nostr-rs-relay:0.9.0" +STRFRY_IMAGE="docker.io/pluja/strfry:latest" # No stable tag available yet + +# IndeedHub stack (local builds use :local tag, not :latest) +MINIO_IMAGE="docker.io/minio/minio:RELEASE.2024-11-07T00-52-20Z" +INDEEDHUB_POSTGRES_IMAGE="docker.io/library/postgres:16-alpine" +INDEEDHUB_REDIS_IMAGE="docker.io/library/redis:7-alpine" + +# Base images +NGINX_ALPINE_IMAGE="docker.io/library/nginx:alpine"