docker_prune_images: add --all mode for full orphan cleanup

Default still removes only dangling (untagged) images.
--all removes stopped containers first, then all images not used
by any running container — clears tagged orphan images left behind
by removed or stopped apps.
This commit is contained in:
Gmer4Lfe
2026-05-29 23:36:59 -04:00
parent 86048b32d3
commit d2eb060fe5
+101 -33
View File
@@ -5,25 +5,35 @@
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Removes dangling Docker images — images no longer tagged or referenced by any
# container. These accumulate after container updates pull a new image version,
# leaving the old image behind with no tag.
# Removes orphaned Docker images. Two modes:
#
# Safe to run at any time. Only removes images with no tag and no container
# reference — running containers are never affected.
# Default — dangling only (safe, fast):
# Removes untagged images (no name, no container reference). These accumulate
# after container updates pull a new image, leaving the old one untagged.
# Running containers are never affected.
#
# --all — full orphan cleanup:
# Step 1: removes stopped containers (exited/created state).
# Step 2: removes all images not used by any running container.
# Use this to clear tagged images left behind by removed or stopped apps.
# CAUTION: also removes intentionally stopped containers — only run when you
# know all stopped containers are safe to delete.
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# docker_prune_images.sh
# Show dangling images and remove them.
# Remove dangling (untagged) images only.
#
# docker_prune_images.sh --all
# Remove stopped containers, then remove all unused images.
#
# docker_prune_images.sh --dry-run
# Show what would be removed without removing anything.
# Show what would be removed without making changes.
#
# docker_prune_images.sh --status
# Show current dangling images and disk usage. No changes.
# Show dangling images and stopped containers. No changes.
#
# ==============================================================================================
@@ -31,7 +41,13 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../load_config.sh"
parse_args "$@"
# ── Handle --all before parse_args ────────────────────────────────────────────
ALL_MODE=false
FILTERED_ARGS=()
for _arg in "$@"; do
[[ "$_arg" == "--all" ]] && ALL_MODE=true || FILTERED_ARGS+=("$_arg")
done
parse_args "${FILTERED_ARGS[@]}"
# ==============================================================================================
# ━━━ Setup ━━━
@@ -49,16 +65,32 @@ acquire_lock
if [[ "$SHOW_STATUS" == true ]]; then
echo ""
echo "━━━━━ $ICON_SUMMARY DOCKER PRUNE IMAGES STATUS ━━━━━"
DANGLING=$(docker images -f "dangling=true" --format "{{.ID}}\t{{.Size}}\t{{.CreatedSince}}" 2>/dev/null)
DANGLING=$(docker images -f "dangling=true" --format "{{.ID}}\t{{.Repository}}:{{.Tag}}\t{{.Size}}\t{{.CreatedSince}}" 2>/dev/null)
STOPPED=$(docker ps -a --filter "status=exited" --filter "status=created" \
--format "{{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}" 2>/dev/null)
echo ""
echo "$ICON_CONTAINERS Dangling images (no tag, no container):"
if [[ -z "$DANGLING" ]]; then
echo "$ICON_DONE No dangling images."
echo " none"
else
COUNT=$(echo "$DANGLING" | wc -l)
echo "$ICON_CONTAINERS $COUNT dangling image(s):"
echo "$DANGLING" | while IFS=$'\t' read -r id size age; do
echo " $id $size created $age"
echo "$DANGLING" | while IFS=$'\t' read -r id repo size age; do
echo " $id $repo $size $age"
done
fi
echo ""
echo "$ICON_CONTAINERS Stopped containers (--all would remove these first):"
if [[ -z "$STOPPED" ]]; then
echo " none"
else
echo "$STOPPED" | while IFS=$'\t' read -r id name image status; do
echo " $name ($image) $status"
done
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━"
exit 0
fi
@@ -66,30 +98,66 @@ fi
# ==============================================================================================
# ━━━ Run ━━━
# ==============================================================================================
echo "━━━ $ICON_CONTAINERS Docker Prune Images — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
MODE_LABEL=$([[ "$ALL_MODE" == true ]] && echo "full orphan cleanup" || echo "dangling only")
echo "━━━ $ICON_CONTAINERS Docker Prune Images ($MODE_LABEL) — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
DANGLING_IDS=$(docker images -f "dangling=true" -q 2>/dev/null)
TOTAL_RECLAIMED=0
if [[ -z "$DANGLING_IDS" ]]; then
success "No dangling images — nothing to do"
exit 0
# ── Step 1 (--all only): remove stopped containers ────────────────────────────
if [[ "$ALL_MODE" == true ]]; then
STOPPED_IDS=$(docker ps -a --filter "status=exited" --filter "status=created" -q 2>/dev/null)
STOPPED_COUNT=$(echo "$STOPPED_IDS" | grep -c . || echo 0)
if [[ "$STOPPED_COUNT" -eq 0 ]]; then
log "No stopped containers"
elif [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would remove $STOPPED_COUNT stopped container(s):"
docker ps -a --filter "status=exited" --filter "status=created" \
--format " {{.Names}} {{.Image}} {{.Status}}" 2>/dev/null
else
log "Removing $STOPPED_COUNT stopped container(s)..."
docker container prune -f 2>&1 | grep -v "^Total\|^$" || true
success "Removed $STOPPED_COUNT stopped container(s)"
fi
echo ""
fi
COUNT=$(echo "$DANGLING_IDS" | wc -l)
log "Found $COUNT dangling image(s)"
# ── Step 2: prune images ───────────────────────────────────────────────────────
PRUNE_FLAGS=$([[ "$ALL_MODE" == true ]] && echo "-a" || echo "")
PRUNE_FILTER=$([[ "$ALL_MODE" == true ]] && echo "" || echo '-f "dangling=true"')
# Preview count
if [[ "$ALL_MODE" == true ]]; then
# Images not used by any running container
RUNNING_IMAGES=$(docker ps --format "{{.Image}}" 2>/dev/null)
UNUSED_COUNT=$(docker images --format "{{.Repository}}:{{.Tag}}" 2>/dev/null \
| grep -vxF "$RUNNING_IMAGES" | grep -c . || echo 0)
TARGET_LABEL="$UNUSED_COUNT unused image(s)"
else
DANGLING_IDS=$(docker images -f "dangling=true" -q 2>/dev/null)
UNUSED_COUNT=$(echo "$DANGLING_IDS" | grep -c . || echo 0)
TARGET_LABEL="$UNUSED_COUNT dangling image(s)"
fi
if [[ "$UNUSED_COUNT" -eq 0 ]]; then
success "No images to remove"
exit 0
fi
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would remove $COUNT dangling image(s):"
docker images -f "dangling=true" --format " {{.ID}} {{.Size}} created {{.CreatedSince}}" 2>/dev/null
warn "DRY RUN — would remove $TARGET_LABEL:"
if [[ "$ALL_MODE" == true ]]; then
docker images --format " {{.Repository}}:{{.Tag}} {{.Size}} {{.CreatedSince}}" 2>/dev/null \
| grep -vF "$(docker ps --format '{{.Image}}' 2>/dev/null)" || true
else
docker images -f "dangling=true" \
--format " {{.ID}} {{.Size}} created {{.CreatedSince}}" 2>/dev/null
fi
exit 0
fi
OUTPUT=$(docker image prune -f 2>&1)
echo "$OUTPUT"
RECLAIMED=$(echo "$OUTPUT" | grep -E "^Total reclaimed" || echo "")
if [[ -n "$RECLAIMED" ]]; then
success "Done — $RECLAIMED"
else
success "Done — $COUNT image(s) removed"
fi
log "Pruning $TARGET_LABEL..."
OUTPUT=$(docker image prune $PRUNE_FLAGS -f 2>&1)
[[ "$ENABLE_LOGGING" == "true" ]] && echo "$OUTPUT" | sed 's/^/ /'
RECLAIMED=$(echo "$OUTPUT" | grep -E "^Total reclaimed" || echo "Total reclaimed space: unknown")
success "Done — $RECLAIMED"