diff --git a/neode-ui/src/views/Dashboard.vue b/neode-ui/src/views/Dashboard.vue index a0e39f6f..16b0716e 100644 --- a/neode-ui/src/views/Dashboard.vue +++ b/neode-ui/src/views/Dashboard.vue @@ -75,9 +75,6 @@ - - -
@@ -118,6 +115,9 @@ + + +
diff --git a/scripts/container-specs.sh b/scripts/container-specs.sh new file mode 100755 index 00000000..e20ea679 --- /dev/null +++ b/scripts/container-specs.sh @@ -0,0 +1,579 @@ +#!/bin/bash +# Container specification registry — SINGLE SOURCE OF TRUTH +# Every container's exact creation spec lives here. +# Sourced by reconcile-containers.sh, first-boot-containers.sh, deploy scripts. +# +# Usage: +# source container-specs.sh +# load_spec "bitcoin-knots" # Sets SPEC_* variables +# all_specs # Returns ordered list of all containers + +[ -n "${_CONTAINER_SPECS_LOADED:-}" ] && return 0 +_CONTAINER_SPECS_LOADED=1 + +# Source image versions +for f in /opt/archipelago/image-versions.sh \ + "$(dirname "${BASH_SOURCE[0]}")/image-versions.sh" \ + "$(dirname "${BASH_SOURCE[0]}")/../image-versions.sh"; do + [ -f "$f" ] && { source "$f"; break; } +done + +# Source common utilities (mem_limit) +for f in "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh" \ + /opt/archipelago/scripts/lib/common.sh; do + [ -f "$f" ] && { source "$f"; break; } +done + +# ── Environment detection ───────────────────────────────────────────── +detect_environment() { + DISK_GB=$(df --output=size -BG / 2>/dev/null | tail -1 | tr -dc '0-9') + DISK_GB=${DISK_GB:-500} + TOTAL_MEM_MB=$(($(awk '/MemTotal/{print $2}' /proc/meminfo 2>/dev/null || echo 16000000) / 1024)) + LOW_MEM=false + [ "$TOTAL_MEM_MB" -lt 12000 ] && LOW_MEM=true + HOST_IP=$(hostname -I 2>/dev/null | awk '{print $1}') + HOST_IP=${HOST_IP:-127.0.0.1} + + # Secrets + SECRETS_DIR="/var/lib/archipelago/secrets" + BITCOIN_RPC_USER="archipelago" + BITCOIN_RPC_PASS=$(cat "$SECRETS_DIR/bitcoin-rpc-password" 2>/dev/null || echo "") + MEMPOOL_DB_PASS=$(cat "$SECRETS_DIR/mempool-db-password" 2>/dev/null || echo "") + BTCPAY_DB_PASS=$(cat "$SECRETS_DIR/btcpay-db-password" 2>/dev/null || echo "") + MYSQL_ROOT_PASS=$(cat "$SECRETS_DIR/mysql-root-db-password" 2>/dev/null || echo "") + FEDI_HASH=$(cat "$SECRETS_DIR/fedimint-gateway-hash" 2>/dev/null || echo "") +} + +# ── Spec variables ──────────────────────────────────────────────────── +# Each load_spec_* function sets these variables: +# SPEC_NAME Container name +# SPEC_IMAGE Full image reference (pinned) +# SPEC_NETWORK Network mode (archy-net, bridge, host) +# SPEC_PORTS Space-separated host:container port pairs +# SPEC_VOLUMES Space-separated host:container volume mappings +# SPEC_MEMORY Memory limit (e.g. 2g, 512m) +# SPEC_CAPS Space-separated capabilities to add +# SPEC_SECURITY Security options +# SPEC_RESTART Restart policy +# SPEC_HEALTH_CMD Health check command +# SPEC_ENV Space-separated KEY=VALUE environment variables +# SPEC_CUSTOM_ARGS Extra args appended to podman run +# SPEC_READONLY true/false for --read-only +# SPEC_TMPFS Space-separated tmpfs mounts +# SPEC_TIER 0=DB, 1=Core, 2=Service, 3=App, 4=UI +# SPEC_DATA_DIR Host data directory (for ownership fix) +# SPEC_DATA_UID Host UID:GID for data dir (rootless mapped) +# SPEC_DEPENDS Space-separated container dependencies +# SPEC_LOCAL_IMAGE true if image is built locally (don't pull) +# SPEC_OPTIONAL true if container should be skipped when image missing + +reset_spec() { + SPEC_NAME="" SPEC_IMAGE="" SPEC_NETWORK="bridge" SPEC_PORTS="" + SPEC_VOLUMES="" SPEC_MEMORY="512m" SPEC_CAPS="CHOWN FOWNER SETUID SETGID DAC_OVERRIDE" + SPEC_SECURITY="no-new-privileges:true" SPEC_RESTART="unless-stopped" + SPEC_HEALTH_CMD="" SPEC_ENV="" SPEC_CUSTOM_ARGS="" SPEC_READONLY="false" + SPEC_TMPFS="" SPEC_TIER="3" SPEC_DATA_DIR="" SPEC_DATA_UID="100000:100000" + SPEC_DEPENDS="" SPEC_LOCAL_IMAGE="false" SPEC_OPTIONAL="false" + SPEC_ENTRYPOINT="" +} + +# ── Tier 0: Databases ──────────────────────────────────────────────── + +load_spec_archy-mempool-db() { + reset_spec + SPEC_NAME="archy-mempool-db" + SPEC_IMAGE="${MARIADB_IMAGE:-docker.io/library/mariadb:11.4}" + SPEC_NETWORK="archy-net" + SPEC_MEMORY="$(mem_limit archy-mempool-db)" + SPEC_VOLUMES="/var/lib/archipelago/mysql-mempool:/var/lib/mysql" + SPEC_HEALTH_CMD="mariadb -uroot -e 'SELECT 1' || exit 1" + SPEC_ENV="MYSQL_DATABASE=mempool MYSQL_USER=mempool MYSQL_PASSWORD=$MEMPOOL_DB_PASS MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASS" + SPEC_TIER="0" + SPEC_DATA_DIR="/var/lib/archipelago/mysql-mempool" + SPEC_DATA_UID="100999:100999" + SPEC_CAPS="CHOWN FOWNER SETUID SETGID DAC_OVERRIDE" +} + +load_spec_archy-btcpay-db() { + reset_spec + SPEC_NAME="archy-btcpay-db" + SPEC_IMAGE="${BTCPAY_POSTGRES_IMAGE:-docker.io/library/postgres:15}" + SPEC_NETWORK="archy-net" + SPEC_MEMORY="$(mem_limit archy-btcpay-db)" + SPEC_VOLUMES="/var/lib/archipelago/postgres-btcpay:/var/lib/postgresql/data" + SPEC_HEALTH_CMD="pg_isready -U postgres || exit 1" + SPEC_ENV="POSTGRES_DB=btcpay POSTGRES_USER=btcpay POSTGRES_PASSWORD=$BTCPAY_DB_PASS" + SPEC_TIER="0" + SPEC_DATA_DIR="/var/lib/archipelago/postgres-btcpay" + SPEC_DATA_UID="100070:100070" + SPEC_CAPS="CHOWN FOWNER SETUID SETGID DAC_OVERRIDE" +} + +load_spec_immich_postgres() { + reset_spec + SPEC_NAME="immich_postgres" + SPEC_IMAGE="ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0" + SPEC_NETWORK="bridge" + SPEC_MEMORY="$(mem_limit immich_postgres)" + SPEC_VOLUMES="/var/lib/archipelago/immich-db:/var/lib/postgresql/data" + SPEC_ENV="POSTGRES_USER=postgres POSTGRES_DB=immich POSTGRES_PASSWORD=$BTCPAY_DB_PASS" + SPEC_TIER="0" + SPEC_DATA_DIR="/var/lib/archipelago/immich-db" + SPEC_DATA_UID="100070:100070" + SPEC_CAPS="CHOWN FOWNER SETUID SETGID DAC_OVERRIDE" + SPEC_OPTIONAL="true" +} + +load_spec_immich_redis() { + reset_spec + SPEC_NAME="immich_redis" + SPEC_IMAGE="${VALKEY_IMAGE:-docker.io/valkey/valkey:8}" + SPEC_NETWORK="bridge" + SPEC_MEMORY="$(mem_limit immich_redis)" + SPEC_TIER="0" + SPEC_CAPS="CHOWN SETUID SETGID" + SPEC_OPTIONAL="true" +} + +# ── Tier 1: Core Infrastructure ────────────────────────────────────── + +load_spec_bitcoin-knots() { + reset_spec + SPEC_NAME="bitcoin-knots" + SPEC_IMAGE="${BITCOIN_KNOTS_IMAGE:-docker.io/bitcoinknots/bitcoin:28.1}" + SPEC_NETWORK="archy-net" + SPEC_PORTS="8332:8332 8333:8333" + SPEC_VOLUMES="/var/lib/archipelago/bitcoin:/home/bitcoin/.bitcoin" + SPEC_MEMORY="$(mem_limit bitcoin-knots)" + SPEC_HEALTH_CMD="bitcoin-cli -rpcuser=\$BITCOIN_RPC_USER -rpcpassword=\$BITCOIN_RPC_PASS getblockchaininfo || exit 1" + SPEC_TIER="1" + SPEC_DATA_DIR="/var/lib/archipelago/bitcoin" + SPEC_DATA_UID="100101:100101" + # Dynamic: prune on small disk + if [ "${DISK_GB:-0}" -lt 1000 ]; then + SPEC_CUSTOM_ARGS="-server=1 -prune=550 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=512" + else + SPEC_CUSTOM_ARGS="-server=1 -txindex=1 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=4096" + fi +} + +load_spec_electrumx() { + reset_spec + SPEC_NAME="electrumx" + SPEC_IMAGE="docker.io/lukechilds/electrumx:v1.16.0" + SPEC_NETWORK="archy-net" + SPEC_PORTS="50001:50001" + SPEC_VOLUMES="/var/lib/archipelago/electrumx:/data" + SPEC_MEMORY="$(mem_limit electrumx)" + SPEC_HEALTH_CMD="curl -sf http://localhost:8000/ || exit 1" + SPEC_ENV="DAEMON_URL=http://$BITCOIN_RPC_USER:$BITCOIN_RPC_PASS@bitcoin-knots:8332/ COIN=Bitcoin DB_DIRECTORY=/data SERVICES=tcp://:50001,rpc://0.0.0.0:8000" + SPEC_TIER="1" + SPEC_DATA_DIR="/var/lib/archipelago/electrumx" + SPEC_DEPENDS="bitcoin-knots" + SPEC_CAPS="" +} + +# ── Tier 2: Services ───────────────────────────────────────────────── + +load_spec_lnd() { + reset_spec + SPEC_NAME="lnd" + SPEC_IMAGE="${LND_IMAGE:-docker.io/lightninglabs/lnd:v0.18.5-beta}" + SPEC_NETWORK="archy-net" + SPEC_PORTS="9735:9735 10009:10009 8080:8080" + SPEC_VOLUMES="/var/lib/archipelago/lnd:/root/.lnd" + SPEC_MEMORY="$(mem_limit lnd)" + SPEC_HEALTH_CMD="curl -sf --insecure https://localhost:8080/v1/getinfo || exit 1" + SPEC_TIER="2" + SPEC_DATA_DIR="/var/lib/archipelago/lnd" + SPEC_DEPENDS="bitcoin-knots" +} + +load_spec_mempool-api() { + reset_spec + SPEC_NAME="mempool-api" + SPEC_IMAGE="${MEMPOOL_BACKEND_IMAGE:-docker.io/mempool/backend:v3.0.0}" + SPEC_NETWORK="archy-net" + SPEC_PORTS="8999:8999" + SPEC_VOLUMES="/var/lib/archipelago/mempool:/data" + SPEC_MEMORY="$(mem_limit mempool-api)" + SPEC_HEALTH_CMD="curl -sf http://localhost:8999/ || exit 1" + local MYSQL_CNT="archy-mempool-db" + SPEC_ENV="MEMPOOL_BACKEND=electrum ELECTRUM_HOST=electrumx ELECTRUM_PORT=50001 ELECTRUM_TLS_ENABLED=false CORE_RPC_HOST=bitcoin-knots CORE_RPC_PORT=8332 CORE_RPC_USERNAME=$BITCOIN_RPC_USER CORE_RPC_PASSWORD=$BITCOIN_RPC_PASS DATABASE_ENABLED=true DATABASE_HOST=$MYSQL_CNT DATABASE_DATABASE=mempool DATABASE_USERNAME=mempool DATABASE_PASSWORD=$MEMPOOL_DB_PASS" + SPEC_TIER="2" + SPEC_DATA_DIR="/var/lib/archipelago/mempool" + SPEC_DEPENDS="bitcoin-knots electrumx archy-mempool-db" + SPEC_CAPS="" +} + +load_spec_archy-mempool-web() { + reset_spec + SPEC_NAME="archy-mempool-web" + SPEC_IMAGE="${MEMPOOL_WEB_IMAGE:-docker.io/mempool/frontend:v3.0.0}" + SPEC_NETWORK="archy-net" + SPEC_PORTS="4080:8080" + SPEC_MEMORY="$(mem_limit archy-mempool-web)" + SPEC_HEALTH_CMD="curl -sf http://localhost:8080/ || exit 1" + SPEC_ENV="FRONTEND_HTTP_PORT=8080 BACKEND_MAINNET_HTTP_HOST=mempool-api" + SPEC_TIER="2" + SPEC_DEPENDS="mempool-api" + SPEC_CAPS="" +} + +load_spec_archy-nbxplorer() { + reset_spec + SPEC_NAME="archy-nbxplorer" + SPEC_IMAGE="${NBXPLORER_IMAGE:-docker.io/nicolasdorier/nbxplorer:2.5.13}" + SPEC_NETWORK="archy-net" + SPEC_PORTS="32838:32838" + SPEC_VOLUMES="/var/lib/archipelago/nbxplorer:/data" + SPEC_MEMORY="$(mem_limit archy-nbxplorer)" + SPEC_HEALTH_CMD="curl -sf http://localhost:32838/ || exit 1" + SPEC_ENV="NBXPLORER_DATADIR=/data NBXPLORER_NETWORK=mainnet NBXPLORER_CHAINS=btc NBXPLORER_BIND=0.0.0.0:32838 NBXPLORER_BTCRPCURL=http://bitcoin-knots:8332 NBXPLORER_BTCRPCUSER=$BITCOIN_RPC_USER NBXPLORER_BTCRPCPASSWORD=$BITCOIN_RPC_PASS NBXPLORER_POSTGRES=User ID=btcpay;Password=$BTCPAY_DB_PASS;Host=archy-btcpay-db;Port=5432;Database=nbxplorer;Include Error Detail=true" + SPEC_TIER="2" + SPEC_DATA_DIR="/var/lib/archipelago/nbxplorer" + SPEC_DEPENDS="bitcoin-knots archy-btcpay-db" + SPEC_CAPS="" +} + +load_spec_btcpay-server() { + reset_spec + SPEC_NAME="btcpay-server" + SPEC_IMAGE="${BTCPAY_IMAGE:-docker.io/btcpayserver/btcpayserver:1.13.7}" + SPEC_NETWORK="archy-net" + SPEC_PORTS="23000:49392" + SPEC_VOLUMES="/var/lib/archipelago/btcpay:/datadir" + SPEC_MEMORY="$(mem_limit btcpay-server)" + SPEC_HEALTH_CMD="curl -sf http://localhost:49392/ || exit 1" + SPEC_ENV="ASPNETCORE_URLS=http://0.0.0.0:49392 BTCPAY_PROTOCOL=http BTCPAY_HOST=$HOST_IP:23000 BTCPAY_CHAINS=btc BTCPAY_BTCEXPLORERURL=http://archy-nbxplorer:32838 BTCPAY_BTCRPCURL=http://bitcoin-knots:8332 BTCPAY_BTCRPCUSER=$BITCOIN_RPC_USER BTCPAY_BTCRPCPASSWORD=$BITCOIN_RPC_PASS BTCPAY_POSTGRES=User ID=btcpay;Password=$BTCPAY_DB_PASS;Host=archy-btcpay-db;Port=5432;Database=btcpay;Include Error Detail=true" + SPEC_TIER="2" + SPEC_DATA_DIR="/var/lib/archipelago/btcpay" + SPEC_DEPENDS="archy-nbxplorer archy-btcpay-db" +} + +load_spec_fedimint() { + reset_spec + SPEC_NAME="fedimint" + SPEC_IMAGE="${FEDIMINT_IMAGE:-docker.io/fedimint/fedimintd:v0.5.1}" + SPEC_NETWORK="archy-net" + SPEC_PORTS="8173:8173 8174:8174 8175:8175" + SPEC_VOLUMES="/var/lib/archipelago/fedimint:/data" + SPEC_MEMORY="$(mem_limit fedimint)" + SPEC_HEALTH_CMD="curl -sf http://localhost:8174/ || exit 1" + SPEC_ENV="FM_DATA_DIR=/data FM_BITCOIND_USERNAME=$BITCOIN_RPC_USER FM_BITCOIND_PASSWORD=$BITCOIN_RPC_PASS FM_BITCOIN_NETWORK=bitcoin FM_BIND_P2P=0.0.0.0:8173 FM_BIND_API=0.0.0.0:8174 FM_BIND_UI=0.0.0.0:8175 FM_P2P_URL=fedimint://$HOST_IP:8173 FM_API_URL=ws://$HOST_IP:8174 FM_BITCOIND_URL=http://$HOST_IP:8332" + SPEC_TIER="2" + SPEC_DATA_DIR="/var/lib/archipelago/fedimint" + SPEC_DEPENDS="bitcoin-knots" +} + +load_spec_fedimint-gateway() { + reset_spec + SPEC_NAME="fedimint-gateway" + SPEC_IMAGE="${FEDIMINT_GATEWAY_IMAGE:-docker.io/fedimint/gatewayd:v0.5.1}" + SPEC_NETWORK="archy-net" + SPEC_PORTS="8176:8176" + SPEC_VOLUMES="/var/lib/archipelago/fedimint-gateway:/data" + SPEC_MEMORY="$(mem_limit fedimint-gateway)" + SPEC_HEALTH_CMD="curl -sf http://localhost:8175/ || exit 1" + SPEC_TIER="2" + SPEC_DATA_DIR="/var/lib/archipelago/fedimint-gateway" + SPEC_DEPENDS="bitcoin-knots fedimint" + # Custom entrypoint depends on whether LND is available + local LND_CERT=/var/lib/archipelago/lnd/tls.cert + local LND_MAC=/var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/admin.macaroon + if [ -f "$LND_CERT" ] && [ -f "$LND_MAC" ]; then + SPEC_VOLUMES="$SPEC_VOLUMES $LND_CERT:/lnd/tls.cert:ro $LND_MAC:/lnd/admin.macaroon:ro" + SPEC_ENTRYPOINT="gatewayd --data-dir /data --listen 0.0.0.0:8176 --bcrypt-password-hash $FEDI_HASH --network bitcoin --bitcoind-url http://$HOST_IP:8332 --bitcoind-username $BITCOIN_RPC_USER --bitcoind-password $BITCOIN_RPC_PASS lnd --lnd-rpc-host $HOST_IP:10009 --lnd-tls-cert /lnd/tls.cert --lnd-macaroon /lnd/admin.macaroon" + else + SPEC_PORTS="8176:8176 9737:9737" + SPEC_ENTRYPOINT="gatewayd --data-dir /data --listen 0.0.0.0:8176 --bcrypt-password-hash $FEDI_HASH --network bitcoin --bitcoind-url http://$HOST_IP:8332 --bitcoind-username $BITCOIN_RPC_USER --bitcoind-password $BITCOIN_RPC_PASS ldk --ldk-lightning-port 9737 --ldk-alias archipelago-gateway" + fi +} + +load_spec_immich_server() { + reset_spec + SPEC_NAME="immich_server" + SPEC_IMAGE="ghcr.io/immich-app/immich-server:release" + SPEC_NETWORK="bridge" + SPEC_PORTS="2283:2283" + SPEC_VOLUMES="/var/lib/archipelago/immich:/usr/src/app/upload" + SPEC_MEMORY="$(mem_limit immich_server)" + SPEC_ENV="DB_HOSTNAME=immich_postgres DB_DATABASE_NAME=immich DB_USERNAME=postgres DB_PASSWORD=$BTCPAY_DB_PASS REDIS_HOSTNAME=immich_redis UPLOAD_LOCATION=/usr/src/app/upload" + SPEC_TIER="2" + SPEC_DATA_DIR="/var/lib/archipelago/immich" + SPEC_DEPENDS="immich_postgres immich_redis" + SPEC_CAPS="" + SPEC_OPTIONAL="true" +} + +# ── Tier 3: Applications ───────────────────────────────────────────── + +load_spec_homeassistant() { + reset_spec + SPEC_NAME="homeassistant" + SPEC_IMAGE="${HOMEASSISTANT_IMAGE:-ghcr.io/home-assistant/home-assistant:2024.12}" + SPEC_PORTS="8123:8123" + SPEC_VOLUMES="/var/lib/archipelago/home-assistant:/config" + SPEC_MEMORY="$(mem_limit homeassistant)" + SPEC_HEALTH_CMD="curl -sf http://localhost:8123/ || exit 1" + SPEC_ENV="TZ=UTC" + SPEC_TIER="3" + SPEC_DATA_DIR="/var/lib/archipelago/home-assistant" + SPEC_CAPS="CHOWN SETUID SETGID DAC_OVERRIDE" +} + +load_spec_grafana() { + reset_spec + SPEC_NAME="grafana" + SPEC_IMAGE="${GRAFANA_IMAGE:-docker.io/grafana/grafana:11.4.0}" + SPEC_PORTS="3000:3000" + SPEC_VOLUMES="/var/lib/archipelago/grafana:/var/lib/grafana" + SPEC_MEMORY="$(mem_limit grafana)" + SPEC_HEALTH_CMD="curl -sf http://localhost:3000/api/health || exit 1" + SPEC_ENV="GF_PATHS_DATA=/var/lib/grafana GF_USERS_ALLOW_SIGN_UP=false" + SPEC_READONLY="true" + SPEC_TMPFS="/tmp:rw,noexec,nosuid,size=256m /run:rw,noexec,nosuid,size=64m" + SPEC_TIER="3" + SPEC_DATA_DIR="/var/lib/archipelago/grafana" + SPEC_DATA_UID="100472:100472" + SPEC_CAPS="CHOWN SETUID SETGID" +} + +load_spec_uptime-kuma() { + reset_spec + SPEC_NAME="uptime-kuma" + SPEC_IMAGE="${UPTIME_KUMA_IMAGE:-docker.io/louislam/uptime-kuma:1}" + SPEC_PORTS="3001:3001" + SPEC_VOLUMES="/var/lib/archipelago/uptime-kuma:/app/data" + SPEC_MEMORY="$(mem_limit uptime-kuma)" + SPEC_HEALTH_CMD="curl -sf http://localhost:3001/ || exit 1" + SPEC_ENV="TZ=UTC" + SPEC_TIER="3" + SPEC_DATA_DIR="/var/lib/archipelago/uptime-kuma" + SPEC_CAPS="CHOWN FOWNER SETUID SETGID" +} + +load_spec_jellyfin() { + reset_spec + SPEC_NAME="jellyfin" + SPEC_IMAGE="${JELLYFIN_IMAGE:-docker.io/jellyfin/jellyfin:10.10.3}" + SPEC_PORTS="8096:8096" + SPEC_VOLUMES="/var/lib/archipelago/jellyfin/config:/config /var/lib/archipelago/jellyfin/cache:/cache" + SPEC_MEMORY="$(mem_limit jellyfin)" + SPEC_HEALTH_CMD="curl -sf http://localhost:8096/ || exit 1" + SPEC_TIER="3" + SPEC_DATA_DIR="/var/lib/archipelago/jellyfin" + SPEC_CAPS="" +} + +load_spec_photoprism() { + reset_spec + SPEC_NAME="photoprism" + SPEC_IMAGE="${PHOTOPRISM_IMAGE:-docker.io/photoprism/photoprism:240915}" + SPEC_PORTS="2342:2342" + SPEC_VOLUMES="/var/lib/archipelago/photoprism:/photoprism/storage" + SPEC_MEMORY="$(mem_limit photoprism)" + SPEC_HEALTH_CMD="curl -sf http://localhost:2342/ || exit 1" + SPEC_ENV="PHOTOPRISM_ADMIN_PASSWORD=archipelago PHOTOPRISM_DEFAULT_LOCALE=en" + SPEC_TIER="3" + SPEC_DATA_DIR="/var/lib/archipelago/photoprism" + SPEC_CAPS="CHOWN SETUID SETGID" +} + +load_spec_vaultwarden() { + reset_spec + SPEC_NAME="vaultwarden" + SPEC_IMAGE="${VAULTWARDEN_IMAGE:-docker.io/vaultwarden/server:1.32.5}" + SPEC_PORTS="8082:80" + SPEC_VOLUMES="/var/lib/archipelago/vaultwarden:/data" + SPEC_MEMORY="$(mem_limit vaultwarden)" + SPEC_HEALTH_CMD="curl -sf http://localhost:80/ || exit 1" + SPEC_TIER="3" + SPEC_DATA_DIR="/var/lib/archipelago/vaultwarden" + SPEC_CAPS="CHOWN SETUID SETGID NET_BIND_SERVICE" +} + +load_spec_nextcloud() { + reset_spec + SPEC_NAME="nextcloud" + SPEC_IMAGE="${NEXTCLOUD_IMAGE:-docker.io/library/nextcloud:29}" + SPEC_PORTS="8085:80" + SPEC_VOLUMES="/var/lib/archipelago/nextcloud:/var/www/html" + SPEC_MEMORY="$(mem_limit nextcloud)" + SPEC_HEALTH_CMD="curl -sf http://localhost:80/ || exit 1" + SPEC_TIER="3" + SPEC_DATA_DIR="/var/lib/archipelago/nextcloud" + SPEC_CAPS="CHOWN SETUID SETGID DAC_OVERRIDE" +} + +load_spec_searxng() { + reset_spec + SPEC_NAME="searxng" + SPEC_IMAGE="${SEARXNG_IMAGE:-docker.io/searxng/searxng:2026.3.20-6c7e9c197}" + SPEC_PORTS="8888:8080" + SPEC_MEMORY="$(mem_limit searxng)" + SPEC_HEALTH_CMD="curl -sf http://localhost:8080/ || exit 1" + SPEC_READONLY="true" + SPEC_TMPFS="/tmp:rw,noexec,nosuid,size=256m /run:rw,noexec,nosuid,size=64m /etc/searxng:rw,noexec,nosuid,size=16m" + SPEC_TIER="3" + SPEC_CAPS="" +} + +load_spec_onlyoffice() { + reset_spec + SPEC_NAME="onlyoffice" + SPEC_IMAGE="${ONLYOFFICE_IMAGE:-docker.io/onlyoffice/documentserver:8.2}" + SPEC_PORTS="9980:80" + SPEC_MEMORY="$(mem_limit onlyoffice)" + SPEC_HEALTH_CMD="curl -sf http://localhost:80/ || exit 1" + SPEC_TIER="3" + SPEC_CAPS="CHOWN SETUID SETGID DAC_OVERRIDE" +} + +load_spec_filebrowser() { + reset_spec + SPEC_NAME="filebrowser" + SPEC_IMAGE="${FILEBROWSER_IMAGE:-docker.io/filebrowser/filebrowser:v2}" + SPEC_PORTS="8083:80" + SPEC_VOLUMES="/var/lib/archipelago/filebrowser:/srv" + SPEC_MEMORY="$(mem_limit filebrowser)" + SPEC_HEALTH_CMD="curl -sf http://localhost:80/ || exit 1" + SPEC_TIER="3" + SPEC_DATA_DIR="/var/lib/archipelago/filebrowser" + SPEC_CAPS="" +} + +load_spec_nginx-proxy-manager() { + reset_spec + SPEC_NAME="nginx-proxy-manager" + SPEC_IMAGE="${NPM_IMAGE:-docker.io/jc21/nginx-proxy-manager:2}" + SPEC_PORTS="81:81 8084:80 8443:443" + SPEC_VOLUMES="/var/lib/archipelago/nginx-proxy-manager/data:/data /var/lib/archipelago/nginx-proxy-manager/letsencrypt:/etc/letsencrypt" + SPEC_MEMORY="$(mem_limit nginx-proxy-manager)" + SPEC_HEALTH_CMD="curl -sf http://localhost:81/ || exit 1" + SPEC_TIER="3" + SPEC_DATA_DIR="/var/lib/archipelago/nginx-proxy-manager" + SPEC_CAPS="CHOWN SETUID SETGID NET_BIND_SERVICE" +} + +load_spec_portainer() { + reset_spec + SPEC_NAME="portainer" + SPEC_IMAGE="${PORTAINER_IMAGE:-docker.io/portainer/portainer-ce:2.21.5}" + SPEC_PORTS="9000:9000" + SPEC_VOLUMES="/var/lib/archipelago/portainer:/data /run/user/1000/podman/podman.sock:/var/run/docker.sock" + SPEC_MEMORY="$(mem_limit portainer)" + SPEC_HEALTH_CMD="curl -sf http://localhost:9000/ || exit 1" + SPEC_TIER="3" + SPEC_DATA_DIR="/var/lib/archipelago/portainer" +} + +load_spec_ollama() { + reset_spec + SPEC_NAME="ollama" + SPEC_IMAGE="${OLLAMA_IMAGE:-docker.io/ollama/ollama:0.5.4}" + SPEC_PORTS="11434:11434" + SPEC_VOLUMES="/var/lib/archipelago/ollama:/root/.ollama" + SPEC_MEMORY="$(mem_limit ollama)" + SPEC_HEALTH_CMD="curl -sf http://localhost:11434/ || exit 1" + SPEC_READONLY="true" + SPEC_TMPFS="/tmp:rw,noexec,nosuid,size=256m /run:rw,noexec,nosuid,size=64m" + SPEC_TIER="3" + SPEC_DATA_DIR="/var/lib/archipelago/ollama" + SPEC_CAPS="" + SPEC_OPTIONAL="true" +} + +# ── Tier 4: Frontend UIs ───────────────────────────────────────────── + +load_spec_archy-bitcoin-ui() { + reset_spec + SPEC_NAME="archy-bitcoin-ui" + SPEC_IMAGE="localhost/bitcoin-ui:local" + SPEC_NETWORK="host" + SPEC_MEMORY="$(mem_limit archy-bitcoin-ui)" + SPEC_TIER="4" + SPEC_LOCAL_IMAGE="true" + SPEC_CAPS="" + SPEC_SECURITY="" +} + +load_spec_archy-lnd-ui() { + reset_spec + SPEC_NAME="archy-lnd-ui" + SPEC_IMAGE="localhost/lnd-ui:local" + SPEC_PORTS="8081:80" + SPEC_MEMORY="$(mem_limit archy-lnd-ui)" + SPEC_TIER="4" + SPEC_LOCAL_IMAGE="true" + SPEC_CAPS="" + SPEC_SECURITY="" +} + +load_spec_archy-electrs-ui() { + reset_spec + SPEC_NAME="archy-electrs-ui" + SPEC_IMAGE="localhost/electrs-ui:local" + SPEC_NETWORK="host" + SPEC_MEMORY="$(mem_limit archy-electrs-ui)" + SPEC_TIER="4" + SPEC_LOCAL_IMAGE="true" + SPEC_CAPS="" + SPEC_SECURITY="" +} + +# ── Registry ───────────────────────────────────────────────────────── +# Ordered by tier, then dependency order within tier + +ALL_CONTAINER_SPECS=( + # Tier 0: Databases + archy-mempool-db + archy-btcpay-db + immich_postgres + immich_redis + # Tier 1: Core + bitcoin-knots + electrumx + # Tier 2: Services + lnd + mempool-api + archy-mempool-web + archy-nbxplorer + btcpay-server + fedimint + fedimint-gateway + immich_server + # Tier 3: Apps + homeassistant + grafana + uptime-kuma + jellyfin + photoprism + vaultwarden + nextcloud + searxng + onlyoffice + filebrowser + nginx-proxy-manager + portainer + ollama + # Tier 4: UIs + archy-bitcoin-ui + archy-lnd-ui + archy-electrs-ui +) + +# Load a spec by name. Usage: load_spec "bitcoin-knots" +load_spec() { + local fn="load_spec_${1}" + if declare -f "$fn" >/dev/null 2>&1; then + "$fn" + return 0 + fi + return 1 +} + +# Return all spec names +all_specs() { + echo "${ALL_CONTAINER_SPECS[@]}" +} diff --git a/scripts/reconcile-containers.sh b/scripts/reconcile-containers.sh new file mode 100755 index 00000000..b141aebd --- /dev/null +++ b/scripts/reconcile-containers.sh @@ -0,0 +1,523 @@ +#!/bin/bash +# +# Archipelago Container Reconciler +# Ensures every container matches the canonical spec from container-specs.sh. +# Safe to run repeatedly (idempotent). Run on any node. +# +# Usage: +# sudo ./reconcile-containers.sh # Fix everything +# sudo ./reconcile-containers.sh --check-only # Audit only, no changes +# sudo ./reconcile-containers.sh --force # Override user-stopped +# sudo ./reconcile-containers.sh --tier=2 # Only reconcile tier 2 +# sudo ./reconcile-containers.sh --container=lnd # Only reconcile lnd +# +set -o pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +# ── Parse arguments ────────────────────────────────────────────────── +CHECK_ONLY=false +FORCE=false +FILTER_TIER="" +FILTER_CONTAINER="" +for arg in "$@"; do + case "$arg" in + --check-only) CHECK_ONLY=true ;; + --force) FORCE=true ;; + --tier=*) FILTER_TIER="${arg#*=}" ;; + --container=*) FILTER_CONTAINER="${arg#*=}" ;; + -h|--help) + echo "Usage: $0 [--check-only] [--force] [--tier=N] [--container=NAME]" + exit 0 ;; + esac +done + +# ── Colors ─────────────────────────────────────────────────────────── +RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' +BLUE='\033[0;34m' CYAN='\033[0;36m' BOLD='\033[1m' +NC='\033[0m' + +ok() { echo -e " ${GREEN}[OK]${NC} $*"; } +fixed() { echo -e " ${CYAN}[FIXED]${NC} $*"; } +skip() { echo -e " ${YELLOW}[SKIP]${NC} $*"; } +fail() { echo -e " ${RED}[FAIL]${NC} $*"; } +info() { echo -e " ${BLUE}[INFO]${NC} $*"; } +header(){ echo -e "\n${BOLD}$*${NC}"; } + +# ── Source specs ───────────────────────────────────────────────────── +source "$SCRIPT_DIR/container-specs.sh" || { echo "Cannot source container-specs.sh"; exit 1; } +detect_environment + +# ── Podman command ─────────────────────────────────────────────────── +# Run as archipelago user — podman sees rootless containers directly. +# Use sudo only for chown/mkdir operations. +PODMAN="podman" + +# ── Pre-flight ─────────────────────────────────────────────────────── +header "╔══════════════════════════════════════════════════╗" +header "║ ARCHIPELAGO CONTAINER RECONCILER ║" +header "╚══════════════════════════════════════════════════╝" +echo "" +info "Host: $(hostname) ($HOST_IP)" +info "Disk: ${DISK_GB}GB | RAM: ${TOTAL_MEM_MB}MB | Low-mem: $LOW_MEM" +info "Mode: $($CHECK_ONLY && echo 'CHECK ONLY (no changes)' || echo 'APPLY FIXES')" +echo "" + +# Ensure archy-net exists +if ! $PODMAN network exists archy-net 2>/dev/null; then + if $CHECK_ONLY; then + info "archy-net missing (would create)" + else + $PODMAN network create archy-net 2>/dev/null && info "Created archy-net" || fail "Cannot create archy-net" + fi +fi + +# Load user-stopped list +USER_STOPPED_FILE="/var/lib/archipelago/user-stopped.json" +USER_STOPPED="" +if [ -f "$USER_STOPPED_FILE" ]; then + USER_STOPPED=$(cat "$USER_STOPPED_FILE" 2>/dev/null) +fi +is_user_stopped() { + [ "$FORCE" = "true" ] && return 1 + echo "$USER_STOPPED" | grep -q "\"$1\"" 2>/dev/null +} + +# ── Inspection helpers ─────────────────────────────────────────────── +container_exists() { + $PODMAN ps -a --format '{{.Names}}' 2>/dev/null | grep -qx "$1" +} + +container_running() { + $PODMAN ps --format '{{.Names}}' 2>/dev/null | grep -qx "$1" +} + +container_image() { + $PODMAN inspect "$1" --format '{{.ImageName}}' 2>/dev/null +} + +container_network() { + # Use actual Networks map — NetworkMode is unreliable (always shows 'bridge' in rootless) + local nets + nets=$($PODMAN inspect "$1" --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' 2>/dev/null) + # Return first network name, trimmed + echo "$nets" | awk '{print $1}' +} + +container_memory() { + $PODMAN inspect "$1" --format '{{.HostConfig.Memory}}' 2>/dev/null +} + +image_exists() { + $PODMAN images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q "$1" +} + +# Convert memory string to bytes for comparison +mem_to_bytes() { + local m="$1" + case "$m" in + *g|*G) echo $(( ${m%[gG]} * 1073741824 )) ;; + *m|*M) echo $(( ${m%[mM]} * 1048576 )) ;; + *) echo "$m" ;; + esac +} + +# ── Build podman run command from spec ─────────────────────────────── +build_run_cmd() { + local cmd="$PODMAN run -d --name $SPEC_NAME" + cmd+=" --restart $SPEC_RESTART" + + # Network + if [ "$SPEC_NETWORK" = "host" ]; then + cmd+=" --network=host" + elif [ "$SPEC_NETWORK" = "archy-net" ]; then + cmd+=" --network archy-net" + fi + + # Memory + [ -n "$SPEC_MEMORY" ] && cmd+=" --memory=$SPEC_MEMORY" + + # Capabilities + cmd+=" --cap-drop ALL" + for cap in $SPEC_CAPS; do + cmd+=" --cap-add $cap" + done + + # Security + [ -n "$SPEC_SECURITY" ] && cmd+=" --security-opt $SPEC_SECURITY" + + # Read-only + [ "$SPEC_READONLY" = "true" ] && cmd+=" --read-only" + + # Tmpfs + for t in $SPEC_TMPFS; do + cmd+=" --tmpfs $t" + done + + # Health check + if [ -n "$SPEC_HEALTH_CMD" ]; then + cmd+=" --health-cmd=\"$SPEC_HEALTH_CMD\" --health-interval=30s --health-timeout=5s --health-retries=3" + fi + + # Ports + for p in $SPEC_PORTS; do + cmd+=" -p $p" + done + + # Volumes + for v in $SPEC_VOLUMES; do + cmd+=" -v $v" + done + + # Environment + for e in $SPEC_ENV; do + cmd+=" -e \"$e\"" + done + + # Image + cmd+=" $SPEC_IMAGE" + + # Custom args + [ -n "$SPEC_CUSTOM_ARGS" ] && cmd+=" $SPEC_CUSTOM_ARGS" + + # Entrypoint override + [ -n "$SPEC_ENTRYPOINT" ] && cmd+=" $SPEC_ENTRYPOINT" + + echo "$cmd" +} + +# ── Counters ───────────────────────────────────────────────────────── +COUNT_OK=0 COUNT_FIXED=0 COUNT_CREATED=0 COUNT_SKIPPED=0 COUNT_FAILED=0 +FAILED_LIST="" + +# ── Reconcile one container ────────────────────────────────────────── +reconcile() { + local name="$1" + + if ! load_spec "$name"; then + skip "$name — no spec defined" + COUNT_SKIPPED=$((COUNT_SKIPPED + 1)) + return + fi + + # Filter by tier + [ -n "$FILTER_TIER" ] && [ "$SPEC_TIER" != "$FILTER_TIER" ] && return + + # User-stopped + if is_user_stopped "$name"; then + skip "$name — user-stopped" + COUNT_SKIPPED=$((COUNT_SKIPPED + 1)) + fix_ownership "$name" + return + fi + + # Optional/local images: skip if image doesn't exist and container doesn't exist + if [ "$SPEC_OPTIONAL" = "true" ] || [ "$SPEC_LOCAL_IMAGE" = "true" ]; then + if ! image_exists "$SPEC_IMAGE" && ! container_exists "$name"; then + skip "$name — image not available" + COUNT_SKIPPED=$((COUNT_SKIPPED + 1)) + return + fi + fi + + # Check dependencies + for dep in $SPEC_DEPENDS; do + if ! container_running "$dep"; then + skip "$name — dependency $dep not running" + COUNT_SKIPPED=$((COUNT_SKIPPED + 1)) + return + fi + done + + local action="OK" + local reasons="" + + if container_exists "$name"; then + local cur_image cur_network cur_memory + cur_image=$(container_image "$name") + cur_network=$(container_network "$name") + cur_memory=$(container_memory "$name") + local spec_memory_bytes expected_network + + spec_memory_bytes=$(mem_to_bytes "$SPEC_MEMORY") + + # Check network mismatch + # For archy-net and host: exact match required + # For bridge/default: accept any non-archy-net, non-host network + if [ "$SPEC_NETWORK" = "archy-net" ]; then + if [ "$cur_network" != "archy-net" ]; then + action="RECREATE" + reasons+="network($cur_network→archy-net) " + fi + elif [ "$SPEC_NETWORK" = "host" ]; then + if [ "$cur_network" != "host" ]; then + action="RECREATE" + reasons+="network($cur_network→host) " + fi + else + # Default/bridge: anything that isn't archy-net or host is fine + if [ "$cur_network" = "archy-net" ] || [ "$cur_network" = "host" ]; then + action="RECREATE" + reasons+="network($cur_network→bridge) " + fi + fi + + # Check memory limit (0 = no limit) + if [ "${cur_memory:-0}" = "0" ] && [ "${spec_memory_bytes:-0}" != "0" ]; then + action="RECREATE" + reasons+="memory(none→$SPEC_MEMORY) " + fi + + # Check if running + if ! container_running "$name" && [ "$action" = "OK" ]; then + action="START" + reasons+="not-running " + fi + else + action="CREATE" + reasons+="missing " + fi + + # Fix ownership regardless + fix_ownership "$name" + + case "$action" in + OK) + ok "$name" + COUNT_OK=$((COUNT_OK + 1)) + ;; + START) + if $CHECK_ONLY; then + info "$name — would start ($reasons)" + else + if $PODMAN start "$name" >/dev/null 2>&1; then + fixed "$name — started ($reasons)" + else + fail "$name — start failed" + COUNT_FAILED=$((COUNT_FAILED + 1)) + FAILED_LIST+=" $name" + return + fi + fi + COUNT_FIXED=$((COUNT_FIXED + 1)) + ;; + RECREATE) + if $CHECK_ONLY; then + info "$name — would recreate ($reasons)" + else + info "$name — recreating ($reasons)" + $PODMAN stop "$name" >/dev/null 2>&1 + $PODMAN rm "$name" >/dev/null 2>&1 + if eval "$(build_run_cmd)" >/dev/null 2>&1; then + fixed "$name — recreated ($reasons)" + else + fail "$name — recreate failed: $(eval "$(build_run_cmd)" 2>&1 | tail -1)" + COUNT_FAILED=$((COUNT_FAILED + 1)) + FAILED_LIST+=" $name" + return + fi + fi + COUNT_FIXED=$((COUNT_FIXED + 1)) + ;; + CREATE) + if $CHECK_ONLY; then + info "$name — would create ($reasons)" + else + for v in $SPEC_VOLUMES; do + local host_dir="${v%%:*}" + [ -n "$host_dir" ] && sudo mkdir -p "$host_dir" 2>/dev/null + done + if eval "$(build_run_cmd)" >/dev/null 2>&1; then + fixed "$name — created" + else + fail "$name — create failed" + COUNT_FAILED=$((COUNT_FAILED + 1)) + FAILED_LIST+=" $name" + return + fi + fi + COUNT_CREATED=$((COUNT_CREATED + 1)) + ;; + esac +} + +# ── Fix ownership ──────────────────────────────────────────────────── +fix_ownership() { + local name="$1" + [ -z "$SPEC_DATA_DIR" ] && return + [ ! -d "$SPEC_DATA_DIR" ] && return + [ "$SPEC_DATA_UID" = "100000:100000" ] && return + + local expected_uid="${SPEC_DATA_UID%%:*}" + local current_uid + current_uid=$(stat -c '%u' "$SPEC_DATA_DIR" 2>/dev/null) + + if [ "$current_uid" != "$expected_uid" ]; then + if $CHECK_ONLY; then + info "$name — ownership: $current_uid → $SPEC_DATA_UID" + else + sudo chown -R "$SPEC_DATA_UID" "$SPEC_DATA_DIR" 2>/dev/null + info "$name — fixed ownership → $SPEC_DATA_UID" + fi + fi +} + +# ── Ensure secrets exist ───────────────────────────────────────────── +ensure_secrets() { + local SECRETS_DIR="/var/lib/archipelago/secrets" + sudo mkdir -p "$SECRETS_DIR" 2>/dev/null + sudo chmod 700 "$SECRETS_DIR" 2>/dev/null + + for svc in bitcoin-rpc-password mempool-db-password btcpay-db-password mysql-root-db-password; do + if [ ! -f "$SECRETS_DIR/$svc" ]; then + if $CHECK_ONLY; then + info "Would generate secret: $svc" + else + openssl rand -hex 16 | sudo tee "$SECRETS_DIR/$svc" >/dev/null + sudo chmod 600 "$SECRETS_DIR/$svc" + info "Generated secret: $svc" + fi + fi + done + + if [ ! -f "$SECRETS_DIR/fedimint-gateway-password" ]; then + if ! $CHECK_ONLY; then + local fpass + fpass=$(openssl rand -base64 16) + echo "$fpass" | sudo tee "$SECRETS_DIR/fedimint-gateway-password" >/dev/null + sudo chmod 600 "$SECRETS_DIR/fedimint-gateway-password" + if command -v htpasswd >/dev/null 2>&1; then + htpasswd -bnBC 10 "" "$fpass" | tr -d ':\n' | sudo tee "$SECRETS_DIR/fedimint-gateway-hash" >/dev/null + sudo chmod 600 "$SECRETS_DIR/fedimint-gateway-hash" + fi + info "Generated fedimint gateway secret" + fi + fi + + # Reload after generation + detect_environment +} + +# ── Ensure bitcoin.conf ───────────────────────────────────────────── +ensure_bitcoin_conf() { + local BITCOIN_CONF="/var/lib/archipelago/bitcoin/bitcoin.conf" + sudo mkdir -p /var/lib/archipelago/bitcoin 2>/dev/null + if [ ! -f "$BITCOIN_CONF" ] || ! grep -q "^rpcauth=" "$BITCOIN_CONF" 2>/dev/null; then + if ! $CHECK_ONLY && [ -n "$BITCOIN_RPC_PASS" ]; then + local salt hash rpcauth + salt=$(openssl rand -hex 16) + hash=$(echo -n "$BITCOIN_RPC_PASS" | openssl dgst -sha256 -hmac "$salt" -hex 2>/dev/null | awk '{print $NF}') + rpcauth="${BITCOIN_RPC_USER}:${salt}\$${hash}" + # Only rpcauth + printtoconsole here — all other options are in SPEC_CUSTOM_ARGS + # to avoid duplicate bind conflicts + sudo tee "$BITCOIN_CONF" >/dev/null << BTCEOF +rpcauth=${rpcauth} +printtoconsole=1 +BTCEOF + info "Generated bitcoin.conf" + fi + fi + # Strip duplicate server/rpc/listen lines from existing conf to avoid conflicts with custom args + if [ -f "$BITCOIN_CONF" ]; then + sudo sed -i '/^server=/d; /^rpcbind=/d; /^rpcallowip=/d; /^rpcport=/d; /^listen=/d' "$BITCOIN_CONF" 2>/dev/null + fi + sudo chown -R 100101:100101 /var/lib/archipelago/bitcoin 2>/dev/null +} + +# ── Ensure lnd.conf ───────────────────────────────────────────────── +ensure_lnd_conf() { + local LND_CONF="/var/lib/archipelago/lnd/lnd.conf" + sudo mkdir -p /var/lib/archipelago/lnd 2>/dev/null + if [ ! -f "$LND_CONF" ] && [ -n "$BITCOIN_RPC_PASS" ]; then + if ! $CHECK_ONLY; then + sudo tee "$LND_CONF" >/dev/null << LNDEOF +[Application Options] +listen=0.0.0.0:9735 +rpclisten=0.0.0.0:10009 +restlisten=0.0.0.0:8080 +debuglevel=info +noseedbackup=true + +[Bitcoin] +bitcoin.mainnet=true +bitcoin.node=bitcoind + +[Bitcoind] +bitcoind.rpchost=bitcoin-knots:8332 +bitcoind.rpcuser=$BITCOIN_RPC_USER +bitcoind.rpcpass=$BITCOIN_RPC_PASS +bitcoind.rpcpolling=true +bitcoind.estimatemode=ECONOMICAL + +[autopilot] +autopilot.active=false +LNDEOF + info "Generated lnd.conf" + fi + fi +} + +# ── Ensure BTCPay databases ───────────────────────────────────────── +ensure_btcpay_db() { + if container_running "archy-btcpay-db"; then + $PODMAN exec archy-btcpay-db psql -U postgres -tc \ + "SELECT 1 FROM pg_database WHERE datname='nbxplorer'" 2>/dev/null | grep -q 1 || \ + $PODMAN exec archy-btcpay-db psql -U postgres -c \ + "CREATE DATABASE nbxplorer;" 2>/dev/null || true + fi +} + +# ══════════════════════════════════════════════════════════════════════ +# MAIN +# ══════════════════════════════════════════════════════════════════════ +START_TIME=$(date +%s) + +header "Phase 0: Prerequisites" +ensure_secrets +ensure_bitcoin_conf +ensure_lnd_conf + +TIER_NAMES=("Databases" "Core Infrastructure" "Services" "Applications" "Frontend UIs") + +for tier in 0 1 2 3 4; do + [ -n "$FILTER_TIER" ] && [ "$FILTER_TIER" != "$tier" ] && continue + + header "Tier $tier: ${TIER_NAMES[$tier]}" + + for name in "${ALL_CONTAINER_SPECS[@]}"; do + [ -n "$FILTER_CONTAINER" ] && [ "$name" != "$FILTER_CONTAINER" ] && continue + + # Load spec to check tier before reconciling + if load_spec "$name" && [ "$SPEC_TIER" = "$tier" ]; then + reconcile "$name" + fi + done + + # After databases, ensure BTCPay DB schemas exist + [ "$tier" = "0" ] && ensure_btcpay_db + + # Brief pause between tiers + [ "$tier" -lt 4 ] && ! $CHECK_ONLY && sleep 2 +done + +# ── Summary ────────────────────────────────────────────────────────── +ELAPSED=$(( $(date +%s) - START_TIME )) +TOTAL=$((COUNT_OK + COUNT_FIXED + COUNT_CREATED + COUNT_SKIPPED + COUNT_FAILED)) + +echo "" +header "╔══════════════════════════════════════════════════╗" +header "║ RECONCILIATION REPORT ║" +header "╚══════════════════════════════════════════════════╝" +echo "" +echo -e " Total: ${BOLD}$TOTAL${NC}" +echo -e " OK: ${GREEN}$COUNT_OK${NC}" +echo -e " Fixed: ${CYAN}$COUNT_FIXED${NC}" +echo -e " Created: ${CYAN}$COUNT_CREATED${NC}" +echo -e " Skipped: ${YELLOW}$COUNT_SKIPPED${NC}" +echo -e " Failed: ${RED}$COUNT_FAILED${NC}" +[ -n "$FAILED_LIST" ] && echo -e " Failed: ${RED}$FAILED_LIST${NC}" +echo -e " Duration: ${ELAPSED}s" +echo "" + +[ "$COUNT_FAILED" -gt 0 ] && exit 1 +exit 0