archy/scripts/first-boot-containers.sh
Dorian 7741dc8652 feat: ISO networking stack — relay + nvpn v0.3.7 + WireGuard
Add nostr-rs-relay as native system service (port 7777) for VPN
signaling. Every node runs its own private relay from first boot.
Update nvpn binary from v0.3.4 to v0.3.7 (fixes mesh event
processing). Add WireGuard helper and address service for peer VPN.
First-boot script configures relay, nvpn identity, relay URLs
(direct + Tor onion), and syncs daemon config.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 15:06:27 +02:00

1281 lines
61 KiB
Bash

#!/bin/bash
#
# First-boot container creation for Archipelago autoinstaller
# Creates core containers so My Apps works out of the box after ISO install
# Runs after archipelago-load-images.service and archipelago-setup-tor.service
#
# Based on scripts/deploy-to-target.sh (--live) container logic - do not diverge.
# No set -e: each section continues even if one fails (idempotent, best-effort).
#
# Image versions: sourced from /opt/archipelago/image-versions.sh (single source of truth).
# All container image references use the $*_IMAGE variables defined there.
# Images pull from the Archipelago app registry (80.71.235.15:3000/archipelago/).
#
# --- PLANNED REFACTOR (post-beta) ---
# This script is ~995 lines and should be split into a modular library.
# DO NOT split until tested on the build server — this is critical infrastructure.
#
LOG="/var/log/archipelago-first-boot.log"
# Source pinned image versions (single source of truth)
# ISO copies to scripts/ subdir; also check the direct path for manual installs
source /opt/archipelago/scripts/image-versions.sh 2>/dev/null \
|| source /opt/archipelago/image-versions.sh 2>/dev/null \
|| source /home/archipelago/archy/scripts/image-versions.sh 2>/dev/null \
|| true
# Verify image-versions loaded — fail loudly if not
if [ -z "$ARCHY_REGISTRY" ] || [ -z "$BITCOIN_KNOTS_IMAGE" ]; then
log "FATAL: image-versions.sh not loaded — checked:"
log " /opt/archipelago/scripts/image-versions.sh"
log " /opt/archipelago/image-versions.sh"
log " /home/archipelago/archy/scripts/image-versions.sh"
log "Container creation will fail. Check ISO build."
fi
# Source shared utility library
SCRIPT_DIR_FBC="$(cd "$(dirname "$0")" && pwd)"
[ -f "$SCRIPT_DIR_FBC/lib/common.sh" ] && source "$SCRIPT_DIR_FBC/lib/common.sh" || true
# Must run as root for system setup (sysctl, loginctl, subuid, chown).
# Podman commands run as the archipelago user (rootless) so the backend
# (which also runs as archipelago) can see and manage the containers.
[ "$(id -u)" -eq 0 ] || { echo "Must run as root" >&2; exit 1; }
# Run podman as the archipelago user (rootless) — NOT as root.
# The backend service runs as User=archipelago and connects to the rootless
# podman socket at /run/user/$(id -u archipelago)/podman/podman.sock. If we create containers
# as root (rootful podman), the backend can't see them at all.
DOCKER="runuser -u archipelago -- env XDG_RUNTIME_DIR=/run/user/$(id -u archipelago) podman"
TARGET_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
[ -z "$TARGET_IP" ] && TARGET_IP="127.0.0.1"
# Resolve host-gateway for --add-host (podman 4.3.x doesn't support "host-gateway")
# Use the default gateway IP from the podman network, falling back to host LAN IP
HOST_GATEWAY=$(ip route show default 2>/dev/null | awk '/default/ {print $3}' | head -1)
[ -z "$HOST_GATEWAY" ] && HOST_GATEWAY="$TARGET_IP"
ADD_HOST_FLAG="--add-host=host.containers.internal:${HOST_GATEWAY}"
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $*" | tee -a "$LOG"; }
# Ensure Tor is running for hidden services (LND connect, Electrumx, etc.)
if ! systemctl is-active tor >/dev/null 2>&1; then
log "Starting Tor..."
systemctl enable tor 2>/dev/null || true
systemctl start tor 2>/dev/null || true
sleep 5
if systemctl is-active tor >/dev/null 2>&1; then
log " Tor started successfully"
else
log " WARNING: Tor failed to start, hidden services will be unavailable"
fi
fi
# Populate tor-hostnames directory so the backend can read onion addresses
# The backend reads from /var/lib/archipelago/tor-hostnames/{service} at startup
TOR_HOSTNAMES="/var/lib/archipelago/tor-hostnames"
mkdir -p "$TOR_HOSTNAMES"
for svc in archipelago bitcoin lnd electrumx btcpay mempool fedimint; do
for dir in /var/lib/tor/hidden_service_${svc}; do
if [ -f "$dir/hostname" ]; then
cp "$dir/hostname" "$TOR_HOSTNAMES/$svc" 2>/dev/null
fi
done
done
chown -R archipelago:archipelago "$TOR_HOSTNAMES" 2>/dev/null
log "Tor hostnames populated: $(ls $TOR_HOSTNAMES 2>/dev/null | tr '\n' ' ')"
# ── Private Nostr Relay: start for VPN signaling and general use ──────
if command -v nostr-rs-relay >/dev/null 2>&1; then
# Relay config is pre-installed by ISO at /var/lib/archipelago/nostr-relay/config.toml
mkdir -p /var/lib/archipelago/nostr-relay
if [ ! -f /var/lib/archipelago/nostr-relay/config.toml ] && [ -f /etc/archipelago/nostr-relay-config.toml ]; then
cp /etc/archipelago/nostr-relay-config.toml /var/lib/archipelago/nostr-relay/config.toml
fi
chown -R archipelago:archipelago /var/lib/archipelago/nostr-relay
systemctl enable --now nostr-relay 2>/dev/null || true
log "Private Nostr relay started on port 7777"
else
log "nostr-rs-relay binary not found — skipping relay setup"
fi
# ── NostrVPN: configure native system service with node identity ──────
if command -v nvpn >/dev/null 2>&1; then
NOSTR_SECRET=$(cat /var/lib/archipelago/identity/nostr_secret 2>/dev/null)
NOSTR_PUBKEY=$(cat /var/lib/archipelago/identity/nostr_pubkey 2>/dev/null)
if [ -n "$NOSTR_SECRET" ]; then
# Initialize nvpn config if not already done
NVPN_CONFIG_DIR="/home/archipelago/.config/nvpn"
mkdir -p "$NVPN_CONFIG_DIR"
if [ ! -f "$NVPN_CONFIG_DIR/config.toml" ]; then
# Run nvpn init as archipelago user to generate default config
su -l archipelago -c "nvpn init" 2>/dev/null || true
fi
# Set the node's Nostr identity from onboarding seed phrase
su -l archipelago -c "nvpn set --config '$NVPN_CONFIG_DIR/config.toml'" 2>/dev/null || true
# Get server's public IP for WireGuard endpoint
HOST_IP=$(cat /var/lib/archipelago/host-ip.env 2>/dev/null | grep ARCHIPELAGO_HOST_IP | cut -d= -f2)
[ -z "$HOST_IP" ] && HOST_IP=$(curl -s --connect-timeout 5 https://api.ipify.org 2>/dev/null || hostname -I | awk '{print $1}')
# Configure nvpn with node identity and endpoint
if [ -f "$NVPN_CONFIG_DIR/config.toml" ]; then
su -l archipelago -c "nvpn set --endpoint '${HOST_IP}:51821'" 2>/dev/null || true
fi
# Add this node's own relay as a signaling relay
# Direct relay (public IP) — only if not behind NAT
if [ -n "$HOST_IP" ] && ! echo "$HOST_IP" | grep -qE '^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.)'; then
su -l archipelago -c "nvpn relay add 'ws://${HOST_IP}:7777'" 2>/dev/null || true
fi
# Tor relay (works behind NAT)
RELAY_ONION=$(cat /var/lib/archipelago/tor-hostnames/relay 2>/dev/null)
if [ -n "$RELAY_ONION" ]; then
su -l archipelago -c "nvpn relay add 'ws://${RELAY_ONION}:7777'" 2>/dev/null || true
fi
# Sync config to daemon HOME so the service finds it
# (service runs with HOME=/var/lib/archipelago/nostr-vpn)
DAEMON_CONFIG_DIR="/var/lib/archipelago/nostr-vpn/.config/nvpn"
mkdir -p "$DAEMON_CONFIG_DIR"
if [ -f "$NVPN_CONFIG_DIR/config.toml" ]; then
cp "$NVPN_CONFIG_DIR/config.toml" "$DAEMON_CONFIG_DIR/config.toml"
fi
# Ensure env file exists for the service
mkdir -p /var/lib/archipelago/nostr-vpn
cat > /var/lib/archipelago/nostr-vpn/env <<NVPNENV
NOSTR_SECRET=${NOSTR_SECRET}
NOSTR_PUBKEY=${NOSTR_PUBKEY}
NVPNENV
chmod 600 /var/lib/archipelago/nostr-vpn/env
# Load WireGuard kernel module
modprobe wireguard 2>/dev/null || true
# Start NostrVPN and WireGuard address services
systemctl enable --now nostr-vpn 2>/dev/null || true
systemctl enable --now archipelago-wg-address 2>/dev/null || true
log "NostrVPN configured with node identity and started"
else
log "NostrVPN: no Nostr identity yet — will configure after onboarding"
fi
else
log "NostrVPN binary not found — skipping VPN setup"
fi
# Wait for a container to be healthy (accepting connections)
wait_for_container() {
local name="$1" check_cmd="$2" max_wait="${3:-30}"
local waited=0
while [ $waited -lt $max_wait ]; do
if eval "$check_cmd" 2>/dev/null; then
log " $name is ready (${waited}s)"
return 0
fi
sleep 2
waited=$((waited + 2))
done
log " WARNING: $name not ready after ${max_wait}s, continuing anyway"
return 1
}
# rpcauth: password hash in bitcoin.conf, plaintext in secrets file only.
# Credentials are STABLE across reboots, restarts, and deploys.
SECRETS_DIR="/var/lib/archipelago/secrets"
mkdir -p "$SECRETS_DIR" && chmod 700 "$SECRETS_DIR"
if [ ! -f "$SECRETS_DIR/bitcoin-rpc-password" ]; then
openssl rand -hex 16 > "$SECRETS_DIR/bitcoin-rpc-password"
chmod 600 "$SECRETS_DIR/bitcoin-rpc-password"
fi
BITCOIN_RPC_USER="archipelago"
BITCOIN_RPC_PASS=$(cat "$SECRETS_DIR/bitcoin-rpc-password" 2>/dev/null)
if [ -z "$BITCOIN_RPC_PASS" ]; then
log "FATAL: Bitcoin RPC password is empty — secrets file missing or unreadable"
log " Expected: $SECRETS_DIR/bitcoin-rpc-password"
exit 1
fi
# Generate rpcauth line for bitcoin.conf (salted HMAC-SHA256 hash)
generate_rpcauth() {
local user="$1" pass="$2"
local salt=$(openssl rand -hex 16)
local hash=$(echo -n "$pass" | openssl dgst -sha256 -hmac "$salt" -hex 2>/dev/null | awk '{print $NF}')
echo "${user}:${salt}\$${hash}"
}
# Write bitcoin.conf with rpcauth if not exists or needs update
BITCOIN_CONF="/var/lib/archipelago/bitcoin/bitcoin.conf"
if [ ! -f "$BITCOIN_CONF" ] || ! grep -q "^rpcauth=" "$BITCOIN_CONF" 2>/dev/null; then
mkdir -p /var/lib/archipelago/bitcoin
RPCAUTH=$(generate_rpcauth "$BITCOIN_RPC_USER" "$BITCOIN_RPC_PASS")
cat > "$BITCOIN_CONF" << BTCCONF
# rpcauth: salted hash only — no plaintext password in config or CLI
rpcauth=${RPCAUTH}
server=1
rpcbind=0.0.0.0
rpcallowip=0.0.0.0/0
rpcport=8332
listen=1
printtoconsole=1
# ZMQ publishers for LND and other services that need real-time block/tx notifications
zmqpubrawblock=tcp://0.0.0.0:28332
zmqpubrawtx=tcp://0.0.0.0:28333
BTCCONF
log "Generated bitcoin.conf with rpcauth (no plaintext credentials)"
fi
# Generate per-installation database passwords if not already saved
for svc in mempool btcpay mysql-root; do
if [ ! -f "$SECRETS_DIR/${svc}-db-password" ]; then
openssl rand -base64 24 > "$SECRETS_DIR/${svc}-db-password"
chmod 600 "$SECRETS_DIR/${svc}-db-password"
fi
done
MEMPOOL_DB_PASS=$(cat "$SECRETS_DIR/mempool-db-password")
BTCPAY_DB_PASS=$(cat "$SECRETS_DIR/btcpay-db-password")
MYSQL_ROOT_PASS=$(cat "$SECRETS_DIR/mysql-root-db-password")
# Generate Fedimint gateway password and bcrypt hash
if [ ! -f "$SECRETS_DIR/fedimint-gateway-password" ]; then
FEDI_PASS=$(openssl rand -base64 16)
echo "$FEDI_PASS" > "$SECRETS_DIR/fedimint-gateway-password"
chmod 600 "$SECRETS_DIR/fedimint-gateway-password"
# Pre-compute bcrypt hash (requires htpasswd from apache2-utils)
if command -v htpasswd >/dev/null 2>&1; then
htpasswd -bnBC 10 "" "$FEDI_PASS" | tr -d ':\n' > "$SECRETS_DIR/fedimint-gateway-hash"
chmod 600 "$SECRETS_DIR/fedimint-gateway-hash"
fi
fi
FEDI_PASS=$(cat "$SECRETS_DIR/fedimint-gateway-password")
if [ -f "$SECRETS_DIR/fedimint-gateway-hash" ]; then
FEDI_HASH=$(cat "$SECRETS_DIR/fedimint-gateway-hash")
else
# Fallback: generate hash now
if command -v htpasswd >/dev/null 2>&1; then
FEDI_HASH=$(htpasswd -bnBC 10 "" "$FEDI_PASS" | tr -d ':\n')
echo "$FEDI_HASH" > "$SECRETS_DIR/fedimint-gateway-hash"
chmod 600 "$SECRETS_DIR/fedimint-gateway-hash"
else
log "WARNING: htpasswd not found, using default Fedimint gateway hash"
FEDI_HASH='$2y$10$t9YjjxkiktrlYvjajB/zgOMDnSNVg4HqrbDqh47u7Jf42whNdxNqC'
fi
fi
log "Fedimint gateway password stored in $SECRETS_DIR/fedimint-gateway-password"
BITCOIN_READY=false
TOTAL=0
SUCCESS=0
FAILED_LIST=""
# Track container start result — call after each container creation attempt
track_container() {
local name="$1"
TOTAL=$((TOTAL + 1))
if $DOCKER ps --filter "name=^${name}$" --format '{{.Names}}' 2>/dev/null | grep -q "^${name}$"; then
SUCCESS=$((SUCCESS + 1))
log " [OK] $name is running"
else
FAILED_LIST="$FAILED_LIST $name"
log " [FAIL] $name is NOT running"
fi
}
log "First-boot container creation starting (host=$TARGET_IP)"
# Create swap file if not present (50% of RAM, min 2GB, max 8GB)
if ! swapon --show | grep -q /swapfile; then
TOTAL_MEM_KB=$(awk '/MemTotal/ {print $2}' /proc/meminfo)
SWAP_MB=$((TOTAL_MEM_KB / 2 / 1024))
[ "$SWAP_MB" -lt 2048 ] && SWAP_MB=2048
[ "$SWAP_MB" -gt 8192 ] && SWAP_MB=8192
log "Creating ${SWAP_MB}MB swap file..."
if dd if=/dev/zero of=/swapfile bs=1M count="$SWAP_MB" status=progress 2>>"$LOG"; then
chmod 600 /swapfile
mkswap /swapfile >>"$LOG" 2>&1
swapon /swapfile
if ! grep -q '/swapfile' /etc/fstab; then
echo '/swapfile none swap sw 0 0' >> /etc/fstab
fi
log "Swap created: ${SWAP_MB}MB"
else
log "WARNING: Failed to create swap file"
fi
else
log "Swap already configured"
fi
# Rootless podman prerequisites (run as root, configures for archipelago user)
log "Setting up rootless podman prerequisites..."
# Allow binding to ports >= 80 (rootless default is 1024)
if ! grep -q "unprivileged_port_start=80" /etc/sysctl.d/99-rootless-podman.conf 2>/dev/null; then
echo "net.ipv4.ip_unprivileged_port_start=80" > /etc/sysctl.d/99-rootless-podman.conf
sysctl -p /etc/sysctl.d/99-rootless-podman.conf 2>/dev/null
log " Rootless port binding enabled (>=80)"
fi
# Linger for container persistence after logout
if [ "$(loginctl show-user archipelago 2>/dev/null | grep Linger)" != "Linger=yes" ]; then
loginctl enable-linger archipelago 2>/dev/null
log " Linger enabled for archipelago user"
fi
# Ensure subuid/subgid mappings
grep -q "^archipelago:" /etc/subuid 2>/dev/null || {
echo "archipelago:100000:65536" >> /etc/subuid
echo "archipelago:100000:65536" >> /etc/subgid
log " subuid/subgid configured"
}
# Ensure /etc/hosts is readable (rootless podman needs it)
chmod 644 /etc/hosts 2>/dev/null
# Ensure XDG_RUNTIME_DIR exists for rootless podman
mkdir -p /run/user/$(id -u archipelago)
chown archipelago:archipelago /run/user/$(id -u archipelago)
chmod 700 /run/user/$(id -u archipelago)
# Start rootless podman socket (required before first podman command)
runuser -u archipelago -- env XDG_RUNTIME_DIR=/run/user/$(id -u archipelago) \
systemctl --user start podman.socket 2>/dev/null || true
# Ensure archy-net exists — critical for inter-container DNS (mempool→bitcoin, etc.)
$DOCKER network create archy-net 2>/dev/null || true
if ! $DOCKER network exists archy-net 2>/dev/null; then
log "WARNING: archy-net creation failed, retrying in 5s..."
sleep 5
$DOCKER network create archy-net 2>>"$LOG"
if ! $DOCKER network exists archy-net 2>/dev/null; then
log "FATAL: Cannot create archy-net — inter-container DNS will not work."
log " All containers requiring archy-net will fail. Exiting."
exit 1
fi
fi
log "archy-net network ready"
# Rootless podman UID mapping: fix data dir ownership so container processes
# can write. Rootless podman maps container UIDs via subuid (container UID N
# → host UID 100000+N). Must run BEFORE container creation.
log "Fixing rootless podman UID mapping..."
# Containers running as root (UID 0 → host UID 100000)
for dir in lnd electrumx btcpay nbxplorer jellyfin vaultwarden \
home-assistant fedimint fedimint-gateway photoprism ollama filebrowser \
nextcloud uptime-kuma nginx-proxy-manager portainer nostr-rs-relay; do
[ -d "/var/lib/archipelago/$dir" ] && chown -R 100000:100000 "/var/lib/archipelago/$dir" 2>/dev/null
done
# Bitcoin Knots: container UID 101 → host UID 100101
[ -d /var/lib/archipelago/bitcoin ] && chown -R 100101:100101 /var/lib/archipelago/bitcoin 2>/dev/null
# Postgres: container UID 70 → host UID 100070
for dir in postgres-btcpay; do
[ -d "/var/lib/archipelago/$dir" ] && chown -R 100070:100070 "/var/lib/archipelago/$dir" 2>/dev/null
done
# MariaDB: container UID 999 → host UID 100999
for dir in mempool mysql-mempool; do
[ -d "/var/lib/archipelago/$dir" ] && chown -R 100999:100999 "/var/lib/archipelago/$dir" 2>/dev/null
done
# Grafana: container UID 472 → host UID 100472
[ -d /var/lib/archipelago/grafana ] && chown -R 100472:100472 /var/lib/archipelago/grafana 2>/dev/null
log "UID mapping done"
# ── Memory limits per container ──────────────────────────────────────────
# Matches core/archipelago/src/api/rpc/package.rs get_memory_limit()
# Prevents a single runaway container from OOMing the whole system.
TOTAL_MEM_MB=$(($(awk '/MemTotal/{print $2}' /proc/meminfo) / 1024))
LOW_MEM=false
[ "$TOTAL_MEM_MB" -lt 12000 ] && LOW_MEM=true && log "Low-memory system (${TOTAL_MEM_MB}MB) — reducing limits"
mem_limit() {
case "$1" in
bitcoin-knots) $LOW_MEM && echo "2g" || echo "4g";;
cryptpad) echo "512m";;
ollama) $LOW_MEM && echo "1g" || echo "4g";;
lnd) echo "512m";;
electrumx) echo "1g";;
nextcloud) echo "1g";;
btcpay-server) echo "1g";;
homeassistant) echo "512m";;
fedimint) echo "512m";;
fedimint-gateway) echo "512m";;
photoprism) $LOW_MEM && echo "512m" || echo "1g";;
mempool-api) echo "512m";;
jellyfin) echo "1g";;
searxng) echo "512m";;
archy-btcpay-db) echo "512m";;
archy-nbxplorer) echo "512m";;
archy-mempool-db) echo "512m";;
archy-mempool-web) echo "256m";;
grafana) echo "256m";;
vaultwarden) echo "256m";;
uptime-kuma) echo "256m";;
filebrowser) echo "256m";;
portainer) echo "256m";;
nginx-proxy-manager) echo "256m";;
tailscale) echo "256m";;
indeedhub|archy-bitcoin-ui|archy-lnd-ui|archy-electrs-ui) echo "128m";;
*) echo "512m";;
esac
}
# ── Verify critical images are loaded ──────────────────────────────────
# archipelago-load-images.service should have loaded these from tarballs.
# If any are missing (corrupt tarball, disk full, etc.), try re-loading.
log "Verifying container images..."
MISSING_IMAGES=""
for img_var in BITCOIN_KNOTS_IMAGE MARIADB_IMAGE ELECTRUMX_IMAGE \
MEMPOOL_BACKEND_IMAGE MEMPOOL_WEB_IMAGE BTCPAY_POSTGRES_IMAGE \
NBXPLORER_IMAGE BTCPAY_IMAGE LND_IMAGE FEDIMINT_IMAGE \
FEDIMINT_GATEWAY_IMAGE HOMEASSISTANT_IMAGE GRAFANA_IMAGE \
UPTIME_KUMA_IMAGE JELLYFIN_IMAGE VAULTWARDEN_IMAGE \
NEXTCLOUD_IMAGE SEARXNG_IMAGE FILEBROWSER_IMAGE; do
img="${!img_var}"
if [ -z "$img" ]; then
continue # Variable not defined in image-versions.sh
fi
if ! $DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -qF "$img"; then
MISSING_IMAGES="$MISSING_IMAGES $img_var"
fi
done
if [ -n "$MISSING_IMAGES" ]; then
log "WARNING: Missing images:$MISSING_IMAGES"
log "Attempting to re-load from /opt/archipelago/container-images/..."
RELOAD_COUNT=0
for tarfile in /opt/archipelago/container-images/*.tar; do
if [ -f "$tarfile" ]; then
if $DOCKER load -i "$tarfile" 2>>"$LOG"; then
RELOAD_COUNT=$((RELOAD_COUNT + 1))
else
log " Failed to load: $tarfile"
fi
fi
done
log "Re-loaded $RELOAD_COUNT image tarballs"
else
log "All critical images verified"
fi
# ── Tier 1: Databases & Core Infrastructure ──────────────────────────────
log "=== Tier 1: Databases & Core Infrastructure ==="
# 1. Bitcoin Knots (matches deploy exactly)
# Auto-detect: if disk < 1TB, use pruning to prevent disk-full crashes
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE 'bitcoin-knots|archy-bitcoin-knots'; then
log "Creating Bitcoin Knots..."
mkdir -p /var/lib/archipelago/bitcoin
# Check the DATA partition size, not root — Bitcoin data goes to /var/lib/archipelago
DISK_GB=$(df --output=size -BG /var/lib/archipelago 2>/dev/null | tail -1 | tr -dc '0-9')
[ -z "$DISK_GB" ] && DISK_GB=$(df --output=size -BG / 2>/dev/null | tail -1 | tr -dc '0-9')
if [ "${DISK_GB:-0}" -lt 1000 ]; then
BTC_EXTRA_ARGS="-prune=550"
BTC_DBCACHE=512
log " Small disk (${DISK_GB}GB) — enabling pruning"
else
BTC_EXTRA_ARGS="-txindex=1"
BTC_DBCACHE=2048
log " Large disk (${DISK_GB}GB) — enabling txindex"
fi
if $DOCKER run -d --name bitcoin-knots --restart unless-stopped \
--health-cmd="bitcoin-cli -datadir=/home/bitcoin/.bitcoin getblockchaininfo || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit bitcoin-knots) --network archy-net --network-alias bitcoin-knots \
$ADD_HOST_FLAG \
--cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
--security-opt no-new-privileges:true \
-p 8332:8332 -p 8333:8333 -p 28332:28332 -p 28333:28333 \
-v /var/lib/archipelago/bitcoin:/home/bitcoin/.bitcoin \
"${BITCOIN_KNOTS_IMAGE}" \
$BTC_EXTRA_ARGS \
-printtoconsole=1 -dbcache=$BTC_DBCACHE 2>>"$LOG"; then
log "Bitcoin Knots started"
else
log "Bitcoin Knots failed (may already exist)"
fi
else
$DOCKER network connect archy-net bitcoin-knots 2>/dev/null || true
log "Bitcoin Knots already running"
fi
# Check Bitcoin Knots RPC (informational — containers created regardless)
# Dependent containers use --restart=unless-stopped and the health monitor
# will auto-restart them once Bitcoin becomes responsive.
if wait_for_container "Bitcoin Knots RPC" "$DOCKER exec bitcoin-knots bitcoin-cli -datadir=/home/bitcoin/.bitcoin getblockchaininfo" 60; then
BITCOIN_READY=true
log "Bitcoin Knots is ready"
else
BITCOIN_READY=false
log "Bitcoin Knots not yet responsive (normal during IBD) — creating dependent containers anyway"
log " They will auto-restart via health monitor once Bitcoin is ready"
fi
track_container "bitcoin-knots"
# Ensure wallet exists (Bitcoin Knots no longer auto-creates a default wallet)
if ! $DOCKER exec bitcoin-knots bitcoin-cli -datadir=/home/bitcoin/.bitcoin listwallets 2>/dev/null | grep -q "archipelago"; then
$DOCKER exec bitcoin-knots bitcoin-cli -datadir=/home/bitcoin/.bitcoin loadwallet "archipelago" 2>/dev/null || \
$DOCKER exec bitcoin-knots bitcoin-cli -datadir=/home/bitcoin/.bitcoin createwallet "archipelago" 2>/dev/null
log "Bitcoin Knots wallet 'archipelago' created/loaded"
fi
# ── Bootstrap: use a remote Bitcoin node during IBD ───────────────────
# If the local node is still syncing (IBD=true), point dependent services at
# a fully-synced bootstrap node so wallets/payments work immediately.
BOOTSTRAP_CONF="/opt/archipelago/bootstrap.conf"
BOOTSTRAP_FLAG="/var/lib/archipelago/.bootstrap-active"
USE_BOOTSTRAP=false
BTC_HOST="bitcoin-knots" # default: local container via archy-net DNS
BTC_RPC_USER="$BITCOIN_RPC_USER"
BTC_RPC_PASS="$BITCOIN_RPC_PASS"
if [ -f "$BOOTSTRAP_CONF" ]; then
. "$BOOTSTRAP_CONF"
if [ -n "${BOOTSTRAP_RPC_PASS:-}" ]; then
# Check if local Bitcoin is in IBD
LOCAL_IBD=$($DOCKER exec bitcoin-knots bitcoin-cli -datadir=/home/bitcoin/.bitcoin getblockchaininfo 2>/dev/null \
| python3 -c "import sys,json; print(json.load(sys.stdin).get('initialblockdownload',True))" 2>/dev/null) || LOCAL_IBD="True"
if [ "$LOCAL_IBD" = "True" ]; then
BOOT_USER="${BOOTSTRAP_RPC_USER:-archipelago}"
BOOT_TEST='{"jsonrpc":"1.0","id":"boot","method":"getblockcount","params":[]}'
# Try 1: LAN (fast, ~1ms)
if [ -n "${BOOTSTRAP_LAN_HOST:-}" ] && \
curl -sf --max-time 5 -u "${BOOT_USER}:${BOOTSTRAP_RPC_PASS}" \
-H "Content-Type: application/json" -d "$BOOT_TEST" \
"http://${BOOTSTRAP_LAN_HOST}:8332/" >/dev/null 2>&1; then
USE_BOOTSTRAP=true
BTC_HOST="$BOOTSTRAP_LAN_HOST"
BTC_RPC_USER="$BOOT_USER"
BTC_RPC_PASS="$BOOTSTRAP_RPC_PASS"
touch "$BOOTSTRAP_FLAG"
echo "lan" > "$BOOTSTRAP_FLAG"
log "BOOTSTRAP: Local Bitcoin in IBD — using LAN ${BOOTSTRAP_LAN_HOST} for dependent services"
# Try 2: Tor (works from any network, ~5-15s)
elif [ -n "${BOOTSTRAP_ONION:-}" ] && command -v socat >/dev/null 2>&1; then
log "BOOTSTRAP: LAN unreachable, trying Tor (${BOOTSTRAP_ONION})..."
# Create a socat tunnel: localhost:18332 → onion:8332 via Tor SOCKS
socat TCP-LISTEN:18332,bind=127.0.0.1,reuseaddr,fork \
SOCKS4A:127.0.0.1:${BOOTSTRAP_ONION}:8332,socksport=9050 &
SOCAT_PID=$!
sleep 3
if curl -sf --max-time 30 -u "${BOOT_USER}:${BOOTSTRAP_RPC_PASS}" \
-H "Content-Type: application/json" -d "$BOOT_TEST" \
"http://127.0.0.1:18332/" >/dev/null 2>&1; then
USE_BOOTSTRAP=true
# Containers reach host via host.containers.internal (set by $ADD_HOST_FLAG)
BTC_HOST="${HOST_GATEWAY:-$TARGET_IP}"
BTC_HOST_PORT=18332
BTC_RPC_USER="$BOOT_USER"
BTC_RPC_PASS="$BOOTSTRAP_RPC_PASS"
echo "tor:$SOCAT_PID" > "$BOOTSTRAP_FLAG"
log "BOOTSTRAP: Using Tor tunnel (socat pid=$SOCAT_PID) for dependent services"
# Persist the tunnel as a systemd service so it survives first-boot
cat > /etc/systemd/system/archipelago-bootstrap-tunnel.service <<TUNNELSVC
[Unit]
Description=Bootstrap Bitcoin RPC tunnel via Tor
After=tor.service
[Service]
Type=simple
User=archipelago
ExecStart=/usr/bin/socat TCP-LISTEN:18332,bind=127.0.0.1,reuseaddr,fork SOCKS4A:127.0.0.1:${BOOTSTRAP_ONION}:8332,socksport=9050
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
TUNNELSVC
systemctl daemon-reload
systemctl enable --now archipelago-bootstrap-tunnel.service 2>/dev/null || true
# Kill the ad-hoc socat — systemd takes over
kill "$SOCAT_PID" 2>/dev/null || true
else
kill "$SOCAT_PID" 2>/dev/null || true
log "BOOTSTRAP: Tor tunnel test failed — using local Bitcoin"
fi
else
log "BOOTSTRAP: No reachable bootstrap node — using local Bitcoin"
fi
if [ "$USE_BOOTSTRAP" = "true" ]; then
log " Services will auto-switch to local node when synced (bootstrap-switchover timer)"
fi
else
log "BOOTSTRAP: Local Bitcoin already synced — no bootstrap needed"
fi
fi
fi
# Override port if Tor tunnel is active (containers use host gateway:18332 instead of :8332)
BTC_PORT=${BTC_HOST_PORT:-8332}
# 2. Mempool stack (matches deploy) — depends on Bitcoin
# Note: containers created regardless of BITCOIN_READY — they will restart
# automatically once Bitcoin becomes responsive (--restart=unless-stopped).
if ! $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -qE 'archy-mempool-db|mysql-mempool'; then
log "Creating mysql-mempool..."
mkdir -p /var/lib/archipelago/mysql-mempool
$DOCKER run -d --name archy-mempool-db --restart unless-stopped \
--health-cmd="mariadb -uroot -e 'SELECT 1' || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit archy-mempool-db) --network archy-net --network-alias archy-mempool-db \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
--security-opt no-new-privileges:true \
-v /var/lib/archipelago/mysql-mempool:/var/lib/mysql \
-e MYSQL_DATABASE=mempool -e MYSQL_USER=mempool -e "MYSQL_PASSWORD=$MEMPOOL_DB_PASS" \
-e "MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASS" \
"$MARIADB_IMAGE" 2>>"$LOG" || true
wait_for_container "Mempool MariaDB" "echo 'SELECT 1' | $DOCKER exec -i archy-mempool-db mariadb -uroot --password=\"$MYSQL_ROOT_PASS\"" 30
fi
MYSQL_CNT=$($DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -E 'mysql-mempool|archy-mempool-db' | head -1)
MYSQL_CNT=${MYSQL_CNT:-archy-mempool-db}
$DOCKER network connect archy-net "$MYSQL_CNT" 2>/dev/null || true
track_container "archy-mempool-db"
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q electrumx; then
if $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -q electrumx; then
$DOCKER start electrumx 2>/dev/null || true
else
log "Creating electrumx..."
mkdir -p /var/lib/archipelago/electrumx
$DOCKER run -d --name electrumx --restart unless-stopped \
--health-cmd="curl -sf http://localhost:8000/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit electrumx) --network archy-net --network-alias electrumx \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
-p 50001:50001 -v /var/lib/archipelago/electrumx:/data \
-e "DAEMON_URL=http://$BTC_RPC_USER:$BTC_RPC_PASS@$BTC_HOST:$BTC_PORT/" \
-e COIN=Bitcoin -e DB_DIRECTORY=/data \
-e SERVICES=tcp://:50001,rpc://0.0.0.0:8000 \
"$ELECTRUMX_IMAGE" 2>>"$LOG" || true
fi
fi
track_container "electrumx"
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q mempool-api; then
log "Creating mempool-api..."
mkdir -p /var/lib/archipelago/mempool
$DOCKER run -d --name mempool-api --restart unless-stopped \
--health-cmd="curl -sf http://localhost:8999/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit mempool-api) --network archy-net --network-alias mempool-api \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
-p 8999:8999 -v /var/lib/archipelago/mempool:/data \
-e MEMPOOL_BACKEND=electrum -e ELECTRUM_HOST=electrumx -e ELECTRUM_PORT=50001 \
-e ELECTRUM_TLS_ENABLED=false -e "CORE_RPC_HOST=$BTC_HOST" -e CORE_RPC_PORT=8332 \
-e "CORE_RPC_USERNAME=$BTC_RPC_USER" -e "CORE_RPC_PASSWORD=$BTC_RPC_PASS" \
-e DATABASE_ENABLED=true -e DATABASE_HOST="$MYSQL_CNT" -e DATABASE_DATABASE=mempool \
-e DATABASE_USERNAME=mempool -e "DATABASE_PASSWORD=$MEMPOOL_DB_PASS" \
"$MEMPOOL_BACKEND_IMAGE" 2>>"$LOG" || true
fi
track_container "mempool-api"
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE 'archy-mempool-web|mempool-web'; then
log "Creating mempool frontend..."
$DOCKER run -d --name archy-mempool-web --restart unless-stopped \
--health-cmd="curl -sf http://localhost:8080/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit archy-mempool-web) --network archy-net --network-alias archy-mempool-web \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
-p 4080:8080 -e FRONTEND_HTTP_PORT=8080 -e BACKEND_MAINNET_HTTP_HOST=mempool-api \
"$MEMPOOL_WEB_IMAGE" 2>>"$LOG" || true
fi
track_container "archy-mempool-web"
# 2b. ElectrumX UI (status dashboard on port 50002, host network for backend access)
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q electrs-ui; then
if $DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q 'electrs-ui'; then
log "Starting ElectrumX UI from pre-built image..."
$DOCKER run -d --name archy-electrs-ui --network host --restart unless-stopped \
--health-cmd="curl -sf http://localhost:80/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--user 0:0 \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
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=120s --health-timeout=5s --health-retries=3 \
--user 0:0 \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
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: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=120s --health-timeout=5s --health-retries=3 \
--user 0:0 \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
electrs-ui:local 2>>"$LOG" || true
else
log "ElectrumX UI: no image or source found, skipping"
fi
fi
# 3. BTCPay stack (matches deploy) — depends on Bitcoin
if ! $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -qE 'archy-btcpay-db|postgres-btcpay'; then
log "Creating PostgreSQL for BTCPay..."
mkdir -p /var/lib/archipelago/postgres-btcpay
$DOCKER run -d --name archy-btcpay-db --restart unless-stopped \
--health-cmd="pg_isready -U postgres || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit archy-btcpay-db) --network archy-net --network-alias archy-btcpay-db \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
--security-opt no-new-privileges:true \
-v /var/lib/archipelago/postgres-btcpay:/var/lib/postgresql/data \
-e POSTGRES_DB=btcpay -e POSTGRES_USER=btcpay -e "POSTGRES_PASSWORD=$BTCPAY_DB_PASS" \
"$BTCPAY_POSTGRES_IMAGE" 2>>"$LOG" || true
wait_for_container "BTCPay PostgreSQL" "$DOCKER exec archy-btcpay-db pg_isready -U postgres" 30
fi
track_container "archy-btcpay-db"
# Create nbxplorer DB only if postgres is running
if $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE 'archy-btcpay-db|postgres-btcpay'; then
$DOCKER exec archy-btcpay-db psql -U postgres -tc "SELECT 1 FROM pg_database WHERE datname='nbxplorer'" 2>/dev/null | grep -q 1 || \
$DOCKER exec -e "PGPASSWORD=$BTCPAY_DB_PASS" archy-btcpay-db psql -U postgres -c "CREATE DATABASE nbxplorer;" 2>/dev/null || true
fi
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q archy-nbxplorer; then
if $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -q archy-nbxplorer; then
$DOCKER start archy-nbxplorer 2>/dev/null || true
else
log "Creating NBXplorer..."
mkdir -p /var/lib/archipelago/nbxplorer/Main
$DOCKER run -d --name archy-nbxplorer --restart unless-stopped \
--health-cmd="curl -sf http://localhost:32838/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit archy-nbxplorer) --network archy-net --network-alias archy-nbxplorer \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
-p 32838:32838 -v /var/lib/archipelago/nbxplorer:/data \
-e NBXPLORER_DATADIR=/data -e NBXPLORER_NETWORK=mainnet -e NBXPLORER_CHAINS=btc \
-e NBXPLORER_BIND=0.0.0.0:32838 -e "NBXPLORER_BTCRPCURL=http://$BTC_HOST:$BTC_PORT" \
-e "NBXPLORER_BTCRPCUSER=$BTC_RPC_USER" -e "NBXPLORER_BTCRPCPASSWORD=$BTC_RPC_PASS" \
-e NBXPLORER_POSTGRES='User ID=btcpay;Password=$BTCPAY_DB_PASS;Host=archy-btcpay-db;Port=5432;Database=nbxplorer;Include Error Detail=true' \
"$NBXPLORER_IMAGE" 2>>"$LOG" && sleep 5 || true
fi
fi
track_container "archy-nbxplorer"
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q btcpay-server; then
log "Creating BTCPay Server..."
mkdir -p /var/lib/archipelago/btcpay/Main
$DOCKER run -d --name btcpay-server --restart unless-stopped \
--health-cmd="curl -sf http://localhost:49392/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit btcpay-server) --network archy-net --network-alias btcpay-server \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
--security-opt no-new-privileges:true \
-p 23000:49392 -v /var/lib/archipelago/btcpay:/datadir \
-e ASPNETCORE_URLS=http://0.0.0.0:49392 -e BTCPAY_PROTOCOL=http \
-e BTCPAY_HOST="$TARGET_IP:23000" -e BTCPAY_CHAINS=btc \
-e BTCPAY_BTCEXPLORERURL=http://archy-nbxplorer:32838 \
-e "BTCPAY_BTCRPCURL=http://$BTC_HOST:$BTC_PORT" \
-e "BTCPAY_BTCRPCUSER=$BTC_RPC_USER" -e "BTCPAY_BTCRPCPASSWORD=$BTC_RPC_PASS" \
-e BTCPAY_POSTGRES='User ID=btcpay;Password=$BTCPAY_DB_PASS;Host=archy-btcpay-db;Port=5432;Database=btcpay;Include Error Detail=true' \
"$BTCPAY_IMAGE" 2>>"$LOG" || true
fi
track_container "btcpay-server"
# ── Tier 2: Core Services ─────────────────────────────────────────────────
log "=== Tier 2: Core Services ==="
sleep 5 # Let databases stabilize
# 4. LND — depends on Bitcoin
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE '^lnd$'; then
log "Creating LND..."
mkdir -p /var/lib/archipelago/lnd
# Create lnd.conf with rpcauth credentials (stable across restarts)
if [ ! -f /var/lib/archipelago/lnd/lnd.conf ]; then
cat > /var/lib/archipelago/lnd/lnd.conf <<LNDCONF
[Application Options]
listen=0.0.0.0:9735
rpclisten=0.0.0.0:10009
restlisten=0.0.0.0:8080
debuglevel=info
noseedbackup=true
tlsextraip=0.0.0.0
tlsextradomain=lnd
tor.active=true
tor.socks=host.containers.internal:9050
tor.streamisolation=true
[Bitcoin]
bitcoin.mainnet=true
bitcoin.node=bitcoind
[Bitcoind]
bitcoind.rpchost=$BTC_HOST:$BTC_PORT
bitcoind.rpcuser=$BTC_RPC_USER
bitcoind.rpcpass=$BTC_RPC_PASS
bitcoind.rpcpolling=true
bitcoind.estimatemode=ECONOMICAL
[autopilot]
autopilot.active=false
LNDCONF
log "LND config created (rpcauth credentials, Tor via system)"
fi
$DOCKER run -d --name lnd --restart unless-stopped \
--health-cmd="curl -sf --insecure https://localhost:8080/v1/getinfo || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit lnd) --network archy-net --network-alias lnd \
$ADD_HOST_FLAG \
--cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE --cap-add NET_RAW \
--security-opt no-new-privileges:true \
-p 9735:9735 -p 10009:10009 -p 8080:8080 \
-v /var/lib/archipelago/lnd:/root/.lnd \
"$LND_IMAGE" 2>>"$LOG" || true
fi
track_container "lnd"
# 5. Fedimint — depends on Bitcoin
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q fedimint; then
log "Creating Fedimint..."
mkdir -p /var/lib/archipelago/fedimint
chmod 775 /var/lib/archipelago/fedimint # fedimint container runs as non-root
$DOCKER run -d --name fedimint --restart unless-stopped \
--health-cmd="curl -sf http://localhost:8174/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit fedimint) --network archy-net --network-alias fedimint \
--cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
--security-opt no-new-privileges:true \
-p 8173:8173 -p 8174:8174 -p 8175:8175 \
-v /var/lib/archipelago/fedimint:/data \
-e FM_DATA_DIR=/data -e "FM_BITCOIND_USERNAME=$BTC_RPC_USER" -e "FM_BITCOIND_PASSWORD=$BTC_RPC_PASS" \
-e FM_BITCOIN_NETWORK=bitcoin -e FM_BIND_P2P=0.0.0.0:8173 \
-e FM_BIND_API=0.0.0.0:8174 -e FM_BIND_UI=0.0.0.0:8175 \
-e FM_P2P_URL=fedimint://"$TARGET_IP":8173 -e FM_API_URL=ws://"$TARGET_IP":8174 \
-e "FM_BITCOIND_URL=http://$BTC_HOST:$BTC_PORT" \
"$FEDIMINT_IMAGE" 2>>"$LOG" || true
fi
track_container "fedimint"
# 5b. Fedimint Gateway (companion to fedimint)
# Auto-detect LND: if running with credentials, use lnd mode; otherwise use ldk (built-in)
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q fedimint-gateway; then
log "Creating Fedimint Gateway..."
mkdir -p /var/lib/archipelago/fedimint-gateway
LND_CERT=/var/lib/archipelago/lnd/tls.cert
LND_MACAROON=/var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/admin.macaroon
if $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q '^lnd$' && [ -f "$LND_CERT" ] && [ -f "$LND_MACAROON" ]; then
log " LND detected — using lnd mode"
$DOCKER run -d --name fedimint-gateway --restart unless-stopped \
--health-cmd="curl -sf http://localhost:8175/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit fedimint-gateway) --network archy-net --network-alias fedimint-gateway \
--cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
--security-opt no-new-privileges:true \
-p 8176:8176 \
-v /var/lib/archipelago/fedimint-gateway:/data \
-v "$LND_CERT":/lnd/tls.cert:ro \
-v "$LND_MACAROON":/lnd/admin.macaroon:ro \
"$FEDIMINT_GATEWAY_IMAGE" \
gatewayd --data-dir /data --listen 0.0.0.0:8176 \
--bcrypt-password-hash "$FEDI_HASH" \
--network bitcoin --bitcoind-url "http://$BTC_HOST:$BTC_PORT" \
--bitcoind-username "$BTC_RPC_USER" --bitcoind-password "$BTC_RPC_PASS" \
lnd --lnd-rpc-host "$TARGET_IP":10009 --lnd-tls-cert /lnd/tls.cert --lnd-macaroon /lnd/admin.macaroon 2>>"$LOG" || true
else
log " No LND found — using ldk (built-in Lightning)"
$DOCKER run -d --name fedimint-gateway --restart unless-stopped \
--health-cmd="curl -sf http://localhost:8175/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit fedimint-gateway) --network archy-net --network-alias fedimint-gateway \
--cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
--security-opt no-new-privileges:true \
-p 8176:8176 -p 9737:9737 \
-v /var/lib/archipelago/fedimint-gateway:/data \
"$FEDIMINT_GATEWAY_IMAGE" \
gatewayd --data-dir /data --listen 0.0.0.0:8176 \
--bcrypt-password-hash "$FEDI_HASH" \
--network bitcoin --bitcoind-url "http://$BTC_HOST:$BTC_PORT" \
--bitcoind-username "$BTC_RPC_USER" --bitcoind-password "$BTC_RPC_PASS" \
ldk --ldk-lightning-port 9737 --ldk-alias archipelago-gateway 2>>"$LOG" || true
fi
fi
track_container "fedimint-gateway"
# (Bitcoin-dependent containers created above regardless of BITCOIN_READY)
# ── Tier 3: Applications (independent — always attempt) ───────────────────
log "=== Tier 3: Applications ==="
sleep 5 # Let core services stabilize
# 6. Home Assistant
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE 'homeassistant|home-assistant'; then
log "Creating Home Assistant..."
mkdir -p /var/lib/archipelago/home-assistant
$DOCKER run -d --name homeassistant --restart unless-stopped \
--health-cmd="curl -sf http://localhost:8123/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit homeassistant) \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
--security-opt no-new-privileges:true \
-p 8123:8123 -v /var/lib/archipelago/home-assistant:/config \
-e TZ=UTC \
"$HOMEASSISTANT_IMAGE" 2>>"$LOG" || true
fi
track_container "homeassistant"
# 7. Single-container apps (Grafana, Uptime Kuma, Jellyfin, PhotoPrism, Ollama, Vaultwarden)
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q grafana; then
log "Creating Grafana..."
mkdir -p /var/lib/archipelago/grafana
chown 472:472 /var/lib/archipelago/grafana 2>/dev/null || true
$DOCKER run -d --name grafana --restart unless-stopped \
--health-cmd="curl -sf http://localhost:3000/api/health || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit grafana) \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
--read-only --tmpfs /tmp:rw,noexec,nosuid,size=256m --tmpfs /run:rw,noexec,nosuid,size=64m \
-p 3000:3000 -v /var/lib/archipelago/grafana:/var/lib/grafana \
-e GF_PATHS_DATA=/var/lib/grafana -e GF_USERS_ALLOW_SIGN_UP=false \
"$GRAFANA_IMAGE" 2>>"$LOG" || true
fi
track_container "grafana"
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q uptime-kuma; then
log "Creating Uptime Kuma..."
mkdir -p /var/lib/archipelago/uptime-kuma
$DOCKER run -d --name uptime-kuma --restart unless-stopped \
--health-cmd="curl -sf http://localhost:3001/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit uptime-kuma) \
--cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
-p 3001:3001 -v /var/lib/archipelago/uptime-kuma:/app/data \
-e TZ=UTC \
"$UPTIME_KUMA_IMAGE" 2>>"$LOG" || true
fi
track_container "uptime-kuma"
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q jellyfin; then
log "Creating Jellyfin..."
mkdir -p /var/lib/archipelago/jellyfin/config /var/lib/archipelago/jellyfin/cache
$DOCKER run -d --name jellyfin --restart unless-stopped \
--health-cmd="curl -sf http://localhost:8096/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit jellyfin) \
--cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
--security-opt no-new-privileges:true \
--tmpfs /tmp:rw,exec,size=256m \
-p 8096:8096 \
-v /var/lib/archipelago/jellyfin/config:/config \
-v /var/lib/archipelago/jellyfin/cache:/cache \
"$JELLYFIN_IMAGE" 2>>"$LOG" || true
fi
track_container "jellyfin"
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q photoprism; then
log "Creating PhotoPrism..."
mkdir -p /var/lib/archipelago/photoprism
$DOCKER run -d --name photoprism --restart unless-stopped \
--health-cmd="curl -sf http://localhost:2342/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit photoprism) \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
-p 2342:2342 -v /var/lib/archipelago/photoprism:/photoprism/storage \
-e PHOTOPRISM_ADMIN_PASSWORD=archipelago -e PHOTOPRISM_DEFAULT_LOCALE=en \
"${PHOTOPRISM_IMAGE}" 2>>"$LOG" || true
fi
track_container "photoprism"
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q ollama; then
log "Creating Ollama..."
mkdir -p /var/lib/archipelago/ollama
$DOCKER run -d --name ollama --restart unless-stopped \
--health-cmd="curl -sf http://localhost:11434/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit ollama) \
--cap-drop ALL --security-opt no-new-privileges:true \
--read-only --tmpfs /tmp:rw,noexec,nosuid,size=256m --tmpfs /run:rw,noexec,nosuid,size=64m \
-p 11434:11434 -v /var/lib/archipelago/ollama:/root/.ollama \
"${OLLAMA_IMAGE}" 2>>"$LOG" || true
fi
track_container "ollama"
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q vaultwarden; then
log "Creating Vaultwarden..."
mkdir -p /var/lib/archipelago/vaultwarden
$DOCKER run -d --name vaultwarden --restart unless-stopped \
--health-cmd="curl -sf http://localhost:80/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit vaultwarden) \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add NET_BIND_SERVICE \
--security-opt no-new-privileges:true \
-p 8082:80 -v /var/lib/archipelago/vaultwarden:/data \
"$VAULTWARDEN_IMAGE" 2>>"$LOG" || true
fi
track_container "vaultwarden"
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q nextcloud; then
log "Creating Nextcloud..."
mkdir -p /var/lib/archipelago/nextcloud
$DOCKER run -d --name nextcloud --restart unless-stopped \
--health-cmd="curl -sf http://localhost:80/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit nextcloud) \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
--security-opt no-new-privileges:true \
-p 8085:80 -v /var/lib/archipelago/nextcloud:/var/www/html \
"$NEXTCLOUD_IMAGE" 2>>"$LOG" || true
fi
track_container "nextcloud"
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q searxng; then
log "Creating SearXNG..."
# SearXNG requires settings.yml or it exits immediately
SEARXNG_CONF="/var/lib/archipelago/searxng"
if [ ! -f "$SEARXNG_CONF/settings.yml" ]; then
mkdir -p "$SEARXNG_CONF"
SEARX_SECRET=$(openssl rand -hex 32)
cat > "$SEARXNG_CONF/settings.yml" <<SEARXCFG
use_default_settings: true
general:
instance_name: Archipelago Search
server:
secret_key: "$SEARX_SECRET"
bind_address: "0.0.0.0"
port: 8080
limiter: false
ui:
default_theme: simple
SEARXCFG
chown -R 100000:100000 "$SEARXNG_CONF" 2>/dev/null
log " Created SearXNG settings.yml"
fi
$DOCKER run -d --name searxng --restart unless-stopped \
--health-cmd="curl -sf http://localhost:8080/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit searxng) \
--cap-drop ALL --security-opt no-new-privileges:true \
--read-only --tmpfs /tmp:rw,noexec,nosuid,size=256m --tmpfs /run:rw,noexec,nosuid,size=64m \
-p 8888:8080 \
-v /var/lib/archipelago/searxng:/etc/searxng \
"${SEARXNG_IMAGE}" 2>>"$LOG" || true
fi
track_container "searxng"
# OnlyOffice removed — incompatible with rootless Podman (internal postgres/rabbitmq)
# CryptPad is the replacement (single Node.js process, e2e encrypted)
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q filebrowser; then
log "Creating File Browser..."
mkdir -p /var/lib/archipelago/filebrowser /var/lib/archipelago/filebrowser-data
# Pre-create default directories so FileBrowser doesn't 404 on first load
mkdir -p /var/lib/archipelago/filebrowser/{Documents,Photos,Music,Downloads,Builds}
$DOCKER run -d --name filebrowser --restart unless-stopped \
--health-cmd="curl -sf http://localhost:80/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit filebrowser) \
--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 8083:80 \
-v /var/lib/archipelago/filebrowser:/srv \
-v /var/lib/archipelago/filebrowser-data:/data \
"$FILEBROWSER_IMAGE" \
--database=/data/database.db --root=/srv --address=0.0.0.0 --port=80 2>>"$LOG" || true
fi
track_container "filebrowser"
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q nginx-proxy-manager; then
log "Creating Nginx Proxy Manager..."
mkdir -p /var/lib/archipelago/nginx-proxy-manager/data /var/lib/archipelago/nginx-proxy-manager/letsencrypt
$DOCKER run -d --name nginx-proxy-manager --restart unless-stopped \
--health-cmd="curl -sf http://localhost:81/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit nginx-proxy-manager) \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add NET_BIND_SERVICE \
--security-opt no-new-privileges:true \
-p 81:81 -p 8084:80 -p 8443:443 \
-v /var/lib/archipelago/nginx-proxy-manager/data:/data \
-v /var/lib/archipelago/nginx-proxy-manager/letsencrypt:/etc/letsencrypt \
"${NPM_IMAGE}" 2>>"$LOG" || true
fi
track_container "nginx-proxy-manager"
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q portainer; then
log "Creating Portainer..."
mkdir -p /var/lib/archipelago/portainer
$DOCKER run -d --name portainer --restart unless-stopped \
--health-cmd="curl -sf http://localhost:9000/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit portainer) \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
--security-opt no-new-privileges:true \
-p 9000:9000 \
-v /var/lib/archipelago/portainer:/data \
-v /var/run/podman/podman.sock:/var/run/docker.sock \
"$PORTAINER_IMAGE" 2>>"$LOG" || true
fi
track_container "portainer"
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q tailscale; then
log "Creating Tailscale..."
mkdir -p /var/lib/archipelago/tailscale
# Tailscale needs NET_ADMIN + NET_RAW + TUN device (no --privileged)
$DOCKER run -d --name tailscale --restart unless-stopped \
--health-cmd="tailscale status || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit tailscale) \
--network host \
--cap-drop=ALL \
--cap-add=NET_ADMIN \
--cap-add=NET_RAW \
--security-opt no-new-privileges:true \
--device=/dev/net/tun:/dev/net/tun \
--read-only \
--tmpfs /tmp \
-v /var/lib/archipelago/tailscale:/var/lib/tailscale \
-e TS_STATE_DIR=/var/lib/tailscale \
"$TAILSCALE_IMAGE" \
sh -c 'tailscale web --listen 0.0.0.0:8240 & exec tailscaled' 2>>"$LOG" || true
fi
track_container "tailscale"
# Immich stack (postgres + redis + server - ML optional)
# 8. Nostr relays (optional - only if images were loaded; deploy does not create these on first boot)
# nostr-rs-relay and strfry are in ISO image bundle; create if image exists
if $DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q 'nostr-rs-relay'; then
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q nostr-rs-relay; then
log "Creating nostr-rs-relay..."
mkdir -p /var/lib/archipelago/nostr-rs-relay
$DOCKER run -d --name nostr-rs-relay --restart unless-stopped \
--health-cmd="curl -sf http://localhost:8080/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit nostr-rs-relay) \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
--read-only --tmpfs /tmp:rw,noexec,nosuid,size=32m \
-p 7047:7047 -v /var/lib/archipelago/nostr-rs-relay:/data \
"${NOSTR_RS_RELAY_IMAGE}" 2>>"$LOG" || true
fi
fi
if $DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q 'strfry'; then
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q strfry; then
log "Creating strfry..."
mkdir -p /var/lib/archipelago/strfry
$DOCKER run -d --name strfry --restart unless-stopped \
--health-cmd="curl -sf http://localhost:7777/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit strfry) \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true \
-p 7777:7777 -v /var/lib/archipelago/strfry:/data \
"${STRFRY_IMAGE}" 2>>"$LOG" || true
fi
fi
# 8b. Indeehub (pull from registry, or use local build)
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:local"
# Try registry image
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..."
$DOCKER run -d --name indeedhub --restart unless-stopped \
--health-cmd="curl -sf http://localhost:80/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit indeedhub) \
--cap-drop ALL --security-opt no-new-privileges:true \
--read-only --tmpfs /tmp:rw,noexec,nosuid,size=64m --tmpfs /app/.next/cache:rw,noexec,nosuid,size=128m \
-p 8190:3000 \
-e NODE_ENV=production -e NEXT_TELEMETRY_DISABLED=1 \
"$INDEEDHUB_IMAGE" 2>>"$LOG" || true
# Fix IndeedHub for iframe: remove X-Frame-Options so it loads in Archipelago panel
sleep 2
if $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q "^indeedhub$"; then
$DOCKER exec indeedhub sed -i "/X-Frame-Options/d" /etc/nginx/conf.d/default.conf 2>/dev/null || true
if [ -f /opt/archipelago/web-ui/nostr-provider.js ]; then
$DOCKER cp /opt/archipelago/web-ui/nostr-provider.js indeedhub:/usr/share/nginx/html/nostr-provider.js 2>/dev/null || true
fi
$DOCKER exec indeedhub nginx -s reload 2>/dev/null || true
log "Applied IndeedHub iframe fix (removed X-Frame-Options)"
fi
fi
fi
# 9. Custom UI containers (bitcoin-ui, lnd-ui)
# These are built from Dockerfiles in /opt/archipelago/docker/ or loaded from pre-built images.
# Inject Bitcoin RPC auth into bitcoin-ui nginx.conf BEFORE building
RPC_USER="archipelago"
RPC_PASS_FILE="/var/lib/archipelago/secrets/bitcoin-rpc-password"
if [ -f "$RPC_PASS_FILE" ]; then
RPC_PASS=$(cat "$RPC_PASS_FILE")
AUTH_B64=$(echo -n "${RPC_USER}:${RPC_PASS}" | base64)
for ui_dir in /opt/archipelago/docker/bitcoin-ui /home/archipelago/archy/docker/bitcoin-ui; do
if [ -f "$ui_dir/nginx.conf" ]; then
sed -i "s|__BITCOIN_RPC_AUTH__|${AUTH_B64}|g" "$ui_dir/nginx.conf"
log "Injected Bitcoin RPC auth into $ui_dir/nginx.conf"
fi
done
fi
for ui in bitcoin-ui lnd-ui electrs-ui; do
if $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q "$ui"; then
continue
fi
case $ui in
# UI containers use --network host so they can proxy to localhost services
# Internal nginx ports: bitcoin-ui=8334, electrs-ui=50002, lnd-ui=8080 (host 8081)
bitcoin-ui) PORT_ARG=""; NET_ARG="--network host"; REG_IMG="${BITCOIN_UI_IMAGE}" ;;
lnd-ui) PORT_ARG="-p 8081:8080"; NET_ARG=""; REG_IMG="${LND_UI_IMAGE}" ;;
electrs-ui) PORT_ARG=""; NET_ARG="--network host"; REG_IMG="${ELECTRS_UI_IMAGE}" ;;
esac
CONTAINER_NAME="archy-$ui"
UI_CAPS="--user 0:0 --cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE"
# Try registry image first, then local image, then build from source
if [ -n "$REG_IMG" ] && $DOCKER pull --tls-verify=false "$REG_IMG" 2>>"$LOG"; then
log "Starting $ui from registry ($REG_IMG)..."
$DOCKER run -d --name "$CONTAINER_NAME" $PORT_ARG --restart unless-stopped --memory=$(mem_limit "$CONTAINER_NAME") $NET_ARG \
$UI_CAPS "$REG_IMG" 2>>"$LOG" || true
elif $DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q "$ui"; then
log "Starting $ui from local image..."
IMG=$($DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep "$ui" | head -1)
$DOCKER run -d --name "$CONTAINER_NAME" $PORT_ARG --restart unless-stopped --memory=$(mem_limit "$CONTAINER_NAME") $NET_ARG \
$UI_CAPS "$IMG" 2>>"$LOG" || true
elif [ -d "/opt/archipelago/docker/$ui" ]; then
log "Building $ui from source..."
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_CAPS "$ui:local" 2>>"$LOG" || true
fi
else
log "$ui: no image or source found, skipping"
fi
done
# 10. Initialize backend data directories
# tor-config: backend stores tor service configs here (writable by archipelago user)
mkdir -p /var/lib/archipelago/tor-config
SERVICES_JSON=/var/lib/archipelago/tor-config/services.json
if [ ! -f "$SERVICES_JSON" ]; then
cat > "$SERVICES_JSON" <<'SJSON'
{"services":[
{"name":"archipelago","local_port":80,"enabled":true},
{"name":"lnd","local_port":8081,"enabled":true},
{"name":"btcpay","local_port":23000,"enabled":true},
{"name":"mempool","local_port":4080,"enabled":true},
{"name":"fedimint","local_port":8175,"enabled":true}
]}
SJSON
log "Created initial tor-config/services.json"
fi
# identity: node Ed25519 keypair (DID) — MUST persist across deployments
mkdir -p /var/lib/archipelago/identity
# identities: backend identity manager stores user DIDs here
mkdir -p /var/lib/archipelago/identities
# Ensure archipelago user can write to these directories
chown -R 1000:1000 /var/lib/archipelago/tor-config /var/lib/archipelago/identity /var/lib/archipelago/identities 2>/dev/null || true
# 11. Run container doctor for any remaining issues
log "Running container doctor..."
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if [ -x "$SCRIPT_DIR/container-doctor.sh" ]; then
bash "$SCRIPT_DIR/container-doctor.sh" --local 2>&1 | tee -a "$LOG"
elif [ -x "/opt/archipelago/scripts/container-doctor.sh" ]; then
bash "/opt/archipelago/scripts/container-doctor.sh" --local 2>&1 | tee -a "$LOG"
fi
# 11b. If any containers failed, run the reconciler to attempt recovery
FAILED=$((TOTAL - SUCCESS))
if [ "$FAILED" -gt 0 ]; then
log "Attempting to recover $FAILED failed container(s) via reconciler..."
RECONCILE_SCRIPT=""
if [ -x "$SCRIPT_DIR/reconcile-containers.sh" ]; then
RECONCILE_SCRIPT="$SCRIPT_DIR/reconcile-containers.sh"
elif [ -x "/opt/archipelago/scripts/reconcile-containers.sh" ]; then
RECONCILE_SCRIPT="/opt/archipelago/scripts/reconcile-containers.sh"
fi
if [ -n "$RECONCILE_SCRIPT" ]; then
runuser -u archipelago -- bash "$RECONCILE_SCRIPT" 2>&1 | tee -a "$LOG"
# Recount after reconciliation
SUCCESS=0
for name in $($DOCKER ps --format '{{.Names}}' 2>/dev/null); do
SUCCESS=$((SUCCESS + 1))
done
FAILED=$((TOTAL - SUCCESS))
log "After reconciliation: $SUCCESS running, $FAILED still failed"
fi
fi
# 12. Final summary
log "============================================="
log " FIRST-BOOT CONTAINER SUMMARY"
log "============================================="
log " Total tracked: $TOTAL"
log " Running: $SUCCESS"
log " Failed: $FAILED"
if [ "$BITCOIN_READY" != "true" ]; then
log " Bitcoin: NOT READY (dependent containers will auto-restart when ready)"
fi
if [ -n "$FAILED_LIST" ]; then
log " Failed list: $FAILED_LIST"
fi
log "============================================="
log "First-boot container creation complete"