diff --git a/Tools/docker_prune_images.sh b/Tools/docker_prune_images.sh index 7b2c2a6..be7bd8f 100755 --- a/Tools/docker_prune_images.sh +++ b/Tools/docker_prune_images.sh @@ -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"