#!/usr/bin/env bash # # docker-cleanup.sh — bezpečné čištění Docker odpadu # # Cíle (vše s výslovným potvrzením nebo dry-run): # - dangling images (:) # - starý build cache (docker builder prune -f) # - opuštěné (orphaned) Docker sítě — nikdy bridge/host/none # - zastaralé images s tagem ale s referencí (REPO@sha256…) # - dočasné soubory (XDG_CACHE_HOME pro docker, /tmp/docker-*) # # Nikdy nemaže: # - používané images # - bind-mount ani named volume data # - běžící kontejnery # # Použití: # ./docker-cleanup.sh # zobrazí plán, zeptá se na souhlas # ./docker-cleanup.sh --dry-run # jen vypíše co by smazal # ./docker-cleanup.sh --auto # bez ptaní (default akce) # ./docker-cleanup.sh --dangling # jen dangling images # ./docker-cleanup.sh --build-cache # jen build cache # ./docker-cleanup.sh --networks # jen orphaned sítě # ./docker-cleanup.sh --all # všechno výše uvedené # set -euo pipefail # Default akce: žádná, vyžaduje explicitní přepínač DO_DANGLING=0 DO_BUILDER=0 DO_NETWORKS=0 DO_TMP=0 DO_ALL=0 DO_STOPPED=0 AUTO=0 DRY_RUN=0 usage() { sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//' } for arg in "$@"; do case "$arg" in --dry-run|-n) DRY_RUN=1 ;; --auto|-y) AUTO=1 ;; --dangling) DO_DANGLING=1 ;; --build-cache) DO_BUILDER=1 ;; --networks) DO_NETWORKS=1 ;; --tmp) DO_TMP=1 ;; --stopped-containers) DO_STOPPED=1 ;; --all) DO_ALL=1 ;; --help|-h) usage; exit 0 ;; *) echo "Unknown arg: $arg" >&2; exit 1 ;; esac done if [ "$DO_ALL" = "1" ]; then DO_DANGLING=1 DO_BUILDER=1 DO_NETWORKS=1 DO_TMP=1 fi # Bez přepínače: nic neprovádět if [ "$DO_DANGLING" = "0" ] && [ "$DO_BUILDER" = "0" ] && [ "$DO_NETWORKS" = "0" ] && [ "$DO_TMP" = "0" ] && [ "$DO_STOPPED" = "0" ]; then usage exit 0 fi # Barevné výstupy if [ -t 1 ]; then RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m' else RED=''; GREEN=''; YELLOW=''; BLUE=''; NC='' fi log() { printf '%s[%s]%s %s\n' "$GREEN" "$(date '+%H:%M:%S')" "$NC" "$*"; } warn() { printf '%s[%s]%s %s\n' "$YELLOW" "$(date '+%H:%M:%S')" "$NC" "$*"; } err() { printf '%s[%s]%s %s\n' "$RED" "$(date '+%H:%M:%S')" "$NC" "$*"; } # Kontrola dockeru if ! docker info >/dev/null 2>&1; then err "Docker není dostupný." exit 1 fi PLANNED_TOTAL=0 PLANNED_ITEMS=() show_plan_header() { printf '\n%s%s%s\n' "$BLUE" "=== PLÁN ČIŠTĚNÍ ===" "$NC" printf 'Datum: %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" printf 'Režim: %s\n' "$([ "$DRY_RUN" = "1" ] && echo "DRY-RUN" || echo "SKUTEČNÝ MAZÁNÍ")" printf '\n' } # === DANGLING IMAGES === plan_dangling() { if [ "$DO_DANGLING" = "0" ]; then return; fi printf '%s● Dangling images (:):%s\n' "$BLUE" "$NC" local pure_dangling=0 local stopped_holding=0 while IFS=$'\t' read -r id size; do [ -z "$id" ] && continue # Kolik kontejnerů (včetně zastavených) na tento image ukazuje? local holders holders=$(docker ps -a --filter "ancestor=$id" --format '{{.Names}}\t{{.Status}}' 2>/dev/null | head -3) if [ -n "$holders" ]; then stopped_holding=$((stopped_holding + 1)) printf ' %s %s ⚠ drží zastavený kontejner: %s\n' \ "$id" "$size" "$(echo "$holders" | head -1 | awk '{print $1}')" else pure_dangling=$((pure_dangling + 1)) printf ' %s %s\n' "$id" "$size" fi done < <(docker images -f "dangling=true" --format '{{.ID}}\t{{.Size}}') if [ "$pure_dangling" -eq 0 ] && [ "$stopped_holding" -eq 0 ]; then printf ' (žádné)\n' else printf ' -> %d čistě dangling, %d drží zastavené kontejnery (docker image prune -f je nesmaže)\n' \ "$pure_dangling" "$stopped_holding" if [ "$pure_dangling" -gt 0 ]; then PLANNED_TOTAL=$((PLANNED_TOTAL + pure_dangling)) PLANNED_ITEMS+=("dangling-images:$pure_dangling") fi if [ "$stopped_holding" -gt 0 ]; then PLANNED_ITEMS+=("dangling-with-stopped-containers:$stopped_holding") printf ' 💡 Pro smazání těchto images je nutné nejprve odstranit zastavené kontejnery (--stopped-containers)\n' fi fi } # === BUILD CACHE === plan_build_cache() { if [ "$DO_BUILDER" = "0" ]; then return; fi printf '%s● Build cache:%s\n' "$BLUE" "$NC" local size size=$(docker system df --format '{{.Type}}\t{{.Size}}' 2>/dev/null \ | awk -F'\t' '$1 == "Build Cache" {print $2}') if [ -z "$size" ]; then size="0B" fi printf ' Build cache: %s\n' "$size" if [ "$size" = "0B" ]; then printf ' (žádný ke smazání)\n' else PLANNED_TOTAL=$((PLANNED_TOTAL + 1)) PLANNED_ITEMS+=("build-cache:$size") fi } # === ORPHANED NETWORKS === plan_networks() { if [ "$DO_NETWORKS" = "0" ]; then return; fi printf '%s● Opuštěné Docker sítě:%s\n' "$BLUE" "$NC" local count=0 # Najdi sítě, které NEpoužívá žádný kontejner while IFS= read -r net; do [ -z "$net" ] && continue # Vynech výchozí systémové case "$net" in bridge|host|none|docker_gwbridge|docker-desktop*) continue ;; esac # Používá ji nějaký kontejner? if ! docker ps --format '{{.Networks}}' 2>/dev/null \ | grep -qw "$net"; then printf ' %s\n' "$net" count=$((count + 1)) fi done < <(docker network ls --format '{{.Name}}') if [ "$count" -eq 0 ]; then printf ' (žádné opuštěné)\n' else printf ' -> %d opuštěných sítí\n' "$count" PLANNED_TOTAL=$((PLANNED_TOTAL + count)) PLANNED_ITEMS+=("networks:$count") fi } # === TMP FILES === plan_tmp() { if [ "$DO_TMP" = "0" ]; then return; fi printf '%s● Dočasné soubory:%s\n' "$BLUE" "$NC" local total_bytes=0 local paths=() # /tmp/docker-* (logy docker-update atd., starší než 7 dní) if [ -d /tmp ]; then while IFS= read -r f; do [ -f "$f" ] || continue sz=$(stat -f '%z' "$f") total_bytes=$((total_bytes + sz)) paths+=("$f") done < <(/usr/bin/find /tmp -maxdepth 1 -name 'docker-*' -type f -mtime +7 2>/dev/null) fi # Docker cache adresáře [ -d "$HOME/.docker/buildx" ] && paths+=("$HOME/.docker/buildx") [ -d "$HOME/.cache/docker" ] && paths+=("$HOME/.cache/docker") if [ "${#paths[@]}" -eq 0 ]; then printf ' (žádné)\n' else for p in "${paths[@]}"; do printf ' %s\n' "$p" done printf ' -> %d položek, cca %d B\n' "${#paths[@]}" "$total_bytes" PLANNED_TOTAL=$((PLANNED_TOTAL + ${#paths[@]})) PLANNED_ITEMS+=("tmp-files:${#paths[@]}") fi } confirm() { if [ "$AUTO" = "1" ]; then return 0 fi if [ "$DRY_RUN" = "1" ]; then return 0 fi printf '\n%sPokračovat? [y/N]%s ' "$YELLOW" "$NC" read -r ans case "$ans" in y|Y|yes|YES) return 0 ;; *) printf 'Zrušeno.\n'; exit 1 ;; esac } # === STOPPED CONTAINERS === plan_stopped() { if [ "$DO_STOPPED" = "0" ]; then return; fi printf '%s● Zastavené kontejnery:%s\n' "$BLUE" "$NC" local count=0 while IFS= read -r line; do [ -z "$line" ] && continue # Formát: NAME STATUS printf ' %s\n' "$line" count=$((count + 1)) done < <(docker ps -a --filter "status=exited" --format '{{.Names}}\t{{.Status}}' 2>/dev/null) if [ "$count" -eq 0 ]; then printf ' (žádné)\n' else printf ' -> %d zastavených kontejnerů\n' "$count" PLANNED_TOTAL=$((PLANNED_TOTAL + count)) PLANNED_ITEMS+=("stopped-containers:$count") fi } # Spustit plánování show_plan_header plan_dangling plan_build_cache plan_networks plan_stopped plan_tmp printf '\n%sCelkem plánovaných akcí: %d%s\n' "$BLUE" "$PLANNED_TOTAL" "$NC" if [ "$PLANNED_TOTAL" -eq 0 ]; then log "Není co čistit." exit 0 fi # Bez potvrzení v dry-run režimu if [ "$DRY_RUN" = "1" ]; then printf '\n%sDRY-RUN: žádná akce neprovedena%s\n' "$YELLOW" "$NC" exit 0 fi confirm # === EXECUTE === log "" log "=== PROVÁDÍM ČIŠTĚNÍ ===" if [ "$DO_DANGLING" = "1" ]; then log "docker image prune -f (dangling only)" docker image prune -f 2>&1 || warn "dangling prune selhal" fi if [ "$DO_BUILDER" = "1" ]; then log "docker builder prune -f" docker builder prune -f 2>&1 || warn "builder prune selhal" fi if [ "$DO_NETWORKS" = "1" ]; then log "docker network prune -f (filtrováno na opuštěné)" # docker network prune umí vyfiltrovat, ale my jsme již identifikovali konkrétní while IFS= read -r net; do case "$net" in bridge|host|none|docker_gwbridge|docker-desktop*) continue ;; esac if ! docker ps --format '{{.Networks}}' 2>/dev/null | grep -qw "$net"; then log " rm network: $net" docker network rm "$net" 2>&1 || warn " rm $net selhal" fi done < <(docker network ls --format '{{.Name}}') fi if [ "$DO_TMP" = "1" ]; then log "mazání starých /tmp/docker-* (≥7 dní)" # -print -delete musí být v tomto pořadí; -delete sám o sobě na macOS BSD find někdy selže /usr/bin/find /tmp -maxdepth 1 -name 'docker-*' -type f -mtime +7 -print 2>&1 | while read -r f; do log " rm: $f" /bin/rm -f "$f" 2>&1 || warn " rm $f selhal" done fi if [ "$DO_STOPPED" = "1" ]; then log "docker container prune -f (zastavené kontejnery)" docker container prune -f 2>&1 || warn "container prune selhal" fi log "" log "=== STAV PO ČIŠTĚNÍ ===" docker system df 2>&1 | head -20 log "" log "HOTOVO. Uvolněno viz výše ^"