#!/bin/bash # Recorre contenedores con restart-policy always/unless-stopped que estén # caídos (Exited) o atascados (Restarting) y los recrea vía su propio # proyecto docker compose, para curar referencias de red obsoletas tras # un reinicio del daemon Docker (ej. update automático de docker-ce). # # Idempotente: si nada está roto, no hace nada. Pensado para correr # periódicamente vía systemd timer (docker-selfheal.timer). set -uo pipefail LOG_FILE="/var/log/docker-selfheal.log" TELEGRAM_ENV="/home/felidae/f2b-telegram-bot/.env" SABLIER_CONFIG="/opt/traefik/dynamic/sablier.yml" # Contenedores que Sablier apaga a propósito (scale-to-zero) quedan Exited # con restart-policy unless-stopped igual que un crash real -- no hay forma # de distinguirlos por estado/policy. Se excluyen por nombre, leyendo los # `names:` de cada middleware sablier-* en el dynamic config de Traefik. declare -A SABLIER_MANAGED=() if [ -r "$SABLIER_CONFIG" ]; then while IFS= read -r line; do line="${line#*names:}" line="${line//\"/}" IFS=',' read -ra names <<< "$line" for n in "${names[@]}"; do n="$(echo "$n" | xargs)" [ -n "$n" ] && SABLIER_MANAGED["$n"]=1 done done < <(grep -E '^\s*names:' "$SABLIER_CONFIG") fi log() { echo "$(date -u +'%Y-%m-%dT%H:%M:%SZ') $*" | tee -a "$LOG_FILE" } notify() { local msg="$1" [ -f "$TELEGRAM_ENV" ] || return 0 local token chat_id token=$(grep -E '^BOT_TOKEN=' "$TELEGRAM_ENV" | cut -d= -f2- | tr -d '"') chat_id=$(grep -E '^PRIMARY_CHAT_ID=' "$TELEGRAM_ENV" | cut -d= -f2- | tr -d '"') [ -n "$token" ] && [ -n "$chat_id" ] || return 0 curl -s -m 10 -X POST "https://api.telegram.org/bot${token}/sendMessage" \ -d "chat_id=${chat_id}" \ -d "text=${msg}" >/dev/null 2>&1 } fixed_any=0 # Contenedores parados o atascados que deberían estar corriendo mapfile -t broken < <(docker ps -a --filter "status=exited" --filter "status=restarting" --filter "status=created" --format '{{.ID}}') for cid in "${broken[@]}"; do policy=$(docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' "$cid" 2>/dev/null) name=$(docker inspect -f '{{.Name}}' "$cid" 2>/dev/null | sed 's#^/##') case "$policy" in always|unless-stopped) ;; *) continue ;; esac if [ -n "${SABLIER_MANAGED[$name]:-}" ]; then continue fi workdir=$(docker inspect -f '{{ index .Config.Labels "com.docker.compose.project.working_dir" }}' "$cid" 2>/dev/null) project=$(docker inspect -f '{{ index .Config.Labels "com.docker.compose.project" }}' "$cid" 2>/dev/null) service=$(docker inspect -f '{{ index .Config.Labels "com.docker.compose.service" }}' "$cid" 2>/dev/null) if [ -n "$workdir" ] && [ -d "$workdir" ]; then log "Recreando '$name' (proyecto '$project', servicio '$service') vía compose en $workdir" out=$(cd "$workdir" && docker compose up -d --force-recreate "$service" 2>&1) log "$out" fixed_any=1 notify "🔧 docker-selfheal: '$name' estaba caído (policy=$policy), recreado vía compose en ${project}." else log "Recreando '$name' vía docker start (sin metadata de compose)" docker start "$cid" >/dev/null 2>&1 fixed_any=1 notify "🔧 docker-selfheal: '$name' estaba caído (policy=$policy), levantado con docker start." fi done if [ "$fixed_any" -eq 0 ]; then log "Nada que arreglar." fi