archy/scripts/container-specs.sh
archipelago cc2e055e09 fix(bitcoin,ui): RAM-aware dbcache to stop swap-thrash 502s + snappier status + icon placeholder
Sizes bitcoind -dbcache to host RAM (~1/16, floor 300MB, cap 4096) instead of a
fixed 2048/4096. A multi-GB UTXO cache on an 8GB node running the full app stack
pushed memory past physical RAM and triggered system-wide swap thrash: the disk
saturated, bitcoind could not answer its own RPC, and the dashboard backend's
sqlite reads stalled — surfacing as fleet-wide /rpc/v1 502s and a blank Bitcoin
UI. Applied in scripts/container-specs.sh (reconciler path) and the config.rs
bitcoin-core path.

Bitcoin status cache now polls every 5s (was 10/15) with an 8s timeout (was 20s)
and fetches the four RPCs concurrently, so the cached snapshot tracks bitcoind's
responsive windows during IBD and the UI stops dwelling on "reconnecting...".

Unifies the divergent discover AppGrid/FeaturedApps image-error handlers onto the
canonical placeholder fallback so missing app icons render the placeholder.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 09:14:47 -04:00

641 lines
26 KiB
Bash
Executable File

#!/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
# Bitcoin UTXO cache (dbcache) sized to host RAM, NOT a fixed value.
# A large dbcache on a small box pushes total memory (bitcoind + the ~20 app
# containers) past physical RAM and forces system-wide swap thrash: the disk
# saturates, bitcoind can't answer its own RPC, and the dashboard backend's
# sqlite reads stall — surfacing as fleet-wide /rpc/v1 502s and a blank
# Bitcoin UI. The old binary LOW_MEM->2048 toggle still over-committed 8 GB
# nodes. Budget ~1/16 of RAM for the cache, leaving the bulk for the OS +
# containers; floor 300 MB (bitcoind default is 450), cap 4096 MB.
BTC_DBCACHE=$(( TOTAL_MEM_MB / 16 ))
[ "$BTC_DBCACHE" -lt 300 ] && BTC_DBCACHE=300
[ "$BTC_DBCACHE" -gt 4096 ] && BTC_DBCACHE=4096
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_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,submitpackage,testmempoolaccept,getmempoolinfo,getrawmempool,getmempoolentry,getnetworkinfo,getblockchaininfo,getblockcount,getblockhash,getblock,getblockheader,getrawtransaction,gettxout,gettxspendingprevout,decoderawtransaction,decodescript,estimatesmartfee,uptime,ping,getconnectioncount,getpeerinfo,getindexinfo,getdeploymentinfo,getchaintips"
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=${BTC_DBCACHE} -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/tcp/127.0.0.1/49392'"
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=Username=btcpay;Password=$BTCPAY_DB_PASS;Host=archy-btcpay-db;Port=5432;Database=btcpay"
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}"
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:8175/ || 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_MDNS:8173 FM_API_URL=ws://$HOST_MDNS:8174 FM_BITCOIND_URL=http://bitcoin-knots:8332"
SPEC_TIER="2"
SPEC_DATA_DIR="/var/lib/archipelago/fedimint"
SPEC_DEPENDS="bitcoin-knots"
SPEC_OPTIONAL="true"
}
load_spec_fedimint-gateway() {
reset_spec
SPEC_NAME="fedimint-gateway"
SPEC_IMAGE="${FEDIMINT_GATEWAY_IMAGE}"
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:8176/ || exit 1"
SPEC_TIER="2"
SPEC_DATA_DIR="/var/lib/archipelago/fedimint-gateway"
SPEC_DEPENDS="bitcoin-knots fedimint"
SPEC_OPTIONAL="true"
# 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://bitcoin-knots:8332 --bitcoind-username $BITCOIN_RPC_USER --bitcoind-password $BITCOIN_RPC_PASS lnd --lnd-rpc-host lnd: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://bitcoin-knots: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="${IMMICH_SERVER_IMAGE}"
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}"
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 NET_BIND_SERVICE"
SPEC_OPTIONAL="true"
}
load_spec_grafana() {
reset_spec
SPEC_NAME="grafana"
SPEC_IMAGE="${GRAFANA_IMAGE}"
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 DAC_OVERRIDE NET_BIND_SERVICE"
SPEC_OPTIONAL="true"
}
load_spec_uptime-kuma() {
reset_spec
SPEC_NAME="uptime-kuma"
SPEC_IMAGE="${UPTIME_KUMA_IMAGE}"
SPEC_PORTS="3002: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"
SPEC_OPTIONAL="true"
}
load_spec_jellyfin() {
reset_spec
SPEC_NAME="jellyfin"
SPEC_IMAGE="${JELLYFIN_IMAGE}"
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=""
SPEC_OPTIONAL="true"
}
load_spec_photoprism() {
reset_spec
SPEC_NAME="photoprism"
SPEC_IMAGE="${PHOTOPRISM_IMAGE}"
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"
SPEC_OPTIONAL="true"
}
load_spec_vaultwarden() {
reset_spec
SPEC_NAME="vaultwarden"
SPEC_IMAGE="${VAULTWARDEN_IMAGE}"
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"
SPEC_OPTIONAL="true"
}
load_spec_nextcloud() {
reset_spec
SPEC_NAME="nextcloud"
SPEC_IMAGE="${NEXTCLOUD_IMAGE}"
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 NET_BIND_SERVICE"
SPEC_OPTIONAL="true"
}
load_spec_searxng() {
reset_spec
SPEC_NAME="searxng"
SPEC_IMAGE="${SEARXNG_IMAGE}"
SPEC_PORTS="8888:8080"
SPEC_MEMORY="$(mem_limit searxng)"
SPEC_VOLUMES="/var/lib/archipelago/searxng:/etc/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"
SPEC_TIER="3"
SPEC_CAPS=""
SPEC_DATA_DIR="/var/lib/archipelago/searxng"
SPEC_OPTIONAL="true"
}
load_spec_filebrowser() {
reset_spec
SPEC_NAME="filebrowser"
SPEC_IMAGE="${FILEBROWSER_IMAGE}"
SPEC_NETWORK="archy-net"
SPEC_PORTS="8083:80"
SPEC_VOLUMES="/var/lib/archipelago/filebrowser:/srv /var/lib/archipelago/filebrowser-data:/data"
SPEC_MEMORY="$(mem_limit filebrowser)"
SPEC_HEALTH_CMD="wget -q --spider http://localhost:80/health || exit 1"
SPEC_TIER="3"
SPEC_DATA_DIR="/var/lib/archipelago/filebrowser"
SPEC_DATA_UID="100000:100000"
# first-boot-containers.sh writes /data/.filebrowser.json (see filebrowser
# creation block at ~line 1128). Config path is required or filebrowser
# opens /database.db in CWD and fails with permission denied.
SPEC_CUSTOM_ARGS="--config /data/.filebrowser.json"
# Needs default caps (CHOWN FOWNER SETUID SETGID DAC_OVERRIDE) from reset_spec
# for rootless userns-root to write /data/filebrowser.db, plus NET_BIND_SERVICE
# to listen on port 80.
SPEC_CAPS="CHOWN FOWNER SETUID SETGID DAC_OVERRIDE NET_BIND_SERVICE"
SPEC_OPTIONAL="true"
}
load_spec_nginx-proxy-manager() {
reset_spec
SPEC_NAME="nginx-proxy-manager"
SPEC_IMAGE="${NPM_IMAGE}"
local admin_port http_port https_port
admin_port=$(alloc_port nginx-proxy-manager 8081 81)
http_port=$(alloc_port nginx-proxy-manager-http 8084 80)
https_port=$(alloc_port nginx-proxy-manager-https 8444 443)
SPEC_PORTS="$admin_port:81 $http_port:80 $https_port: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 FOWNER SETUID SETGID DAC_OVERRIDE NET_BIND_SERVICE"
SPEC_OPTIONAL="true"
}
load_spec_portainer() {
reset_spec
SPEC_NAME="portainer"
SPEC_IMAGE="${PORTAINER_IMAGE}"
SPEC_PORTS="9000:9000"
SPEC_VOLUMES="/var/lib/archipelago/portainer:/data /run/user/1000/podman/podman.sock:/var/run/docker.sock /var/lib/archipelago/portainer/compose:/data/compose"
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"
SPEC_DATA_UID="1000:1000"
SPEC_OPTIONAL="true"
}
load_spec_ollama() {
reset_spec
SPEC_NAME="ollama"
SPEC_IMAGE="${OLLAMA_IMAGE}"
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_VOLUMES="/var/lib/archipelago/bitcoin-ui/nginx.conf:/etc/nginx/conf.d/default.conf:ro"
SPEC_MEMORY="$(mem_limit archy-bitcoin-ui)"
SPEC_TIER="4"
SPEC_LOCAL_IMAGE="true"
SPEC_CAPS="CHOWN SETUID SETGID"
SPEC_SECURITY="no-new-privileges:true"
}
load_spec_archy-lnd-ui() {
reset_spec
SPEC_NAME="archy-lnd-ui"
SPEC_IMAGE="localhost/lnd-ui:local"
SPEC_PORTS="18083:80"
SPEC_MEMORY="$(mem_limit archy-lnd-ui)"
SPEC_TIER="4"
SPEC_LOCAL_IMAGE="true"
SPEC_CAPS="CHOWN SETUID SETGID NET_BIND_SERVICE"
SPEC_SECURITY="no-new-privileges:true"
}
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="CHOWN SETUID SETGID"
SPEC_SECURITY="no-new-privileges:true"
}
# ── 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
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[@]}"
}