From fc169e7a4a59f1523265d72571c7cfce0870b241 Mon Sep 17 00:00:00 2001 From: librelad Date: Thu, 13 Aug 2026 00:47:37 +0100 Subject: [PATCH] feat(updater): clean up superseded images after a stepped upgrade MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The live Nextcloud 31→34 climb left 4.4 GB of images behind — one per rung, each ~1.5 GB, all still present after it finished. On a small VPS that is the difference between working and full. `system reclaim` cannot help: it collects DANGLING images, and every rung is a real tag, so all of them stay tagged and stay on disk. (Rolling apps never hit this — moving a floating tag orphans the old image, which reclaim then collects. It is specific to laddering.) After a SUCCESSFUL climb only, remove the images stepped through, keeping the immediately-previous version so a roll-back needs no download. CFG_UPDATER_UPGRADE_PRUNE=false keeps everything. Never runs on failure, where the older images are exactly what recovery may need. Tested: a 3-rung climb removes 31 and 32 and keeps 33; a single-step climb removes nothing (its previous version IS the rollback target); the config switch disables it. Found by looking at the box after the first real ladder run — the feature worked, and then quietly cost 4.4 GB. Co-Authored-By: Claude Opus 5 --- configs/webui/webui_updater | 1 + .../commands/updater/cli_updater_upgrade.sh | 38 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/configs/webui/webui_updater b/configs/webui/webui_updater index b3770eb..89dbbb5 100644 --- a/configs/webui/webui_updater +++ b/configs/webui/webui_updater @@ -6,4 +6,5 @@ CFG_UPDATER_SCAN_INTERVAL=30 # App Scan Interval - CFG_UPDATER_REGISTRY_INTERVAL=360 # Registry Check Interval - Minutes between registry lookups for new image builds (the expensive step; the local scan still refreshes every scan). 0 = only when you press Check now. CFG_HOTFIX_AUTO=security-breakage # Hotfix Auto-Apply - Which signed hotfix severities apply automatically on the update check [security-breakage|all|off] CFG_UPDATER_AUTO=true # Automatic App Updates - Master switch for per-app automatic updates. Each app's own Update Type decides individually; turning this off makes every app manual. Every update snapshots the app first and rolls back on failure. [true:On|false:Off] +CFG_UPDATER_UPGRADE_PRUNE=true # Clean Up After Version Upgrades - After a successful stepped upgrade (e.g. Nextcloud 31→34), delete the images it stepped through. Each one can be well over a gigabyte and nothing else removes them, since every step is a real tag rather than a dangling image. The version immediately before the current one is always kept so a roll-back needs no download. [true:On|false:Off] CFG_UPDATER_WINDOW=06:00-08:00 # Automatic Update Window - When automatic updates are allowed to install, in the host's local time (HH:MM-HH:MM; crosses midnight when start > end; 'always' = any time). Checks still run all day so the Updates page stays current — found updates simply wait for the window. Pressing Update yourself always works. diff --git a/scripts/cli/commands/updater/cli_updater_upgrade.sh b/scripts/cli/commands/updater/cli_updater_upgrade.sh index a89e871..d8005cf 100644 --- a/scripts/cli/commands/updater/cli_updater_upgrade.sh +++ b/scripts/cli/commands/updater/cli_updater_upgrade.sh @@ -158,10 +158,48 @@ updaterUpgradeApp() { done isSuccessful "$app upgraded through ${done_n} version(s) — now on $from, verified." + _updaterUpgradePruneImages "$app" "$cur" "${rungs[@]}" webuiUpdaterScan >/dev/null 2>&1 || true return 0 } +# Drop the images the climb left behind. A 3-rung Nextcloud upgrade downloads +# ~1.5 GB per rung and keeps every one — 4.4 GB of superseded images after a +# single upgrade, which on a small VPS is the difference between working and +# full. `system reclaim` cannot help: it collects DANGLING images, and each rung +# is a distinct tag, so they are all still tagged and all still there. (Rolling +# apps do not have this problem — moving a floating tag orphans the old image, +# which reclaim then collects.) +# +# Only ever after a SUCCESSFUL climb, and the immediately-previous version is +# KEPT as the roll-back target so recovery does not depend on the network. +# CFG_UPDATER_UPGRADE_PRUNE=false to keep everything. +_updaterUpgradePruneImages() { + local app="$1" start="$2"; shift 2 + local -a climbed=("$@") + [[ "${CFG_UPDATER_UPGRADE_PRUNE:-true}" == "true" ]] || { isNotice "Keeping superseded images (CFG_UPDATER_UPGRADE_PRUNE=false)."; return 0; } + (( ${#climbed[@]} >= 2 )) || return 0 # one step: previous IS the rollback target + + local anchor repo + anchor="$(updaterPrimaryImage "$app" "${containers_dir%/}/$app/docker-compose.yml")" + repo="$(updaterRepoTag "$anchor")"; repo="${repo%:*}" + + # Everything we moved off, minus the last one (kept for rollback). + local -a superseded=("$start" "${climbed[@]:0:$(( ${#climbed[@]} - 1 ))}") + unset 'superseded[-1]' + (( ${#superseded[@]} > 0 )) || return 0 + + local tag removed=0 + for tag in "${superseded[@]}"; do + [[ -n "$tag" ]] || continue + if runFileOp docker image rm "${repo}:${tag}" >/dev/null 2>&1; then + removed=$((removed + 1)) + fi + done + (( removed > 0 )) && isSuccessful "Removed $removed superseded image(s); kept ${climbed[-2]} for roll-back." + return 0 +} + # Undo one failed rung: put the version back, restore the snapshot taken moments # ago, start it, and record what happened. Best effort by nature — if the # restore itself fails the user is told plainly rather than reassured.