537 lines
15 KiB
Bash
Executable File
537 lines
15 KiB
Bash
Executable File
#!/bin/bash
|
|
# Docker Cleanup Script
|
|
# Enhanced version matching Go docwell functionality
|
|
|
|
set -euo pipefail
|
|
|
|
VERSION="2.6.2"
|
|
|
|
# Resolve script directory and source shared library
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
# shellcheck source=lib/common.sh
|
|
source "$SCRIPT_DIR/lib/common.sh"
|
|
|
|
# Enable debug trace after sourcing common.sh (so DEBUG variable is available)
|
|
enable_debug_trace
|
|
|
|
# CLI flags
|
|
CLEANUP_CONTAINERS=false
|
|
CLEANUP_IMAGES=false
|
|
CLEANUP_VOLUMES=false
|
|
CLEANUP_NETWORKS=false
|
|
CLEANUP_BUILDCACHE=false
|
|
CLEANUP_SYSTEM=false
|
|
CLEANUP_PACKAGES=false
|
|
CLEANUP_ALL=false
|
|
LOG_FILE="${LOG_FILE:-/tmp/docwell/docker-cleanup.log}"
|
|
|
|
# Load config file overrides
|
|
load_config
|
|
|
|
parse_args() {
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--version)
|
|
echo "docker-cleanup.sh v$VERSION"
|
|
exit 0
|
|
;;
|
|
--quiet|-q)
|
|
QUIET=true
|
|
shift
|
|
;;
|
|
--yes|-y)
|
|
YES=true
|
|
shift
|
|
;;
|
|
--containers)
|
|
CLEANUP_CONTAINERS=true
|
|
shift
|
|
;;
|
|
--images)
|
|
CLEANUP_IMAGES=true
|
|
shift
|
|
;;
|
|
--volumes)
|
|
CLEANUP_VOLUMES=true
|
|
shift
|
|
;;
|
|
--networks)
|
|
CLEANUP_NETWORKS=true
|
|
shift
|
|
;;
|
|
--buildcache)
|
|
CLEANUP_BUILDCACHE=true
|
|
shift
|
|
;;
|
|
--system)
|
|
CLEANUP_SYSTEM=true
|
|
shift
|
|
;;
|
|
--packages)
|
|
CLEANUP_PACKAGES=true
|
|
shift
|
|
;;
|
|
--all)
|
|
CLEANUP_ALL=true
|
|
shift
|
|
;;
|
|
--install-deps)
|
|
AUTO_INSTALL=true
|
|
shift
|
|
;;
|
|
--log)
|
|
LOG_FILE="$2"
|
|
shift 2
|
|
;;
|
|
--dry-run)
|
|
DRY_RUN=true
|
|
shift
|
|
;;
|
|
--debug|-d)
|
|
DEBUG=true
|
|
shift
|
|
;;
|
|
--verbose|-v)
|
|
VERBOSE=true
|
|
shift
|
|
;;
|
|
--help|-h)
|
|
show_help
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo -e "${RED}Unknown option: $1${NC}" >&2
|
|
show_help
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
show_help() {
|
|
cat << EOF
|
|
Docker Cleanup Script v$VERSION
|
|
|
|
Usage: $0 [OPTIONS]
|
|
|
|
Options:
|
|
--containers Remove stopped containers
|
|
--images Remove unused images
|
|
--volumes Remove unused volumes (DATA LOSS WARNING)
|
|
--networks Remove unused networks
|
|
--buildcache Clear build cache
|
|
--system System-wide cleanup
|
|
--packages Clean OS package cache
|
|
--all Run all cleanup operations
|
|
--install-deps Auto-install missing dependencies (requires root)
|
|
--log FILE Log file path (default: /tmp/docwell/docker-cleanup.log)
|
|
--dry-run Show what would be done without actually doing it
|
|
--debug, -d Enable debug mode (verbose output + xtrace)
|
|
--verbose, -v Enable verbose logging
|
|
--quiet, -q Suppress non-error output
|
|
--yes, -y Auto-confirm all prompts
|
|
--version Show version information
|
|
--help, -h Show this help message
|
|
|
|
Examples:
|
|
$0 --containers # Remove stopped containers
|
|
$0 --all --yes # Run all cleanups non-interactively
|
|
$0 --images --volumes # Remove images and volumes
|
|
$0 --all --dry-run # Preview cleanup without executing
|
|
EOF
|
|
}
|
|
|
|
show_docker_usage() {
|
|
log_header "Docker Disk Usage"
|
|
docker system df -v 2>/dev/null || {
|
|
log_error "Docker not running or not accessible"
|
|
exit 1
|
|
}
|
|
echo
|
|
}
|
|
|
|
cleanup_containers() {
|
|
log_header "Container Cleanup"
|
|
|
|
local stopped_count
|
|
stopped_count=$(docker ps -aq --filter status=exited 2>/dev/null | wc -l || echo "0")
|
|
|
|
if [[ "$stopped_count" == "0" ]] || [[ -z "$stopped_count" ]]; then
|
|
log_info "No stopped containers found"
|
|
echo
|
|
return 0
|
|
fi
|
|
|
|
if [[ "$QUIET" == "false" ]]; then
|
|
echo -e "${YELLOW}Stopped containers to remove ($stopped_count):${NC}"
|
|
docker ps -a --filter status=exited --format "table {{.Names}}\t{{.Image}}\t{{.Status}}" 2>/dev/null || true
|
|
echo
|
|
fi
|
|
|
|
if ! confirm "Remove stopped containers?"; then
|
|
log_info "Skipping container cleanup"
|
|
echo
|
|
return 0
|
|
fi
|
|
|
|
if run_or_dry "prune stopped containers" docker container prune -f &>/dev/null; then
|
|
log_success "Removed stopped containers"
|
|
else
|
|
log_error "Failed to remove containers"
|
|
return 1
|
|
fi
|
|
echo
|
|
}
|
|
|
|
cleanup_images() {
|
|
local remove_all="${1:-}"
|
|
log_header "Image Cleanup"
|
|
|
|
local dangling_count
|
|
dangling_count=$(docker images -f "dangling=true" -q 2>/dev/null | wc -l || echo "0")
|
|
|
|
if [[ "$dangling_count" != "0" ]] && [[ -n "$dangling_count" ]]; then
|
|
if [[ "$QUIET" == "false" ]]; then
|
|
echo -e "${YELLOW}Dangling images to remove ($dangling_count):${NC}"
|
|
docker images -f "dangling=true" --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" 2>/dev/null || true
|
|
echo
|
|
fi
|
|
|
|
if ! confirm "Remove dangling images?"; then
|
|
log_info "Skipping dangling image cleanup"
|
|
echo
|
|
return 0
|
|
fi
|
|
|
|
if run_or_dry "prune dangling images" docker image prune -f &>/dev/null; then
|
|
log_success "Removed dangling images"
|
|
else
|
|
log_error "Failed to remove dangling images"
|
|
return 1
|
|
fi
|
|
else
|
|
log_info "No dangling images found"
|
|
fi
|
|
|
|
if [[ "$QUIET" == "false" ]] && [[ "$remove_all" != "--all" ]]; then
|
|
local unused_count
|
|
unused_count=$(docker images -q 2>/dev/null | wc -l || echo "0")
|
|
if [[ "$unused_count" != "0" ]]; then
|
|
echo -e "${YELLOW}Unused images (showing first 20):${NC}"
|
|
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" 2>/dev/null | head -20 || true
|
|
echo
|
|
fi
|
|
fi
|
|
|
|
if [[ "$remove_all" != "--all" ]]; then
|
|
if ! confirm "Remove ALL unused images?"; then
|
|
log_info "Skipping unused image cleanup"
|
|
echo
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
if run_or_dry "prune all unused images" docker image prune -a -f &>/dev/null; then
|
|
log_success "Removed all unused images"
|
|
else
|
|
log_error "Failed to remove unused images"
|
|
return 1
|
|
fi
|
|
echo
|
|
}
|
|
|
|
cleanup_volumes() {
|
|
log_header "Volume Cleanup"
|
|
|
|
local unused_count
|
|
unused_count=$(docker volume ls -q --filter dangling=true 2>/dev/null | wc -l || echo "0")
|
|
|
|
if [[ "$unused_count" == "0" ]] || [[ -z "$unused_count" ]]; then
|
|
log_info "No unused volumes found"
|
|
echo
|
|
return 0
|
|
fi
|
|
|
|
if [[ "$QUIET" == "false" ]]; then
|
|
echo -e "${YELLOW}Unused volumes to remove ($unused_count):${NC}"
|
|
docker volume ls --filter dangling=true --format "table {{.Name}}\t{{.Driver}}" 2>/dev/null || true
|
|
echo
|
|
fi
|
|
|
|
log_warn "This will permanently delete volume data!"
|
|
if ! confirm "Remove unused volumes?"; then
|
|
log_info "Skipping volume cleanup"
|
|
echo
|
|
return 0
|
|
fi
|
|
|
|
if run_or_dry "prune unused volumes" docker volume prune -f &>/dev/null; then
|
|
log_success "Removed unused volumes"
|
|
else
|
|
log_error "Failed to remove volumes"
|
|
return 1
|
|
fi
|
|
echo
|
|
}
|
|
|
|
cleanup_networks() {
|
|
log_header "Network Cleanup"
|
|
|
|
if [[ "$QUIET" == "false" ]]; then
|
|
echo -e "${YELLOW}Unused networks:${NC}"
|
|
docker network ls --format "table {{.Name}}\t{{.Driver}}\t{{.Scope}}"
|
|
echo
|
|
fi
|
|
|
|
if ! confirm "Remove unused networks?"; then
|
|
log_info "Skipping network cleanup"
|
|
echo
|
|
return 0
|
|
fi
|
|
|
|
if run_or_dry "prune unused networks" docker network prune -f &>/dev/null; then
|
|
log_success "Removed unused networks"
|
|
else
|
|
log_error "Failed to remove networks"
|
|
return 1
|
|
fi
|
|
echo
|
|
}
|
|
|
|
cleanup_build_cache() {
|
|
log_header "Build Cache Cleanup"
|
|
|
|
if [[ "$QUIET" == "false" ]]; then
|
|
echo -e "${YELLOW}Build cache usage:${NC}"
|
|
docker system df 2>/dev/null | grep "Build Cache" || echo "No build cache info available"
|
|
echo
|
|
fi
|
|
|
|
if ! confirm "Clear build cache?"; then
|
|
log_info "Skipping build cache cleanup"
|
|
echo
|
|
return 0
|
|
fi
|
|
|
|
if run_or_dry "prune build cache" docker builder prune -f &>/dev/null; then
|
|
log_success "Cleared build cache"
|
|
else
|
|
log_error "Failed to clear build cache"
|
|
return 1
|
|
fi
|
|
echo
|
|
}
|
|
|
|
cleanup_system() {
|
|
log_header "System-wide Cleanup"
|
|
|
|
log_warn "This will remove:"
|
|
echo "- All stopped containers"
|
|
echo "- All networks not used by at least one container"
|
|
echo "- All dangling images"
|
|
echo "- All dangling build cache"
|
|
echo
|
|
|
|
if ! confirm "Perform system cleanup?"; then
|
|
log_info "Skipping system cleanup"
|
|
echo
|
|
return 0
|
|
fi
|
|
|
|
if ! run_or_dry "system prune" docker system prune -f &>/dev/null; then
|
|
log_error "Failed to prune system"
|
|
return 1
|
|
fi
|
|
|
|
log_success "System cleanup completed"
|
|
|
|
if confirm "Also remove unused images?"; then
|
|
if run_or_dry "aggressive system prune" docker system prune -a -f &>/dev/null; then
|
|
log_success "Aggressive cleanup completed"
|
|
else
|
|
log_error "Failed to prune system with images"
|
|
return 1
|
|
fi
|
|
fi
|
|
echo
|
|
}
|
|
|
|
cleanup_packages() {
|
|
log_header "Package Manager Cleanup"
|
|
|
|
if ! check_root; then
|
|
log_error "sudo privileges required for package cleanup"
|
|
echo
|
|
return 1
|
|
fi
|
|
|
|
if command -v pacman &> /dev/null; then
|
|
log_info "Detected Arch Linux"
|
|
|
|
local orphaned
|
|
orphaned=$(pacman -Qtdq 2>/dev/null || true)
|
|
if [[ -n "$orphaned" ]]; then
|
|
if [[ "$QUIET" == "false" ]]; then
|
|
echo -e "${YELLOW}Orphaned packages:${NC}"
|
|
echo "$orphaned"
|
|
echo
|
|
fi
|
|
|
|
if confirm "Remove orphaned packages?"; then
|
|
local orphaned_packages=()
|
|
while IFS= read -r pkg; do
|
|
[[ -n "$pkg" ]] && orphaned_packages+=("$pkg")
|
|
done < <(echo "$orphaned")
|
|
|
|
if (( ${#orphaned_packages[@]} > 0 )); then
|
|
if run_or_dry "remove orphaned packages" sudo pacman -Rns "${orphaned_packages[@]}" &>/dev/null; then
|
|
log_success "Orphaned packages removed"
|
|
else
|
|
log_warn "Failed to remove orphaned packages"
|
|
fi
|
|
fi
|
|
fi
|
|
else
|
|
log_info "No orphaned packages found"
|
|
fi
|
|
|
|
if confirm "Clear package cache?"; then
|
|
if run_or_dry "clear package cache" sudo pacman -Scc --noconfirm &>/dev/null; then
|
|
log_success "Package cache cleared"
|
|
else
|
|
log_error "Failed to clear package cache"
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
elif command -v apt &> /dev/null; then
|
|
log_info "Detected Debian/Ubuntu"
|
|
|
|
if confirm "Remove unused packages?"; then
|
|
if run_or_dry "autoremove packages" sudo apt autoremove -y &>/dev/null; then
|
|
log_success "Removed unused packages"
|
|
else
|
|
log_error "Failed to remove unused packages"
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
if confirm "Clean package cache?"; then
|
|
if run_or_dry "clean package cache" sudo apt autoclean &>/dev/null; then
|
|
log_success "Cache cleaned"
|
|
else
|
|
log_error "Failed to clean cache"
|
|
return 1
|
|
fi
|
|
fi
|
|
else
|
|
log_info "No supported package manager found"
|
|
fi
|
|
echo
|
|
}
|
|
|
|
cleanup_all() {
|
|
show_docker_usage
|
|
cleanup_containers
|
|
cleanup_images
|
|
cleanup_networks
|
|
cleanup_build_cache
|
|
|
|
if confirm "Also remove unused volumes? (DATA LOSS WARNING)"; then
|
|
cleanup_volumes
|
|
fi
|
|
|
|
if confirm "Also remove ALL unused images?"; then
|
|
cleanup_images --all
|
|
fi
|
|
|
|
if confirm "Also clean package cache?"; then
|
|
cleanup_packages
|
|
fi
|
|
}
|
|
|
|
main_menu() {
|
|
while true; do
|
|
log_header "Docker Cleanup Menu"
|
|
echo -e "${GRAY}DocWell Cleanup v$VERSION${NC}"
|
|
echo
|
|
echo "1) Show Docker disk usage"
|
|
echo "2) Clean containers (stopped)"
|
|
echo "3) Clean images (dangling/unused)"
|
|
echo "4) Clean volumes (unused)"
|
|
echo "5) Clean networks (unused)"
|
|
echo "6) Clean build cache"
|
|
echo "7) System-wide cleanup"
|
|
echo "8) Package manager cleanup"
|
|
echo "9) Run all cleanups"
|
|
echo "0) Exit"
|
|
echo
|
|
|
|
read -rp "Select option [0-9]: " choice
|
|
|
|
if [[ ! "$choice" =~ ^[0-9]$ ]]; then
|
|
log_error "Invalid option. Please enter a number between 0-9."
|
|
echo
|
|
read -rp "Press Enter to continue..."
|
|
clear_screen
|
|
continue
|
|
fi
|
|
|
|
case $choice in
|
|
1) show_docker_usage ;;
|
|
2) cleanup_containers ;;
|
|
3) cleanup_images ;;
|
|
4) cleanup_volumes ;;
|
|
5) cleanup_networks ;;
|
|
6) cleanup_build_cache ;;
|
|
7) cleanup_system ;;
|
|
8) cleanup_packages ;;
|
|
9) cleanup_all ;;
|
|
0)
|
|
log_info "Exiting..."
|
|
exit 0
|
|
;;
|
|
*)
|
|
log_error "Invalid option. Please try again."
|
|
;;
|
|
esac
|
|
|
|
echo
|
|
read -rp "Press Enter to continue..."
|
|
clear_screen
|
|
done
|
|
}
|
|
|
|
main() {
|
|
parse_args "$@"
|
|
check_docker || exit 1
|
|
|
|
# Handle CLI flags
|
|
if [[ "$CLEANUP_ALL" == "true" ]]; then
|
|
cleanup_all
|
|
exit 0
|
|
fi
|
|
|
|
[[ "$CLEANUP_CONTAINERS" == "true" ]] && cleanup_containers
|
|
[[ "$CLEANUP_IMAGES" == "true" ]] && cleanup_images
|
|
[[ "$CLEANUP_VOLUMES" == "true" ]] && cleanup_volumes
|
|
[[ "$CLEANUP_NETWORKS" == "true" ]] && cleanup_networks
|
|
[[ "$CLEANUP_BUILDCACHE" == "true" ]] && cleanup_build_cache
|
|
[[ "$CLEANUP_SYSTEM" == "true" ]] && cleanup_system
|
|
[[ "$CLEANUP_PACKAGES" == "true" ]] && cleanup_packages
|
|
|
|
# If no flags provided, run interactive menu
|
|
if [[ "$CLEANUP_CONTAINERS" == "false" ]] && \
|
|
[[ "$CLEANUP_IMAGES" == "false" ]] && \
|
|
[[ "$CLEANUP_VOLUMES" == "false" ]] && \
|
|
[[ "$CLEANUP_NETWORKS" == "false" ]] && \
|
|
[[ "$CLEANUP_BUILDCACHE" == "false" ]] && \
|
|
[[ "$CLEANUP_SYSTEM" == "false" ]] && \
|
|
[[ "$CLEANUP_PACKAGES" == "false" ]]; then
|
|
clear_screen
|
|
main_menu
|
|
fi
|
|
}
|
|
|
|
main "$@"
|