Headers claimed protections the code never had, and several destructive paths had no guard against a collapsed config value.
231 lines
10 KiB
Bash
Executable File
231 lines
10 KiB
Bash
Executable File
#!/bin/bash
|
|
# ==============================================================================================
|
|
# ============================= Docker Prune Images ============================================
|
|
# ==============================================================================================
|
|
#
|
|
# PURPOSE
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Removes orphaned Docker images. Two modes:
|
|
#
|
|
# 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.
|
|
#
|
|
# ==============================================================================================
|
|
# DESIGN PRINCIPLES
|
|
# ==============================================================================================
|
|
#
|
|
# Safe Default, Explicit Escalation
|
|
# The default mode (dangling only) is always safe — running containers are
|
|
# never affected. The --all mode requires deliberate opt-in and carries an
|
|
# explicit caution in the description, because it removes stopped containers
|
|
# that may be intentionally paused.
|
|
#
|
|
# ==============================================================================================
|
|
# OPERATIONAL MODEL
|
|
# ==============================================================================================
|
|
#
|
|
# Default (dangling only):
|
|
# docker image prune -f — removes untagged, unreferenced images only. Every tagged
|
|
# image survives, running containers are untouched, and nothing stopped is removed.
|
|
#
|
|
# --all (full orphan cleanup):
|
|
# 1. Remove containers in exited/created state
|
|
# 2. Remove every image not used by a RUNNING container
|
|
# Step 1 is what makes step 2 reach further: with the stopped containers gone, their
|
|
# images are no longer referenced and become eligible. That is also precisely why
|
|
# --all is destructive to anything deliberately kept stopped.
|
|
#
|
|
# Reclaimed space is reported for both modes.
|
|
#
|
|
# ==============================================================================================
|
|
# CONFIGURATION
|
|
# ==============================================================================================
|
|
#
|
|
# This tool is deliberately unconfigured — it reads live Docker state rather than any
|
|
# configured list, so there is nothing host-specific to alias and no thresholds to tune.
|
|
# Scope is controlled entirely by the mode flag (default vs --all).
|
|
#
|
|
# Note it does not call detect_hosts(): nothing here is host-specific, and it acts only on
|
|
# the local daemon.
|
|
#
|
|
# ==============================================================================================
|
|
# OPERATIONAL SAFEGUARDS
|
|
# ==============================================================================================
|
|
#
|
|
# Root Enforcement
|
|
# Docker prune operations require root.
|
|
#
|
|
# Lock Acquisition
|
|
# acquire_lock prevents concurrent prune runs racing on the same image store.
|
|
#
|
|
# Dangling-Only Default
|
|
# The default mode removes untagged, unreferenced images only. Reaching anything
|
|
# tagged, running, or deliberately stopped requires --all explicitly — the safe
|
|
# behaviour is what you get by not thinking about it.
|
|
#
|
|
# Running Containers Never Touched
|
|
# Neither mode removes a running container or an image a running container uses.
|
|
# --all widens the blast radius to STOPPED containers and their images, never to
|
|
# anything currently up.
|
|
#
|
|
# Dry Run Support
|
|
# --dry-run lists everything that would be removed and removes nothing. Worth using
|
|
# before --all specifically, since that mode deletes intentionally-stopped containers.
|
|
#
|
|
# Status Mode
|
|
# --status lists current dangling images and stopped containers without changing
|
|
# anything, so the scope of a prospective --all is visible up front.
|
|
#
|
|
# ==============================================================================================
|
|
# RUNTIME MODES
|
|
# ==============================================================================================
|
|
#
|
|
# docker_prune_images.sh
|
|
# 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 making changes.
|
|
#
|
|
# docker_prune_images.sh --status
|
|
# Show dangling images and stopped containers. No changes.
|
|
#
|
|
# ==============================================================================================
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
source "$SCRIPT_DIR/../load_config.sh"
|
|
|
|
# ── 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 ━━━
|
|
# ==============================================================================================
|
|
if [[ "$EUID" -ne 0 ]]; then
|
|
error "Must be run as root"
|
|
exit 1
|
|
fi
|
|
|
|
acquire_lock
|
|
|
|
# ==============================================================================================
|
|
# ━━━ Status ━━━
|
|
# ==============================================================================================
|
|
if [[ "$SHOW_STATUS" == true ]]; then
|
|
echo ""
|
|
echo "━━━━━ $ICON_SUMMARY DOCKER PRUNE IMAGES STATUS ━━━━━"
|
|
|
|
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 " none"
|
|
else
|
|
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
|
|
|
|
# ==============================================================================================
|
|
# ━━━ Run ━━━
|
|
# ==============================================================================================
|
|
MODE_LABEL=$([[ "$ALL_MODE" == true ]] && echo "full orphan cleanup" || echo "dangling only")
|
|
log "$ICON_GEAR Config: mode=${MODE_LABEL} dry-run=${DRY_RUN}"
|
|
echo "━━━ $ICON_CONTAINERS Docker Prune Images ($MODE_LABEL) — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
|
|
|
|
TOTAL_RECLAIMED=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
|
|
echo "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
|
|
echo "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
|
|
|
|
# ── 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 $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
|
|
|
|
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"
|