feat(self-update): sync and rebuild UI containers on OTA

self-update.sh previously rebuilt only the backend binary and Vue
frontend. The custom UI containers (archy-bitcoin-ui, archy-lnd-ui,
archy-electrs-ui) were left untouched forever. That meant any change to
docker/<ui>/{Dockerfile, nginx.conf, index.html, ...} never reached a
running node through OTA; it required a manual SSH + rebuild. This is
exactly why the lnd-ui port fix didnt reach .228 in v1.7.43-alpha.

Add a sync-and-rebuild stage:

  1. Hash each docker/<ui>/ tree (content-only, path-stable via
     `cd && find` so src and dst compare equal when identical).
  2. rsync changed trees to /opt/archipelago/docker/<ui>/.
  3. For each changed UI: rebuild image as the archipelago user
     (rootless podman), then stop+remove+recreate the container using
     the canonical spec from scripts/container-specs.sh. Port mappings,
     caps, memory, and security opts all come from the spec, so the
     runtime cant drift from the tree.

Also install first-boot-containers.sh into /opt/archipelago/scripts/ so
a later reconciler run or reboot picks up current orchestration logic.

Idempotent: if no UI tree changed since the last update, the whole stage
is a no-op beyond the hash compare. Verified end-to-end on .228 with a
synthetic change to lnd-ui: detection, sync, build, recreate, and HTTP
200 on both the direct container port and the host-nginx /app/lnd/
proxy.
This commit is contained in:
archipelago
2026-04-23 15:48:53 -04:00
parent 72dec5aaa5
commit ce39430b33

View File

@@ -200,6 +200,96 @@ if [ -f "$REPO_DIR/scripts/image-versions.sh" ]; then
sudo cp "$REPO_DIR/scripts/image-versions.sh" /opt/archipelago/image-versions.sh
fi
# Update first-boot-containers.sh too (the canonical first-boot orchestrator).
# Nodes run it once on install, but keeping a fresh copy on disk means any
# future boot or reconciler invocation uses current port specs and caps.
if [ -f "$REPO_DIR/scripts/first-boot-containers.sh" ]; then
sudo install -m 755 "$REPO_DIR/scripts/first-boot-containers.sh" \
"$SCRIPTS_DEST/first-boot-containers.sh"
fi
# Sync UI container source trees (docker/bitcoin-ui, docker/lnd-ui,
# docker/electrs-ui) into /opt/archipelago/docker/<name>/. If any file in a
# UI tree changed since last update, rebuild that image and recreate its
# container using the spec from container-specs.sh. This is what prevented
# the lnd-ui port mismatch from reaching nodes through OTA: self-update used
# to update only the backend + frontend, never the UI container images.
UI_DOCKER_DEST="/opt/archipelago/docker"
sudo mkdir -p "$UI_DOCKER_DEST"
UI_REBUILD_LIST=""
for ui in bitcoin-ui lnd-ui electrs-ui; do
src="$REPO_DIR/docker/$ui"
dst="$UI_DOCKER_DEST/$ui"
[ -d "$src" ] || continue
# Hash source tree to decide if rebuild is needed. Any content change
# (Dockerfile, nginx.conf, index.html, assets) triggers a rebuild.
# Hash file contents only (not paths or metadata) so src and dst match
# when their contents are identical regardless of directory prefix.
src_hash=$( (cd "$src" && find . -type f | LC_ALL=C sort | xargs sha256sum 2>/dev/null) | sha256sum | cut -d' ' -f1)
dst_hash=""
if [ -d "$dst" ]; then
dst_hash=$( (cd "$dst" && find . -type f | LC_ALL=C sort | xargs sha256sum 2>/dev/null) | sha256sum | cut -d' ' -f1)
fi
if [ "$src_hash" != "$dst_hash" ]; then
log "UI source changed for $ui; syncing and marking for rebuild"
sudo rsync -a --delete "$src/" "$dst/"
UI_REBUILD_LIST="$UI_REBUILD_LIST $ui"
else
ok "UI source unchanged for $ui"
fi
done
# Rebuild changed UI images + recreate containers as the archipelago user
# (rootless podman storage lives under ~archipelago). Port mappings and caps
# come from scripts/container-specs.sh so spec drift can't sneak in.
if [ -n "$UI_REBUILD_LIST" ]; then
log "Rebuilding UI containers:$UI_REBUILD_LIST"
# shellcheck disable=SC1091
# container-specs.sh provides load_spec_archy-<ui> and mem_limit <name>.
SPECS="$SCRIPTS_DEST/container-specs.sh"
if [ ! -f "$SPECS" ]; then
warn "container-specs.sh missing at $SPECS; skipping UI rebuild"
else
for ui in $UI_REBUILD_LIST; do
cname="archy-$ui"
log " rebuilding $cname from $UI_DOCKER_DEST/$ui"
# Build image as archipelago user so it lands in the right store.
if ! sudo -u archipelago bash -c "
export XDG_RUNTIME_DIR=/run/user/\$(id -u archipelago)
cd '$UI_DOCKER_DEST/$ui' &&
podman build --no-cache -t 'localhost/$ui:local' . >>'$LOG_FILE' 2>&1
"; then
err " build failed for $ui; keeping existing container"
continue
fi
# Recreate container using spec from container-specs.sh.
if ! sudo -u archipelago bash -c "
export XDG_RUNTIME_DIR=/run/user/\$(id -u archipelago)
source '$SPECS'
load_spec_$cname || { echo 'spec load failed for $cname'; exit 1; }
podman stop '$cname' 2>/dev/null || true
podman rm '$cname' 2>/dev/null || true
PORT_ARG=''
[ -n \"\$SPEC_PORTS\" ] && PORT_ARG=\"-p \$SPEC_PORTS\"
NET_ARG=''
[ \"\$SPEC_NETWORK\" = 'host' ] && NET_ARG='--network host'
CAP_ARGS='--cap-drop ALL'
for c in \$SPEC_CAPS; do CAP_ARGS=\"\$CAP_ARGS --cap-add \$c\"; done
podman run -d --name '$cname' \$PORT_ARG \$NET_ARG \\
--user 0:0 \$CAP_ARGS \\
--memory=\"\$SPEC_MEMORY\" \\
--restart unless-stopped \\
--security-opt \"\$SPEC_SECURITY\" \\
'localhost/$ui:local' >>'$LOG_FILE' 2>&1
"; then
err " recreate failed for $cname"
continue
fi
ok " $cname rebuilt and running"
done
fi
fi
# Update systemd service if changed
if [ -f "$REPO_DIR/image-recipe/configs/archipelago.service" ]; then
if ! diff -q "$REPO_DIR/image-recipe/configs/archipelago.service" /etc/systemd/system/archipelago.service &>/dev/null; then