Image Cleanup β ACR & local Docker
Images pile up on ACR (meshweaver.azurecr.io), not on your machine. Two sources feed it: every
green merge to main publishes a 3.0.0-ci.<n> version tag (plus a git-sha and per-RID tags) via
main-cd.yml, and any manual dotnet publish -t:PublishContainer pushes whatever tag you named. That
publish talks to the registry directly β it does not create a local Docker image. This page is how
to clear the accumulation out safely.
π¨ The golden rule: never delete an image that any deployment references. A tag that looks "old" by name or date may still be live in another namespace. Always build the keeper list from what is actually deployed across every namespace β never from the tag name or push date.
The repos under meshweaver.azurecr.io:
| Repo | What it holds | Cleanup posture |
|---|---|---|
memex-portal-ai |
The portal image β one tag per deploy | Where the bloat is; prune aggressively |
memex-migration |
The DB-migration image | A few tags; keep the live one + latest |
memex-portal-next |
The portal-next client image |
Keep the tags matching live/kept portal versions |
mw-plugin-test |
Plugin-CI test image (also published to GHCR) | Keep the newest; not deployment-critical |
memex-portal-ai-base |
The custom runtime base image every portal build layers on | Never delete latest β it breaks every future build |
π¨
memex-portal-ai,memex-migration,mw-plugin-testandmemex-portal-nextform the four-image set that.github/scripts/check-image-set.shasserts is complete for a given commit, and CD's reconciler republishes when it is not. Deleting one leg's tag for a commit makes that commit look unpublished to the reconciler. Prune whole versions, never one repo's tag in isolation.
Step 1 β Build the keeper list (do this FIRST)
List every image referenced by a live Deployment in all namespaces on the shared cluster. The
Fleet Console (/Hosting/Console) shows the RUNNING version per recorded instance; the cluster
read below is break-glass β it covers namespaces no record describes, which is exactly why a
cleanup must not trust the record list alone (OperatingFromThePortal).
The cluster is private β kubectl only via az aks command invoke.
Query every namespace, never a hand-written list β the cluster runs at least three portal
namespaces (portalNamespaces in deploy/aks/infra/main.bicep is memex, prod, memex-cloud), and a
loop that names only some of them silently omits a live image from the keeper list, which is exactly how
you delete something in use:
az aks command invoke -g <aks-resource-group> -n <aks-cluster> --command "\
kubectl get deploy,statefulset,job,cronjob -A \
-o jsonpath='{range .items[*]}{.metadata.namespace}{\"\t\"}{.spec.template.spec.containers[*].image}{\"\n\"}{end}' \
| sort -u"
-A covers namespaces added since this page was written. Include Jobs and StatefulSets too β the
migration ships as a Job, so a get deploy-only sweep misses the memex-migration tag entirely.
Everything that prints is a hard keeper. Example output shape:
memex-portal-ai:nicepicker-09149ea0d(a customer portal)memex-portal-ai:settingsfix-bfdd797ae(memex portal β looks old, is live!)memex-migration:settingsfix-bfdd797ae(both envs' migration)
Add to the keeper list:
memex-portal-ai-base:latestβ the build base. Deleting it breaks every build.memex-migration:latestβ the conventional moving tag.- One or two recent rollback tags per environment (e.g. the deploy immediately before the current
one), so you can
kubectl set imageback if a new rollout misbehaves. - Any image you are about to deploy (a tag still mid-
docker pushfrom an in-flight build).
Everything in ACR that is not on this list is safe to delete.
Step 2 β List the ACR tags (newest first)
az acr repository show-tags -n meshweaver --repository memex-portal-ai \
--orderby time_desc --detail --query "[].{tag:name,updated:lastUpdateTime}" -o tsv
azoutput can carry non-ASCII bytes that crash the Windows console (cp1252). Pipe throughtr -cd '\11\12\15\40-\176'to strip them. The same applies toaz aks command invokeoutput.
Step 3 β Delete the old tags
Deleting a tag removes its manifest; ACR ref-counts layers, so layers shared with a kept image survive β you only reclaim what nothing else points at. Deletion is irreversible (no recycle bin), which is why Step 1 comes first.
Delete one tag:
az acr repository delete -n meshweaver --image memex-portal-ai:deploy-9a3488ed4 --yes
Delete many β keep the list explicit so a keeper can never be swept in by a pattern:
KEEP="nicepicker-09149ea0d settingsfix-bfdd797ae cmdux-fdaa94971 fixall-f560d20d6"
for tag in $(az acr repository show-tags -n meshweaver --repository memex-portal-ai -o tsv | tr -cd '\11\12\15\40-\176\n'); do
case " $KEEP " in
*" $tag "*) echo "keep $tag" ;;
*) echo "delete $tag"; az acr repository delete -n meshweaver --image "memex-portal-ai:$tag" --yes >/dev/null ;;
esac
done
Repeat for memex-migration with KEEP="settingsfix-bfdd797ae latest". Skip memex-portal-ai-base
entirely β its only tag, latest, is a keeper.
Optional β automate ongoing hygiene
On a Premium ACR you can stop the pile-up at the source instead of hand-pruning:
- Untagged-manifest retention β auto-delete manifests that lost their tag after N days:
az acr config retention update -r meshweaver --status enabled --days 30 --type UntaggedManifests - Scheduled purge task β
az acr taskrunningacr purgeon a cron, e.g. keep the 10 newestmemex-portal-aitags and drop the rest. Encode the keeper protection with a--keepcount and a--filterthat never matcheslatest.
Automation is good for the untagged/old long tail; the live keeper rule still stands β a retention window must be long enough that no currently-deployed tag ages out, or pin keepers with an exclusion.
Local Docker cleanup
Because the portal/migration images live on ACR (never pulled locally), your local Docker holds only
reusable infrastructure images β the Aspire/testcontainers dependencies (pgvector/pgvector,
dpage/pgadmin4, mcr.microsoft.com/azure-storage/azurite, testcontainers/ryuk). Deleting those
just forces a re-pull (~2 GB) next aspire run/test run, so leave them unless you are truly out of
disk.
What is always safe to reclaim:
docker container prune -f # stopped containers (left over from old aspire runs / testcontainers)
docker builder prune -f # build cache
docker image prune -f # DANGLING (untagged) images only β never touches tagged infra images
Check what is reclaimable first, and how much:
docker system df # TYPE / SIZE / RECLAIMABLE per category
For a full sweep (stopped containers + unused networks + dangling images + build cache) in one go:
docker system prune -f # safe: does NOT remove tagged images that are in use
β οΈ Do not run docker system prune -a (or docker image prune -a) unless you intend to drop the
reusable infra images too β -a removes every image not attached to a running container, forcing the
multi-GB re-pull. The non--a forms above are the routine cleanup.
See also
- DeploymentAKS.md β how the tags get built + rolled out in the first place
- Deployment.md β the deploy-route index