From 7455df03b2ada35274321dac42d25ca12918cda2 Mon Sep 17 00:00:00 2001 From: Till JS Date: Tue, 26 May 2026 18:14:32 +0200 Subject: [PATCH] =?UTF-8?q?fix(mac-mini):=20Watchdog=20env-sicher=20?= =?UTF-8?q?=E2=80=94=20start/restart=20statt=20rm+compose-up?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Behebt den Blank-Secrets-Bug der vorigen Rewrites (d5c0c020d/2448a50cc): deren label-getriebenes rm+compose-up hat für ${VAR}-Interpolations-Apps (Secrets aus der Deploy-Shell, nicht env_file) LEERE Secrets einkompiliert, weil der launchd-Watchdog diese Vars nicht hat. So wurde comicello-api am 2026-05-26 zerlegt (Postgres-PW + MANA_SERVICE_KEY leer → 28P01) — dieselbe Falle wie mana-auth KEK 2026-04-08. Sichere Architektur: - Bestehende Container heilen NUR via `docker start` (stuck/exited) bzw. `docker restart` (crash-loop, Backoff) → behält die einkompilierte Env, kann nie Secrets leeren. Projektübergreifend (auch nicht-mana-*), via Restart-Policy-Gate (nur always/unless-stopped; watchtower etc. nie). - Compose-recreate NUR für fehlende mana-core-Container — sicher, weil Core seine Env via env_file (+ co-located .env) im Compose-Dir trägt; zusätzlich --no-build (Watchdog baut nie ein Image → kein OOM-Crash). - Komplett fehlende App-Container (${VAR}-Apps) werden NICHT auto-neu- erstellt (Blank-Secret-Risiko) — brauchen echten Re-Deploy. - DRY_RUN side-effect-frei. bash -n + DRY_RUN + Live-Lauf auf dem Server grün. Verifiziert: docker compose/rm nur in der Core-Reconcile, App-Recovery nur start/restart. Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/mac-mini/ensure-containers-running.sh | 251 ++++++------------ 1 file changed, 85 insertions(+), 166 deletions(-) diff --git a/scripts/mac-mini/ensure-containers-running.sh b/scripts/mac-mini/ensure-containers-running.sh index ca20bf3ab..7c12aa610 100755 --- a/scripts/mac-mini/ensure-containers-running.sh +++ b/scripts/mac-mini/ensure-containers-running.sh @@ -1,32 +1,35 @@ #!/bin/bash # Mana Container Health Enforcer -# Stellt sicher, dass alle Container, die laufen SOLLTEN, auch laufen. +# Stellt sicher, dass Container, die laufen SOLLTEN, auch laufen. +# +# SICHERHEITS-LEITPRINZIP (nach dem Vorfall 2026-05-26): Recovery +# BESTEHENDER Container NUR via `docker start`/`docker restart` — das +# behaelt die bereits einkompilierte Container-Env. NIEMALS `docker rm` +# + `docker compose up` zur Recovery: viele Apps setzen Secrets per +# `${VAR}`-Interpolation aus der Deploy-Shell (nicht via env_file); der +# launchd-Watchdog hat diese Vars NICHT -> ein compose-up wuerde LEERE +# Secrets einkompilieren. Genau so wurde comicello-api (Postgres-PW + +# MANA_SERVICE_KEY) am 2026-05-26 zerlegt — und frueher mana-auth (KEK, +# 2026-04-08). Der fruehere Rewrite, der label-getrieben rm+compose-up +# machte, ist deshalb zurueckgenommen. # # Erkennt und heilt: -# - Exited/Created mit Restart-Policy always/unless-stopped -> recover -# - Crash-Loop (Restarting) -> recreate (Backoff) -# - FEHLENDE mana-core-Container (gar kein Container da) -> aus Core-Compose neu erstellen +# - Exited/Created (restart always/unless-stopped) -> docker start +# - Crash-Loop (Restarting) -> docker restart (Backoff) +# - FEHLENDE mana-core-Container -> aus Core-Compose neu +# erstellen. Sicher, weil mana-core seine Env via `env_file:` (+ co- +# located .env) IM Compose-Verzeichnis traegt -> compose laedt sie +# unabhaengig von der Shell. Ein KOMPLETT fehlender App-Container (${VAR}- +# Interpolation) wird NICHT auto-neu-erstellt (Blank-Secret-Risiko) — +# das braucht einen echten Re-Deploy; der Watchdog flaggt es nur. # -# Recovery ist LABEL-GETRIEBEN: jeder Container wird über SEIN EIGENES -# Compose-Projekt/-Config (Docker-Labels com.docker.compose.*) wieder -# hochgefahren. Dadurch funktioniert die Heilung projektübergreifend -# (mana-core, managarten, sowie eigenständige App-Stacks wie nutriphi/ -# viadocu/zitare) — nicht mehr nur gegen die eine managarten-Compose. -# -# Bewusst gestoppte Container (Restart-Policy "no", z.B. watchtower) werden -# NIE auto-gestartet. One-Shot-Init-Container werden übersprungen. -# -# Lücken-Historie (2026-05-26, project_uptime_hardening): nach einem -# VM-Crash kam `mana-auth` (Core, fehlte komplett) nicht zurück und -# nicht-`mana-*`-Apps (viadocu/nutriphi) wurden gar nicht abgedeckt — -# beides fixt diese Version. +# Bewusst gestoppte (restart=no, z.B. watchtower) + One-Shot-Init: nie +# anfassen. DRY_RUN=1 -> loggt nur, veraendert nichts. # # Run via LaunchD alle 5 Minuten oder nach Boot. -# DRY_RUN=1 -> loggt nur die geplanten Aktionen, führt nichts aus. set -e -# Ensure PATH includes docker export PATH="/usr/local/bin:/opt/homebrew/bin:$PATH" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -35,21 +38,19 @@ LOG_FILE="/tmp/mana-container-health.log" RESTART_TRACKER="/tmp/mana-restart-tracker" DRY_RUN="${DRY_RUN:-0}" -# mana-core (Plattform-Kern) — eigenes Compose-Projekt seit der Core- -# Isolation (project_uptime_hardening / CORE_ISOLATION.md). Pfad ist -# serverseitig. Gebraucht für die "fehlender Core-Container"-Reconciliation, -# weil ein komplett fehlender Container keine Labels zum Auslesen hat. +# mana-core (Plattform-Kern) — eigenes Compose-Projekt. Traegt seine Env via +# env_file (+ co-located .env) im Compose-Dir, daher ist compose-up vom +# Watchdog hier sicher. Pfad serverseitig. MANA_CORE_PROJECT="mana-core" MANA_CORE_COMPOSE="/Users/mana/projects/mana-platform/infrastructure/core/docker-compose.core.yml" -# Wartungs-Lock (geteilt mit dem colima-Guard): pausiert die aggressivste -# Aktion — das Neu-Erstellen fehlender Core-Container —, damit bewusste -# Stop/Debug-Sessions nicht unterlaufen werden. +# Wartungs-Lock (geteilt mit dem colima-Guard): pausiert das Neu-Erstellen +# fehlender Core-Container, damit bewusste Stop/Debug-Sessions nicht +# unterlaufen werden. MAINT_LOCK="/tmp/mana-colima-maintenance" -# Container, die nach einem One-Shot-Job legitim "exited" sind — NICHT heilen -# (sonst Log-Spam + unnötiges Re-Run des Init-Jobs alle 5 min). Zusätzlich -# greift das Restart-Policy-Gate (One-Shots nutzen i.d.R. "no"/"on-failure"). +# Container, die nach einem One-Shot-Job legitim "exited" sind — NICHT heilen. +# (Zusaetzlich greift das Restart-Policy-Gate: One-Shots nutzen no/on-failure.) ONESHOT_INIT_CONTAINERS=( mana-infra-minio-init ) @@ -62,9 +63,9 @@ is_oneshot_init() { return 1 } -# Soll dieser Container automatisch (wieder-)gestartet werden? Nur wenn seine -# Restart-Policy ihn als "Dauerläufer" markiert. So fassen wir bewusst -# gestoppte (restart=no, z.B. watchtower) und One-Shot-Jobs nie an. +# Nur Container mit Dauerlaeufer-Restart-Policy auto-(wieder-)starten. So +# fassen wir bewusst gestoppte (restart=no, z.B. watchtower) und One-Shot- +# Jobs nie an. should_autostart() { local pol pol=$(docker inspect "$1" --format '{{.HostConfig.RestartPolicy.Name}}' 2>/dev/null || echo "") @@ -83,11 +84,11 @@ log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" } -# Führt einen Befehl aus — oder loggt ihn nur im DRY_RUN. Bricht das Skript +# Fuehrt einen Befehl aus — oder loggt ihn nur im DRY_RUN. Bricht das Skript # bei Fehler NICHT ab (Recovery soll weiterlaufen). run() { if [ "$DRY_RUN" = "1" ]; then - log " [DRY_RUN] würde ausführen: $*" + log " [DRY_RUN] wuerde ausfuehren: $*" return 0 fi "$@" >>"$LOG_FILE" 2>&1 || { @@ -100,7 +101,6 @@ send_notification() { local message="$1" local priority="${2:-default}" - # Telegram if [ -n "$TELEGRAM_BOT_TOKEN" ] && [ -n "$TELEGRAM_CHAT_ID" ]; then curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ -d "chat_id=${TELEGRAM_CHAT_ID}" \ @@ -109,7 +109,6 @@ send_notification() { >/dev/null 2>&1 || true fi - # ntfy if [ -n "$NTFY_TOPIC" ]; then curl -s -d "$message" \ -H "Title: Mana Container Health" \ @@ -120,12 +119,8 @@ send_notification() { } # --- colima-VM-Liveness-Guard --------------------------------------------- -# Wenn die colima-VM im laufenden Betrieb stirbt (Crash/OOM), bringt sonst -# nichts sie zurueck — startup.sh laeuft nur beim Boot. Dieser Guard heilt -# einen Mid-Run-Crash. Schutz vor ungewollten Starts in bewussten Wartungs-/ -# Debug-Stopps (colima restart/stop gegen die egress-/ssh-mux-Bugs): das -# Lock-File pausiert den Guard. Backoff verhindert Endlos-Haemmern, wenn -# colima nicht hochkommt (z.B. stale in_use_by-Symlink auf der ManaData-Disk). +# Heilt einen Mid-Run-Crash der colima-VM (startup.sh laeuft nur beim Boot). +# Wartungs-Lock pausiert; Backoff verhindert Endlos-Haemmern. COLIMA_FAIL_TRACKER="/tmp/mana-colima-start-fails" COLIMA_MAX_FAILS=3 @@ -152,7 +147,6 @@ if ! colima status >/dev/null 2>&1; then exit 1 fi else - # VM laeuft normal — Fehlerzaehler zuruecksetzen rm -f "$COLIMA_FAIL_TRACKER" 2>/dev/null || true fi # --- Ende colima-Guard ---------------------------------------------------- @@ -163,9 +157,9 @@ if ! docker info >/dev/null 2>&1; then exit 1 fi -[ "$DRY_RUN" = "1" ] && log "DRY_RUN aktiv — es wird nichts verändert, nur geloggt." +[ "$DRY_RUN" = "1" ] && log "DRY_RUN aktiv — es wird nichts veraendert, nur geloggt." -# Track restart attempts to avoid infinite loops +# Restart-Versuche tracken (Loop-Guard). DRY_RUN persistiert NICHT. track_restart() { local container="$1" local count_file="$RESTART_TRACKER/$container" @@ -176,133 +170,58 @@ track_restart() { count=$(cat "$count_file" 2>/dev/null || echo 0) case "$count" in '' | *[!0-9]*) count=0 ;; esac age=$(($(date +%s) - $(stat -f %m "$count_file" 2>/dev/null || stat -c %Y "$count_file" 2>/dev/null))) - [ "$age" -gt 3600 ] && count=0 # Zaehler nach 1h zuruecksetzen + [ "$age" -gt 3600 ] && count=0 # nach 1h zuruecksetzen fi count=$((count + 1)) - # Im DRY_RUN den Zaehler NICHT persistieren (Probelaeufe sollen den - # Loop-Guard nicht künstlich hochtreiben). [ "$DRY_RUN" = "1" ] || echo "$count" >"$count_file" echo "$count" } -# Bringt einen Container über SEIN eigenes Compose-Projekt wieder hoch -# (Labels). recreate=1 entfernt ihn vorher (für Crash-Loops, damit ein -# frischer Container entsteht). Ohne Compose-Labels: docker start/restart. -bring_up() { - local name="$1" - local recreate="${2:-0}" - local proj cfg svc - proj=$(docker inspect "$name" --format '{{index .Config.Labels "com.docker.compose.project"}}' 2>/dev/null || echo "") - cfg=$(docker inspect "$name" --format '{{index .Config.Labels "com.docker.compose.project.config_files"}}' 2>/dev/null || echo "") - svc=$(docker inspect "$name" --format '{{index .Config.Labels "com.docker.compose.service"}}' 2>/dev/null || echo "") - cfg="${cfg%%,*}" # bei mehreren -f das erste nehmen +# --- mana-core Reconciliation: fehlende Core-Container neu erstellen ------- +# Nur mana-core, weil dessen Env via env_file (+ co-located .env) im +# Compose-Dir liegt -> compose-up laedt sie sicher (keine Blank-Secrets). +# Ein komplett fehlender Container hat keine Labels -> gegen die Core- +# Compose abgleichen. +reconcile_mana_core() { + [ -f "$MANA_CORE_COMPOSE" ] || { + log "core-reconcile: Compose nicht gefunden ($MANA_CORE_COMPOSE) — skip" + return 0 + } + [ -f "$MAINT_LOCK" ] && { + log "core-reconcile: Wartungs-Lock aktiv — skip" + return 0 + } - if [ -n "$proj" ] && [ -n "$cfg" ] && [ -n "$svc" ] && [ -f "$cfg" ]; then - if [ "$recreate" = "1" ]; then - log " recreate $name via compose (project=$proj service=$svc)" - run docker rm -f "$name" - run docker compose -p "$proj" -f "$cfg" up -d --no-deps --no-build "$svc" - else - log " recover $name via compose (project=$proj service=$svc)" - run docker compose -p "$proj" -f "$cfg" up -d --no-deps --no-build "$svc" - fi - else - # Kein Compose-Container (oder Config-Datei fehlt) — direkter Start. - log " recover $name via docker start (keine nutzbaren Compose-Labels)" - run docker start "$name" - fi -} + local services svc cid missing="" + services=$(docker compose -p "$MANA_CORE_PROJECT" -f "$MANA_CORE_COMPOSE" config --services 2>/dev/null || true) + [ -z "$services" ] && { + log "core-reconcile: keine Services lesbar — skip" + return 0 + } -# --- Reconciliation: fehlende DAUERLÄUFER-Container neu erstellen ---------- -# Ein komplett fehlender Container hat keine Labels — daher pro Compose- -# Projekt (aus den LAUFENDEN Containern abgeleitet) gegen die Compose -# abgleichen, welche Dauerläufer-Services (restart always/unless-stopped) -# definiert, aber ohne Container sind, und nur die gezielt (re-)erzeugen. -# Deckt so auch eigenständige App-Stacks (zitare/nutriphi/viadocu/…) ab, -# nicht nur mana-core. Schutzmechanismen: -# - Projekte ohne laufenden Container = bewusst unten → nicht anfassen. -# - Mehrdeutige Projektnamen (gleicher Name, verschiedene Composes = -# Projekt-Kollision, z.B. manacore-monorepo/herbatrium) → übersprungen. -# - Nur Dauerläufer (restart-Policy via config-json + jq) → keine -# Job/Init/Profile-Services. -# - Wartungs-Lock pausiert die ganze Reconciliation. -# - mana-core garantiert dabei (auch wenn ausnahmsweise kein Core- -# Container laeuft) via hardcodiertem Pfad. -# ALLE compose-up-Aufrufe nutzen --no-build: ein Watchdog darf NIE ein -# Image bauen (schwergewichtig → genau das kippte am 2026-05-26 die VM in -# den OOM-Crash). Fehlt das Image, schlaegt der Start sauber fehl + Notify. -reconcile_project() { - local proj="$1" cfg_csv="$2" - local fargs=() f IFS_OLD="$IFS" - IFS=',' - for f in $cfg_csv; do - [ -n "$f" ] || continue - if [ ! -f "$f" ]; then - IFS="$IFS_OLD" - log "reconcile: $proj — Compose-Datei fehlt ($f) — skip" - return 0 - fi - fargs+=(-f "$f") - done - IFS="$IFS_OLD" - [ "${#fargs[@]}" -eq 0 ] && return 0 - - # Nur Dauerlaeufer — Job/Init/Profile-Services (restart no/on-failure - # oder profil-gated) bleiben aussen vor. - local longrunners - longrunners=$(docker compose -p "$proj" "${fargs[@]}" config --format json 2>/dev/null \ - | jq -r '.services | to_entries[] | select((.value.restart // "") | test("always|unless-stopped")) | .key' 2>/dev/null || true) - [ -z "$longrunners" ] && return 0 - - local svc cid missing="" - for svc in $longrunners; do + for svc in $services; do cid=$(docker ps -a \ - --filter "label=com.docker.compose.project=$proj" \ + --filter "label=com.docker.compose.project=$MANA_CORE_PROJECT" \ --filter "label=com.docker.compose.service=$svc" \ --format '{{.ID}}' 2>/dev/null | head -1) [ -z "$cid" ] && missing="${missing:+$missing }$svc" done if [ -n "$missing" ]; then - log "reconcile: $proj — FEHLENDE Dauerläufer: $missing" + log "core-reconcile: FEHLENDE Core-Container: $missing" for svc in $missing; do - log " (re-)erstelle $proj/$svc" - run docker compose -p "$proj" "${fargs[@]}" up -d --no-deps --no-build "$svc" + log " (re-)erstelle Core-Service: $svc" + run docker compose -p "$MANA_CORE_PROJECT" -f "$MANA_CORE_COMPOSE" up -d --no-deps --no-build "$svc" done - send_notification "🔧 $proj\n\nFehlende Container neu erstellt: $missing" "high" + send_notification "🔧 mana-core\n\nFehlende Container neu erstellt: $missing" "high" fi } -reconcile_missing() { - if [ -f "$MAINT_LOCK" ]; then - log "reconcile: Wartungs-Lock aktiv ($MAINT_LOCK) — skip" - return 0 - fi +reconcile_mana_core - # (Projekt|config_files)-Paare aus LAUFENDEN Containern + garantiert mana-core. - local pairs projects proj cfgs cfgcount - pairs=$(docker ps --format '{{.Label "com.docker.compose.project"}}|{{.Label "com.docker.compose.project.config_files"}}' 2>/dev/null | grep -v '^|' | grep -v '^$' | sort -u || true) - if [ -f "$MANA_CORE_COMPOSE" ]; then - pairs=$(printf '%s\n%s\n' "$pairs" "${MANA_CORE_PROJECT}|${MANA_CORE_COMPOSE}" | grep -v '^$' | sort -u) - fi - - projects=$(printf '%s\n' "$pairs" | cut -d'|' -f1 | sort -u) - for proj in $projects; do - [ -z "$proj" ] && continue - # Alle distinct config_files-Werte fuer dieses Projekt. - cfgs=$(printf '%s\n' "$pairs" | awk -F'|' -v p="$proj" '$1==p{print $2}' | sort -u) - cfgcount=$(printf '%s\n' "$cfgs" | grep -c .) - if [ "$cfgcount" -ne 1 ]; then - log "reconcile: $proj — mehrdeutig ($cfgcount Compose-Sets) — skip" - continue - fi - reconcile_project "$proj" "$cfgs" - done -} - -reconcile_missing - -# Nicht-laufende Container (created/exited), die laufen sollten. +# Nicht-laufende Container (created/exited), die laufen sollten — +# projektuebergreifend (auch nicht-mana-*). Recovery via `docker start` +# (behaelt Env). STUCK_CONTAINERS="" for c in $(docker ps -a --filter "status=created" --filter "status=exited" --format "{{.Names}}"); do is_oneshot_init "$c" && continue @@ -310,7 +229,7 @@ for c in $(docker ps -a --filter "status=created" --filter "status=exited" --for STUCK_CONTAINERS="${STUCK_CONTAINERS:+$STUCK_CONTAINERS$'\n'}$c" done -# Crash-loopende Container (Restarting) — haben per Definition eine Policy. +# Crash-loopende Container (Restarting) — projektuebergreifend. CRASHLOOP_CONTAINERS=$(docker ps -a --filter "status=restarting" --format "{{.Names}}" || true) if [ -z "$STUCK_CONTAINERS" ] && [ -z "$CRASHLOOP_CONTAINERS" ]; then @@ -318,27 +237,27 @@ if [ -z "$STUCK_CONTAINERS" ] && [ -z "$CRASHLOOP_CONTAINERS" ]; then exit 0 fi -# Crash-Loops zuerst (kritischer) — mit Backoff, dann sauber neu erstellen. +# Crash-Loops zuerst — `docker restart` (KEIN rm/compose-up: Env bleibt), +# Backoff + Notify wenn es persistent crasht (echter Bug, nicht heilbar). if [ -n "$CRASHLOOP_CONTAINERS" ]; then - log "WARNING: Crash-loopende Container gefunden:" + log "WARNING: Crash-loopende Container:" for container in $CRASHLOOP_CONTAINERS; do - RESTART_COUNT=$(docker inspect "$container" --format '{{.RestartCount}}' 2>/dev/null || echo "0") - log " - $container (restart count: $RESTART_COUNT)" + RC=$(docker inspect "$container" --format '{{.RestartCount}}' 2>/dev/null || echo "0") + log " - $container (restart count: $RC)" done - for container in $CRASHLOOP_CONTAINERS; do ATTEMPTS=$(track_restart "$container") if [ "$ATTEMPTS" -gt 3 ]; then - log " SKIP: $container wurde in der letzten Stunde $ATTEMPTS-mal neu gestartet — manueller Eingriff noetig" + log " SKIP: $container in der letzten Stunde $ATTEMPTS-mal angestossen — manueller Eingriff noetig" send_notification "🚨 Container braucht manuellen Fix\n\n$container crasht wiederholt ($ATTEMPTS-mal). Logs:\ndocker logs $container" "high" continue fi - log " Recreate $container (Versuch $ATTEMPTS/3)..." - bring_up "$container" 1 + log " docker restart $container (Versuch $ATTEMPTS/3)..." + run docker restart "$container" done fi -# Stuck (Created/Exited) — einfach hochfahren. +# Stuck (Created/Exited) — `docker start` (Env bleibt). if [ -n "$STUCK_CONTAINERS" ]; then log "WARNING: Container nicht laufend (sollten aber):" for container in $STUCK_CONTAINERS; do @@ -346,18 +265,18 @@ if [ -n "$STUCK_CONTAINERS" ]; then log " - $container (status: $STATUS)" done for container in $STUCK_CONTAINERS; do - bring_up "$container" 0 + log " docker start $container..." + run docker start "$container" done fi ALL_PROBLEM_CONTAINERS=$(printf '%s\n%s\n' "$STUCK_CONTAINERS" "$CRASHLOOP_CONTAINERS" | grep -v "^$" | sort -u || true) if [ "$DRY_RUN" = "1" ]; then - log "DRY_RUN: Geplante Recovery für: $(echo $ALL_PROBLEM_CONTAINERS | tr '\n' ' ')" + log "DRY_RUN: geplante Recovery fuer: $(echo $ALL_PROBLEM_CONTAINERS | tr '\n' ' ')" exit 0 fi -# Warten und verifizieren. sleep 10 STILL_STUCK="" @@ -370,11 +289,11 @@ STILL_CRASHING=$(docker ps -a --filter "status=restarting" --format "{{.Names}}" ALL_STILL_BROKEN=$(printf '%s\n%s\n' "$STILL_STUCK" "$STILL_CRASHING" | grep -v "^$" | sort -u || true) if [ -z "$ALL_STILL_BROKEN" ]; then - FIXED_MSG="Auto-fixed containers: $(echo $ALL_PROBLEM_CONTAINERS | tr '\n' ', ')" + FIXED_MSG="Auto-fixed: $(echo $ALL_PROBLEM_CONTAINERS | tr '\n' ', ')" log "SUCCESS: $FIXED_MSG" send_notification "🔧 Mana Auto-Recovery\n\n$FIXED_MSG" else - log "ERROR: Manche Container sind weiterhin kaputt:" + log "ERROR: Weiterhin kaputt:" for container in $ALL_STILL_BROKEN; do STATUS=$(docker inspect "$container" --format '{{.State.Status}}' 2>/dev/null || echo "unknown") log " - $container (status: $STATUS)"