Keep the last few images, and record delete-all-spam as verified live

Every deploy tags an image with its version, which is what makes a
rollback a `docker run` rather than a rebuild -- and what has quietly
put 7.7 GB of them on the host, 4.3 GB of it reclaimable. `docker image
prune` will not help: they are tagged, which is the whole point of them.

The script that creates them now removes them, keeping the newest
IHASMAIL_KEEP_VERSIONS (three by default, 0 to keep everything).

Two things it is careful about, both learned from what could go wrong
rather than from something that did:

  - it runs only after the new container reports healthy, so a rollback
    target is never dropped while the thing meant to replace it is still
    unproven.

  - it excludes the image the container is actually running, by asking
    docker what that is rather than assuming it sorts newest. After a
    rollback it does not: the running image is an old one, and naive
    keep-the-newest-N would delete the image in use. Docker would refuse,
    but being refused is not the same as not having tried.

Exercised against a stubbed docker on PATH, so the pipeline and xargs
are the real ones: keeps 3 and removes the oldest, keeps 1 and removes
three, keeps everything at 0, never lists :current, and spares the
running image in a rollback where it is the fourth-newest.

Also records delete-all-spam as confirmed live on 0.16.19 today: Junk
Mail emptied and Deleted Items stayed empty afterwards, which is the
half a mock cannot prove and the half the feature exists for.
This commit is contained in:
2026-08-26 12:15:51 -07:00
parent e45c43100f
commit e4915dd496
2 changed files with 38 additions and 0 deletions
+29
View File
@@ -48,6 +48,11 @@ VOLUME="${IHASMAIL_VOLUME:-ihasmail-data}"
IMAGE_REPO="${IHASMAIL_IMAGE:-ihasmail}"
# How long to wait for the new container to report healthy, in seconds.
HEALTH_TIMEOUT="${IHASMAIL_HEALTH_TIMEOUT:-30}"
# How many past versions to keep as images, for rolling back to. Each is around
# 650 MB, and a deploy adds one, so left alone they accumulate a gigabyte every
# couple of releases -- and `docker image prune` will not touch them, because
# they are tagged. 0 keeps every version.
KEEP_VERSIONS="${IHASMAIL_KEEP_VERSIONS:-3}"
# --- run from a copy, if this script lives in the checkout it resets ---------
# `git reset --hard` below rewrites the working tree, and this script may be
@@ -153,6 +158,29 @@ git reset --hard --quiet "$TARGET"
# cannot: .dockerignore keeps .git out of the build context. Without this the
# build falls back to the base version in package.json and every deployment
# reports the same number -- see "Version numbers" in the README.
# Drop the oldest versioned images, keeping the newest KEEP_VERSIONS of them.
#
# Only ever runs after the new container reports healthy, so a rollback target
# is never removed while the thing replacing it is still unproven. The image in
# use is excluded outright rather than relied on to sort newest -- docker
# refuses to remove an image a container is using, but being refused is not the
# same as not having tried.
prune_old_images() {
[ "$KEEP_VERSIONS" -gt 0 ] || return 0
local in_use stale
in_use="$(docker inspect "$NAME" --format '{{.Config.Image}}' 2>/dev/null || true)"
# Newest first, tags only, skipping the moving ":current" pointer.
stale="$(docker images "$IMAGE_REPO" --format '{{.Repository}}:{{.Tag}}\t{{.CreatedAt}}' \
| grep -v ":current" \
| sort -k2 -r \
| cut -f1 \
| grep -vxF "$in_use" \
| tail -n +"$((KEEP_VERSIONS + 1))")"
[ -n "$stale" ] || return 0
echo "==> removing $(printf '%s\n' "$stale" | wc -l) old image(s), keeping the newest $KEEP_VERSIONS"
printf '%s\n' "$stale" | xargs -r docker rmi >/dev/null 2>&1 || true
}
VERSION="$(node scripts/version.mjs)"
# A Docker tag may not contain "+", which a version for a commit that did not
# come through a pull request does: 2.16.57+g1fa6578. The image is tagged with
@@ -174,6 +202,7 @@ docker run -d --name "$NAME" --restart unless-stopped \
for _ in $(seq 1 "$HEALTH_TIMEOUT"); do
if health=$(curl -sf "http://$BIND/api/health"); then
echo "==> healthy: $health"
prune_old_images
exit 0
fi
sleep 1