scripts/prune.sh

#!/usr/bin/env bash
# scripts/prune.sh -- clean dangling docker images and build cache, but
# never touch anything a running stack references.
#
# I originally ran 'docker system prune -af' and discovered the next day
# that 'af' will helpfully delete the base image for a stopped but still
# defined service. This script is the defensive version.
#
# Usage:
#   scripts/prune.sh              # safe defaults (dangling only)
#   scripts/prune.sh --deep       # also remove unused volumes (asks first)
# See mercemay.top/src/homelab-compose/ for recovery from an accidental nuke.

set -euo pipefail

DEEP=0
while (( $# )); do
  case "$1" in
    --deep) DEEP=1; shift ;;
    -h|--help) sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
    *) echo "prune: unknown arg $1" >&2; exit 2 ;;
  esac
done

confirm() {
  local msg=$1
  read -r -p "$msg [y/N] " ans
  [[ $ans == [yY] ]]
}

list_in_use_images() {
  # Images that any compose file under stacks/ still names. Safer than
  # docker ps because it survives 'compose down'.
  find stacks -name 'compose.yml' -print0 |
    xargs -0 -I{} docker compose -f {} config --images 2>/dev/null |
    sort -u
}

echo "Dangling images:"
docker images --filter dangling=true --format '  {{.ID}}  {{.Repository}}:{{.Tag}}'
if docker images --filter dangling=true -q | grep -q .; then
  confirm "Remove the above dangling images?" && docker image prune -f
fi

echo
echo "Stopped containers:"
docker ps -aq --filter status=exited --filter status=created |
  xargs -r docker inspect --format '  {{.Id}} {{.Name}} ({{.Config.Image}})'
if confirm "Remove exited containers?"; then
  docker container prune -f
fi

echo
echo "Build cache size:"
docker builder du
if confirm "Prune build cache older than 168h?"; then
  docker builder prune --filter 'until=168h' -f
fi

if (( DEEP )); then
  echo
  echo "Volumes not referenced by any stack compose file:"
  mapfile -t KEEP < <(list_in_use_images)
  # Volumes are a separate namespace; we just print them and defer to
  # docker's own dangling filter. Printing KEEP here so you see what we
  # consider "in use" if something surprising shows up later.
  printf '  (in use by stacks: %s)\n' "${KEEP[*]:-none}"
  docker volume ls --filter dangling=true
  confirm "Remove dangling volumes?" && docker volume prune -f
fi

echo
echo "Disk after prune:"
docker system df