backend: harden rootless app lifecycle orchestration
This commit is contained in:
@@ -177,11 +177,16 @@ load_spec_bitcoin-knots() {
|
||||
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"
|
||||
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"
|
||||
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
|
||||
}
|
||||
|
||||
@@ -518,11 +523,12 @@ load_spec_portainer() {
|
||||
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"
|
||||
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"
|
||||
}
|
||||
|
||||
|
||||
@@ -5,3 +5,7 @@
|
||||
# Edit deploy-config.sh and set ARCHIPELAGO_PASSWORD
|
||||
#
|
||||
export ARCHIPELAGO_PASSWORD='your_password_here'
|
||||
|
||||
# Optional: central beta telemetry collector RPC endpoint.
|
||||
# The reporter sends telemetry.ingest JSON-RPC requests here when users opt in.
|
||||
# export TELEMETRY_COLLECTOR_URL='https://YOUR-COLLECTOR-HOST/rpc/v1'
|
||||
|
||||
@@ -17,6 +17,7 @@ set -eo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
TARGET_DIR="/home/archipelago/archy"
|
||||
PODMAN_IMAGE_CHECK_TIMEOUT="${PODMAN_IMAGE_CHECK_TIMEOUT:-10}"
|
||||
|
||||
# Load deploy config defaults (IP addresses etc.)
|
||||
[ -f "$SCRIPT_DIR/deploy-config-defaults.sh" ] && . "$SCRIPT_DIR/deploy-config-defaults.sh"
|
||||
@@ -186,7 +187,7 @@ deploy_node() {
|
||||
# Transfer custom UI images (individual tarballs — never combined)
|
||||
echo " Transferring custom UI images..."
|
||||
for ui_img in bitcoin-ui lnd-ui electrs-ui; do
|
||||
HAS_IMG=$(ssh $SSH_OPTS "$BUILD_SOURCE" "podman images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q '${ui_img}:' && echo yes || echo no" 2>/dev/null)
|
||||
HAS_IMG=$(ssh $SSH_OPTS "$BUILD_SOURCE" "timeout --kill-after=2s ${PODMAN_IMAGE_CHECK_TIMEOUT}s podman image exists 'localhost/${ui_img}:local' 2>/dev/null && echo yes || echo no" 2>/dev/null)
|
||||
if [ "$HAS_IMG" = "yes" ]; then
|
||||
echo " $ui_img..."
|
||||
if ssh $SSH_OPTS "$BUILD_SOURCE" "podman save 'localhost/${ui_img}:local' 2>/dev/null" > "/tmp/${ui_img}.tar" 2>/dev/null && [ -s "/tmp/${ui_img}.tar" ]; then
|
||||
@@ -926,12 +927,19 @@ LNDCONF
|
||||
if \$DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -qx portainer; then
|
||||
\$DOCKER start portainer 2>/dev/null || true
|
||||
else
|
||||
sudo mkdir -p /var/lib/archipelago/portainer
|
||||
sudo mkdir -p /var/lib/archipelago/portainer/compose
|
||||
sudo chown -R archipelago:archipelago /var/lib/archipelago/portainer 2>/dev/null || true
|
||||
if [ ! -e /data ]; then
|
||||
sudo ln -s /var/lib/archipelago/portainer /data 2>/dev/null || true
|
||||
elif [ -d /data ] && [ ! -L /data ] && [ ! -e /data/compose ]; then
|
||||
sudo ln -s /var/lib/archipelago/portainer/compose /data/compose 2>/dev/null || true
|
||||
fi
|
||||
\$DOCKER run -d --name portainer --restart unless-stopped \
|
||||
--health-cmd 'curl -sf http://localhost:9000/' --health-interval=30s --health-timeout=5s --health-retries=3 \
|
||||
--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/lib/archipelago/portainer/compose:/data/compose \
|
||||
-v /run/user/1000/podman/podman.sock:/var/run/docker.sock \
|
||||
$PORTAINER_IMAGE
|
||||
fi
|
||||
|
||||
@@ -421,6 +421,20 @@ deploy_secondary() {
|
||||
rm -f /tmp/archipelago.service
|
||||
' 2>/dev/null || true
|
||||
fi
|
||||
if [ -n "${TELEMETRY_COLLECTOR_URL:-}" ]; then
|
||||
echo " Syncing telemetry collector config to .$SEC_LABEL..."
|
||||
TMP_TELEMETRY_ENV="$(mktemp)"
|
||||
printf 'TELEMETRY_COLLECTOR_URL=%s\n' "$TELEMETRY_COLLECTOR_URL" > "$TMP_TELEMETRY_ENV"
|
||||
scp $SSH_OPTS "$TMP_TELEMETRY_ENV" "$SEC_TARGET:/tmp/telemetry.env" 2>/dev/null || true
|
||||
rm -f "$TMP_TELEMETRY_ENV"
|
||||
ssh $SSH_OPTS "$SEC_TARGET" '
|
||||
sudo mkdir -p /var/lib/archipelago
|
||||
sudo cp /tmp/telemetry.env /var/lib/archipelago/telemetry.env
|
||||
sudo chown archipelago:archipelago /var/lib/archipelago/telemetry.env
|
||||
sudo chmod 600 /var/lib/archipelago/telemetry.env
|
||||
rm -f /tmp/telemetry.env
|
||||
' 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Deploy udev rule for mesh radio
|
||||
UDEV_RULE="$PROJECT_DIR/image-recipe/configs/99-mesh-radio.rules"
|
||||
@@ -682,6 +696,20 @@ if [ "$LIVE" = true ]; then
|
||||
rm -f /tmp/archipelago.service
|
||||
' 2>/dev/null || true
|
||||
fi
|
||||
if [ -n "${TELEMETRY_COLLECTOR_URL:-}" ]; then
|
||||
progress "Syncing telemetry collector config"
|
||||
TMP_TELEMETRY_ENV="$(mktemp)"
|
||||
printf 'TELEMETRY_COLLECTOR_URL=%s\n' "$TELEMETRY_COLLECTOR_URL" > "$TMP_TELEMETRY_ENV"
|
||||
scp $SSH_OPTS "$TMP_TELEMETRY_ENV" "$TARGET_HOST:/tmp/telemetry.env" 2>/dev/null || true
|
||||
rm -f "$TMP_TELEMETRY_ENV"
|
||||
ssh $SSH_OPTS "$TARGET_HOST" '
|
||||
sudo mkdir -p /var/lib/archipelago
|
||||
sudo cp /tmp/telemetry.env /var/lib/archipelago/telemetry.env
|
||||
sudo chown archipelago:archipelago /var/lib/archipelago/telemetry.env
|
||||
sudo chmod 600 /var/lib/archipelago/telemetry.env
|
||||
rm -f /tmp/telemetry.env
|
||||
' 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Deploy udev rule for mesh radio stable naming (/dev/mesh-radio)
|
||||
UDEV_RULE="$PROJECT_DIR/image-recipe/configs/99-mesh-radio.rules"
|
||||
|
||||
@@ -431,6 +431,17 @@ fi
|
||||
|
||||
# Rootless podman prerequisites (run as root, configures for archipelago user)
|
||||
log "Setting up rootless podman prerequisites..."
|
||||
if ! command -v catatonit >/dev/null 2>&1; then
|
||||
log "Installing catatonit for Podman init support..."
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
apt-get update >>"$LOG" 2>&1 || true
|
||||
apt-get install -y catatonit >>"$LOG" 2>&1 || true
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
dnf install -y catatonit >>"$LOG" 2>&1 || true
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
apk add catatonit >>"$LOG" 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
# 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
|
||||
@@ -612,7 +623,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE 'bitcoin-knots|arch
|
||||
-v /var/lib/archipelago/bitcoin:/home/bitcoin/.bitcoin \
|
||||
"${BITCOIN_KNOTS_IMAGE}" \
|
||||
$BTC_EXTRA_ARGS \
|
||||
-printtoconsole=1 -dbcache=$BTC_DBCACHE -par=0 -maxconnections=125 2>>"$LOG"; then
|
||||
-printtoconsole=1 -dbcache=$BTC_DBCACHE -par=0 -maxconnections=125 -rpcthreads=16 -rpcworkqueue=256 2>>"$LOG"; then
|
||||
log "Bitcoin Knots started"
|
||||
else
|
||||
log "Bitcoin Knots failed (may already exist)"
|
||||
@@ -1202,7 +1213,13 @@ 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
|
||||
mkdir -p /var/lib/archipelago/portainer/compose
|
||||
chown -R archipelago:archipelago /var/lib/archipelago/portainer 2>/dev/null || true
|
||||
if [ ! -e /data ]; then
|
||||
ln -s /var/lib/archipelago/portainer /data 2>/dev/null || true
|
||||
elif [ -d /data ] && [ ! -L /data ] && [ ! -e /data/compose ]; then
|
||||
ln -s /var/lib/archipelago/portainer/compose /data/compose 2>/dev/null || true
|
||||
fi
|
||||
$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) \
|
||||
@@ -1210,7 +1227,8 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q portainer; then
|
||||
--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 \
|
||||
-v /var/lib/archipelago/portainer/compose:/data/compose \
|
||||
-v /run/user/$(id -u archipelago)/podman/podman.sock:/var/run/docker.sock \
|
||||
"$PORTAINER_IMAGE" 2>>"$LOG" || true
|
||||
fi
|
||||
track_container "portainer"
|
||||
@@ -1232,7 +1250,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q tailscale; then
|
||||
-v /var/lib/archipelago/tailscale:/var/lib/tailscale \
|
||||
-e TS_STATE_DIR=/var/lib/tailscale \
|
||||
"$TAILSCALE_IMAGE" \
|
||||
sh -c 'tailscaled --tun=userspace-networking & sleep 2; tailscale web --listen 0.0.0.0:8240 & wait' 2>>"$LOG" || true
|
||||
sh -c 'tailscaled --tun=userspace-networking & for i in $(seq 1 30); do [ -S /var/run/tailscale/tailscaled.sock ] && break; sleep 1; done; tailscale web --listen 0.0.0.0:8240 & wait' 2>>"$LOG" || true
|
||||
fi
|
||||
track_container "tailscale"
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
# This script: stops broken containers, removes them, recreates with correct images.
|
||||
|
||||
echo "=== IndeedHub Container Fix Script ==="
|
||||
PODMAN_IMAGE_CHECK_TIMEOUT="${PODMAN_IMAGE_CHECK_TIMEOUT:-10}"
|
||||
|
||||
# Detect node IP (Tailscale or LAN)
|
||||
NODE_IP=$(hostname -I | awk '{for(i=1;i<=NF;i++) if($i ~ /^100\./) print $i}')
|
||||
@@ -29,7 +30,7 @@ fi
|
||||
# Verify correct images are available
|
||||
echo "Verifying images..."
|
||||
for img in "${INDEEDHUB_REDIS_IMAGE}" "${MINIO_IMAGE}" "${INDEEDHUB_POSTGRES_IMAGE}" "${NOSTR_RS_RELAY_IMAGE}" "${SEARXNG_IMAGE}" "localhost/indeedhub:local" "localhost/indeedhub-build_api:local" "localhost/indeedhub-build_ffmpeg-worker:local"; do
|
||||
if ! podman image exists "$img" 2>/dev/null; then
|
||||
if ! timeout --kill-after=2s "${PODMAN_IMAGE_CHECK_TIMEOUT}s" podman image exists "$img" 2>/dev/null; then
|
||||
echo "ERROR: Missing image $img"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -98,6 +98,11 @@ alloc_port() {
|
||||
# Run as archipelago user — podman sees rootless containers directly.
|
||||
# Use sudo only for chown/mkdir operations.
|
||||
PODMAN="podman"
|
||||
PODMAN_IMAGE_CHECK_TIMEOUT="${PODMAN_IMAGE_CHECK_TIMEOUT:-10}"
|
||||
|
||||
podman_bounded() {
|
||||
timeout --kill-after=2s "${PODMAN_IMAGE_CHECK_TIMEOUT}s" "$PODMAN" "$@"
|
||||
}
|
||||
|
||||
# ── Pre-flight ───────────────────────────────────────────────────────
|
||||
header "╔══════════════════════════════════════════════════╗"
|
||||
@@ -152,7 +157,7 @@ container_image_id() {
|
||||
}
|
||||
|
||||
spec_image_id() {
|
||||
$PODMAN image inspect "$SPEC_IMAGE" --format '{{.Id}}' 2>/dev/null
|
||||
podman_bounded image inspect "$SPEC_IMAGE" --format '{{.Id}}' 2>/dev/null
|
||||
}
|
||||
|
||||
container_network() {
|
||||
@@ -218,6 +223,39 @@ prepare_bind_source() {
|
||||
esac
|
||||
}
|
||||
|
||||
ensure_catatonit() {
|
||||
command -v catatonit >/dev/null 2>&1 && return 0
|
||||
$CHECK_ONLY && { info "catatonit missing (would install)"; return 0; }
|
||||
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
sudo apt-get update >/dev/null 2>&1 || true
|
||||
sudo apt-get install -y catatonit >/dev/null 2>&1 || true
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
sudo dnf install -y catatonit >/dev/null 2>&1 || true
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
sudo apk add catatonit >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
command -v catatonit >/dev/null 2>&1 || { fail "catatonit missing; Portainer compose builds may fail"; return 1; }
|
||||
}
|
||||
|
||||
ensure_portainer_host_paths() {
|
||||
ensure_catatonit
|
||||
if $CHECK_ONLY; then
|
||||
[ -d /var/lib/archipelago/portainer/compose ] || info "Portainer compose dir missing (would create)"
|
||||
[ -e /data ] || info "/data host path missing (would link to /var/lib/archipelago/portainer)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
sudo mkdir -p /var/lib/archipelago/portainer/compose 2>/dev/null || true
|
||||
sudo chown -R 1000:1000 /var/lib/archipelago/portainer 2>/dev/null || true
|
||||
if [ ! -e /data ]; then
|
||||
sudo ln -s /var/lib/archipelago/portainer /data 2>/dev/null || true
|
||||
elif [ -d /data ] && [ ! -L /data ] && [ ! -e /data/compose ]; then
|
||||
sudo ln -s /var/lib/archipelago/portainer/compose /data/compose 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
container_has_mount() {
|
||||
local name="$1" source="$2" target="$3"
|
||||
$PODMAN inspect "$name" --format '{{range .Mounts}}{{println .Source "|" .Destination}}{{end}}' 2>/dev/null \
|
||||
@@ -250,13 +288,7 @@ container_env_val() {
|
||||
URL_ENV_SUFFIXES="_URL _HOST _ENDPOINT"
|
||||
|
||||
image_exists() {
|
||||
# Note: `grep -q` closes stdin after first match → SIGPIPE (exit 141) on podman.
|
||||
# With `set -o pipefail` active in the parent script, that propagates as failure
|
||||
# and spuriously skips local-image containers. Use a full scan + explicit match
|
||||
# check to keep the exit code stable regardless of pipefail.
|
||||
local images
|
||||
images=$($PODMAN images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null)
|
||||
echo "$images" | grep -qF "$1"
|
||||
podman_bounded image exists "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
resolve_spec_image() {
|
||||
@@ -280,7 +312,7 @@ resolve_spec_image() {
|
||||
fi
|
||||
done
|
||||
|
||||
repo=$($PODMAN images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null \
|
||||
repo=$(podman_bounded images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null \
|
||||
| grep -E "/${image_name}:${image_tag}$" \
|
||||
| head -1 || true)
|
||||
if [ -n "$repo" ]; then
|
||||
@@ -377,6 +409,8 @@ reconcile() {
|
||||
return
|
||||
fi
|
||||
|
||||
[ "$name" = "portainer" ] && ensure_portainer_host_paths
|
||||
|
||||
# Filter by tier
|
||||
[ -n "$FILTER_TIER" ] && [ "$SPEC_TIER" != "$FILTER_TIER" ] && return
|
||||
|
||||
@@ -701,7 +735,7 @@ BTCEOF
|
||||
# bitcoin_rw.conf, so clean both files.
|
||||
for conf in "$BITCOIN_CONF" "/var/lib/archipelago/bitcoin/bitcoin_rw.conf"; do
|
||||
if [ -f "$conf" ]; then
|
||||
sudo sed -i '/^server=/d; /^txindex=/d; /^rpcbind=/d; /^rpcallowip=/d; /^rpcport=/d; /^listen=/d; /^bind=/d; /^dbcache=/d' "$conf" 2>/dev/null
|
||||
sudo sed -i '/^server=/d; /^txindex=/d; /^rpcbind=/d; /^rpcallowip=/d; /^rpcport=/d; /^listen=/d; /^bind=/d; /^dbcache=/d; /^rpcthreads=/d; /^rpcworkqueue=/d' "$conf" 2>/dev/null
|
||||
fi
|
||||
done
|
||||
sudo chown -R 100101:100101 /var/lib/archipelago/bitcoin 2>/dev/null
|
||||
|
||||
Reference in New Issue
Block a user