#!/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() { # Measure disk where container data actually lives, not the OS partition. # Archipelago installs mount a separate (usually-encrypted) data volume at # /var/lib/archipelago on any host with meaningful storage, so checking / # would always report the ~30 GB OS partition and wrongly trip prune mode # on 2 TB boxes. Fall back to / only for first-boot before the data # partition is mounted. local disk_target="/var/lib/archipelago" [ -d "$disk_target" ] || disk_target="/" DISK_GB=$(df --output=size -BG "$disk_target" 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} # Stable mDNS hostname for URLs that get baked into federation/consensus data. # Survives DHCP churn and reinstalls-on-different-IP (which $HOST_IP does not). # Requires avahi-daemon (shipped on all Archipelago nodes). HOST_MDNS="$(hostname 2>/dev/null).local" HOST_MDNS="${HOST_MDNS:-archipelago.local}" # 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 "") # Escape $ so SPEC_ENTRYPOINT survives eval in reconcile-containers.sh:build_run_cmd. # bcrypt hashes have the form $2y$10$... and get mangled if $2 and $10 are # interpolated as positional args at eval time. FEDI_HASH="${FEDI_HASH//\$/\\\$}" } # ── 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_OPTIONAL defaults true: reconcile-containers.sh only REPAIRS existing # containers — it never creates missing ones. Baseline (filebrowser) is # bootstrapped by first-boot-containers.sh; all other apps come from the # install RPC. Per-spec `SPEC_OPTIONAL="true"` lines below are now redundant # but kept for readability. SPEC_DEPENDS="" SPEC_LOCAL_IMAGE="false" SPEC_OPTIONAL="true" SPEC_ENTRYPOINT="" } if ! declare -F alloc_port >/dev/null 2>&1; then alloc_port() { printf '%s' "$2"; } fi # ── Tier 0: Databases ──────────────────────────────────────────────── load_spec_archy-mempool-db() { reset_spec SPEC_NAME="archy-mempool-db" SPEC_IMAGE="${MARIADB_IMAGE}" 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}" 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="${IMMICH_POSTGRES_IMAGE}" 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}" 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}" 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" local btc_dbcache=4096 [ "${LOW_MEM:-false}" = "true" ] && btc_dbcache=2048 local btc_rpc_headroom="-rpcthreads=16 -rpcworkqueue=256" local btc_txrelay_flags="-rpcwhitelistdefault=0" if [ -f "$SECRETS_DIR/bitcoin-rpc-txrelay-rpcauth" ]; then btc_txrelay_flags="$btc_txrelay_flags -rpcauth=$(cat "$SECRETS_DIR/bitcoin-rpc-txrelay-rpcauth") -rpcwhitelist=txrelay:sendrawtransaction,testmempoolaccept,getmempoolinfo,getrawmempool,getmempoolentry,getnetworkinfo,getblockchaininfo,getblockcount,getblockhash,getblockheader,getrawtransaction,decoderawtransaction,decodescript,estimatesmartfee" fi # 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=${btc_dbcache} -par=0 -maxconnections=125 ${btc_rpc_headroom} ${btc_txrelay_flags}" 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 -par=0 -maxconnections=125 ${btc_rpc_headroom} ${btc_txrelay_flags}" fi } load_spec_electrumx() { reset_spec SPEC_NAME="electrumx" SPEC_IMAGE="${ELECTRUMX_IMAGE}" SPEC_NETWORK="archy-net" SPEC_PORTS="50001:50001" SPEC_VOLUMES="/var/lib/archipelago/electrumx:/data" SPEC_MEMORY="$(mem_limit electrumx)" SPEC_HEALTH_CMD="python3 -c 'import socket; socket.create_connection((\\\"localhost\\\",8000),2).close()' || 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="DAC_OVERRIDE" } # ── Tier 2: Services ───────────────────────────────────────────────── load_spec_lnd() { reset_spec SPEC_NAME="lnd" SPEC_IMAGE="${LND_IMAGE}" SPEC_NETWORK="archy-net" SPEC_PORTS="9735:9735 10009:10009 18080:8080" SPEC_VOLUMES="/var/lib/archipelago/lnd:/root/.lnd" SPEC_MEMORY="$(mem_limit lnd)" SPEC_CAPS="CHOWN FOWNER SETUID SETGID DAC_OVERRIDE NET_RAW" SPEC_HEALTH_CMD="lncli --tlscertpath /root/.lnd/tls.cert --macaroonpath /root/.lnd/data/chain/bitcoin/mainnet/readonly.macaroon --rpcserver localhost:10009 getinfo > /dev/null 2>&1 || 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}" 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}" 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}" 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=Username=btcpay;Password=$BTCPAY_DB_PASS;Host=archy-btcpay-db;Port=5432;Database=nbxplorer" 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}" SPEC_NETWORK="archy-net" SPEC_PORTS="23000:49392" SPEC_VOLUMES="/var/lib/archipelago/btcpay:/datadir" SPEC_MEMORY="$(mem_limit btcpay-server)" SPEC_HEALTH_CMD="bash -ec '/dev/null 2>&1; then "$fn" return 0 fi return 1 } # Return all spec names all_specs() { echo "${ALL_CONTAINER_SPECS[@]}" }