feat(updater): clean up superseded images after a stepped upgrade

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 <noreply@anthropic.com>
This commit is contained in:
librelad 2026-08-13 00:47:37 +01:00
parent 98b7f7dd39
commit fc169e7a4a
2 changed files with 39 additions and 0 deletions

View File

@ -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.

View File

@ -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.