#!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' # Script Name: docker-image-manager.v1.sh # Beschreibung: Docker Image Manager - verwaltet und inspiziert Docker Images # - Alle Images auflisten # - Ungenutzte Images anzeigen # - Zeigt zugeordnete Container pro Image # - Images interaktiv loeschen # Autor: Patrick Asmus # Web: https://www.cleveradmin.de # Git-Reposit.: https://git.techniverse.net/scriptos/docker-image-manager # Version: 1.0.0 # Datum: 18.02.2026 # Modifikation: Initialer Release (v1.0.0) ##################################################### LAST_IDS=() LAST_LABELS=() declare -A IMAGE_TO_CONTAINERS=() SELECTED_IDS=() SELECTED_LABELS=() require_docker() { if ! command -v docker >/dev/null 2>&1; then echo "Docker CLI nicht gefunden. Bitte Docker installieren und PATH pruefen." >&2 exit 1 fi if ! docker info >/dev/null 2>&1; then echo "Docker Daemon nicht erreichbar. Bitte Docker starten." >&2 exit 1 fi } load_container_map() { IMAGE_TO_CONTAINERS=() local -a lines mapfile -t lines < <(docker ps -a -q | xargs -r docker inspect --format '{{.Id}}|{{.Image}}|{{.Name}}') if [[ ${#lines[@]} -eq 0 ]]; then return 0 fi local line image_id name for line in "${lines[@]}"; do IFS='|' read -r _ image_id name <<< "$line" name=${name#/} if [[ -n "${IMAGE_TO_CONTAINERS[$image_id]:-}" ]]; then IMAGE_TO_CONTAINERS[$image_id]="${IMAGE_TO_CONTAINERS[$image_id]},${name}" else IMAGE_TO_CONTAINERS[$image_id]="$name" fi done } print_images_table() { if [[ ${#LAST_IDS[@]} -eq 0 ]]; then echo "Keine Images gefunden." return 0 fi load_container_map printf "%-4s %-30s %-20s %-15s %-12s %-20s %-30s\n" "Nr." "REPOSITORY" "TAG" "IMAGE ID" "SIZE" "CREATED" "CONTAINERS" printf "%-4s %-30s %-20s %-15s %-12s %-20s %-30s\n" "----" "------------------------------" "--------------------" "---------------" "------------" "--------------------" "------------------------------" for i in "${!LAST_IDS[@]}"; do IFS='|' read -r repo tag short_id size created <<< "${LAST_LABELS[$i]}" local containers="${IMAGE_TO_CONTAINERS[${LAST_IDS[$i]}]:-}" if [[ -z "$containers" ]]; then containers="-" fi printf "%-4s %-30s %-20s %-15s %-12s %-20s %-30s\n" "$((i + 1))" "$repo" "$tag" "$short_id" "$size" "$created" "$containers" done } load_images_from_lines() { local -a lines=("$@") LAST_IDS=() LAST_LABELS=() if [[ ${#lines[@]} -eq 0 ]]; then return 0 fi local line id repo tag created size short_id for line in "${lines[@]}"; do IFS='|' read -r id repo tag created size <<< "$line" short_id=${id:0:12} LAST_IDS+=("$id") LAST_LABELS+=("$repo|$tag|$short_id|$size|$created") done } list_all_images() { local -a lines mapfile -t lines < <(docker images --no-trunc --format '{{.ID}}|{{.Repository}}|{{.Tag}}|{{.CreatedSince}}|{{.Size}}') load_images_from_lines "${lines[@]}" print_images_table } list_unused_images() { local used_file used_file=$(mktemp) if docker ps -a -q >/dev/null 2>&1; then docker ps -a -q | xargs -r docker inspect --format '{{.Image}}' | sort -u > "$used_file" fi local -a lines mapfile -t lines < <(docker images --no-trunc --format '{{.ID}}|{{.Repository}}|{{.Tag}}|{{.CreatedSince}}|{{.Size}}') LAST_IDS=() LAST_LABELS=() local line id repo tag created size short_id for line in "${lines[@]}"; do IFS='|' read -r id repo tag created size <<< "$line" if [[ ! -s "$used_file" ]] || ! grep -Fxq "$id" "$used_file"; then short_id=${id:0:12} LAST_IDS+=("$id") LAST_LABELS+=("$repo|$tag|$short_id|$size|$created") fi done rm -f "$used_file" print_images_table } confirm_action() { local prompt="$1" local answer read -r -e -p "$prompt [y/N]: " answer case "$answer" in y|Y|yes|YES) return 0 ;; *) return 1 ;; esac } select_images_by_numbers() { local -a numbers=() local input="${1:-}" SELECTED_IDS=() SELECTED_LABELS=() if [[ -z "$input" ]]; then read -r -e -p "Nummern der Images (z.B. 1,3,5) oder Enter fuer Abbruch: " input fi if [[ -z "$input" ]]; then echo "Abgebrochen." return 1 fi input=${input//,/ } for token in $input; do if [[ "$token" =~ ^[0-9]+$ ]]; then numbers+=("$token") fi done if [[ ${#numbers[@]} -eq 0 ]]; then echo "Keine gueltigen Nummern angegeben." return 1 fi local idx for idx in "${numbers[@]}"; do if (( idx < 1 || idx > ${#LAST_IDS[@]} )); then echo "Nummer $idx ist ungueltig." return 1 fi SELECTED_IDS+=("${LAST_IDS[$((idx - 1))]}") SELECTED_LABELS+=("${LAST_LABELS[$((idx - 1))]}") done return 0 } print_selected_images() { if [[ ${#SELECTED_IDS[@]} -eq 0 ]]; then return 0 fi printf "%-4s %-30s %-20s %-15s %-12s %-20s\n" "Nr." "REPOSITORY" "TAG" "IMAGE ID" "SIZE" "CREATED" printf "%-4s %-30s %-20s %-15s %-12s %-20s\n" "----" "------------------------------" "--------------------" "---------------" "------------" "--------------------" local i for i in "${!SELECTED_IDS[@]}"; do IFS='|' read -r repo tag short_id size created <<< "${SELECTED_LABELS[$i]}" printf "%-4s %-30s %-20s %-15s %-12s %-20s\n" "$((i + 1))" "$repo" "$tag" "$short_id" "$size" "$created" done } remove_image_by_id() { local id="$1" local -a used mapfile -t used < <(docker ps -a --filter "ancestor=$id" --format '{{.ID}}|{{.Names}}') if [[ ${#used[@]} -gt 0 ]]; then echo "Ueberspringe Image (in Benutzung durch Container): $id" local entry for entry in "${used[@]}"; do echo "- ${entry#*|} (${entry%%|*})" done return fi local -a tags=() mapfile -t tags < <(docker image inspect "$id" --format '{{range .RepoTags}}{{println .}}{{end}}') if [[ ${#tags[@]} -eq 0 ]]; then echo "Loesche Image: $id" docker rmi "$id" return fi local tag local -a tag_list=() for tag in "${tags[@]}"; do if [[ "$tag" == ":" || -z "$tag" ]]; then continue fi tag_list+=("$tag") done if [[ ${#tag_list[@]} -eq 0 ]]; then echo "Loesche Image: $id" docker rmi "$id" return fi for tag in "${tag_list[@]}"; do echo "Loesche Tag: $tag" docker rmi "$tag" done if docker image inspect "$id" >/dev/null 2>&1; then echo "Loesche Image: $id" docker rmi "$id" fi } delete_selected_images() { local -A seen=() local id for id in "${SELECTED_IDS[@]}"; do if [[ -n "${seen[$id]:-}" ]]; then continue fi seen[$id]=1 remove_image_by_id "$id" done } inspect_image_by_number() { local num="${1:-}" if [[ -z "$num" ]]; then read -r -e -p "Nummer des Images: " num fi if ! [[ "$num" =~ ^[0-9]+$ ]]; then echo "Ungueltige Nummer." return 1 fi if (( num < 1 || num > ${#LAST_IDS[@]} )); then echo "Nummer $num ist ungueltig." return 1 fi local id id="${LAST_IDS[$((num - 1))]}" docker image inspect "$id" --format \ $'ID: {{.Id}}\nRepoTags: {{.RepoTags}}\nCreated: {{.Created}}\nSize: {{.Size}}\nArchitecture: {{.Architecture}}\nOS: {{.Os}}\nLabels: {{json .Config.Labels}}' echo echo "Containers:" local -a lines mapfile -t lines < <(docker ps -a --filter "ancestor=$id" --format '{{.ID}}|{{.Names}}|{{.Status}}') if [[ ${#lines[@]} -eq 0 ]]; then echo "- keine" return 0 fi local line cid name status for line in "${lines[@]}"; do IFS='|' read -r cid name status <<< "$line" echo "- $name ($cid) - $status" done } purge_images() { local mode="$1" local force_yes="$2" case "$mode" in unused) list_unused_images if [[ ${#LAST_IDS[@]} -eq 0 ]]; then return 0 fi ;; *) echo "Unbekannter Modus: $mode" return 1 ;; esac if [[ ${#LAST_IDS[@]} -eq 0 ]]; then return 0 fi if [[ "$force_yes" != "true" ]]; then if ! confirm_action "Alle aufgelisteten Images loeschen?"; then echo "Abgebrochen." return 0 fi fi local -A seen=() local id for id in "${LAST_IDS[@]}"; do if [[ -n "${seen[$id]:-}" ]]; then continue fi seen[$id]=1 remove_image_by_id "$id" done } print_help() { echo "Docker Image Manager" echo echo "Verwendung:" echo " bash ./docker-image-manager.v1.sh [BEFEHL] [OPTIONS]" echo echo "Befehle:" echo " list - Alle Docker Images auflisten" echo " unused-images - Images anzeigen, die von keinem Container genutzt werden" echo " inspect - Details zu einem Image anzeigen (inkl. Containerliste)" echo " delete - Einzelne Images nach Nummer loeschen" echo " purge-unused - Alle ungenutzten Images loeschen" echo " help, -h, --help - Diese Hilfe anzeigen" echo echo "Optionen:" echo " --yes - Loeschen ohne Rueckfrage" } interactive_menu() { while true; do echo echo "Docker Image Manager" echo "1) Images anzeigen + Details" echo "2) Ungenutzte Images anzeigen" echo "3) Image loeschen (per Nummer)" echo "4) Alle ungenutzten Images loeschen" echo "0) Beenden" read -r -e -p "Auswahl: " choice case "$choice" in 1) list_all_images if [[ ${#LAST_IDS[@]} -eq 0 ]]; then continue fi read -r -e -p "Nummer fuer Details (Enter fuer zurueck): " detail_num if [[ -n "$detail_num" ]]; then inspect_image_by_number "$detail_num" fi ;; 2) list_unused_images ;; 3) list_all_images if [[ ${#LAST_IDS[@]} -eq 0 ]]; then continue fi if ! select_images_by_numbers ""; then continue fi echo "Ausgewaehlte Images:" print_selected_images if confirm_action "Ausgewaehlte Images loeschen?"; then delete_selected_images else echo "Abgebrochen." fi ;; 4) purge_images "unused" "false" ;; 0) echo "Tschuess." exit 0 ;; *) echo "Ungueltige Auswahl." ;; esac done } main() { require_docker local cmd="" local force_yes="false" local -a extra_args=() for arg in "$@"; do case "$arg" in --yes) force_yes="true" ;; -h|--help|help) cmd="help" ;; *) if [[ -z "$cmd" ]]; then cmd="$arg" else extra_args+=("$arg") fi ;; esac done if [[ -z "$cmd" ]]; then interactive_menu return 0 fi case "$cmd" in list) list_all_images ;; unused-images) list_unused_images ;; inspect) list_all_images if [[ ${#LAST_IDS[@]} -eq 0 ]]; then return 0 fi inspect_image_by_number "${extra_args[0]:-}" ;; delete) list_all_images if [[ ${#LAST_IDS[@]} -eq 0 ]]; then return 0 fi if ! select_images_by_numbers "${extra_args[*]:-}"; then return 1 fi echo "Ausgewaehlte Images:" print_selected_images if [[ "$force_yes" != "true" ]]; then if ! confirm_action "Ausgewaehlte Images loeschen?"; then echo "Abgebrochen." return 0 fi fi delete_selected_images ;; purge-unused) purge_images "unused" "$force_yes" ;; help|*) print_help ;; esac } main "$@"