Files
Portainer/scripts/docker-selfheal.sh
T
2026-07-25 11:21:08 +00:00

88 lines
3.3 KiB
Bash
Executable File

#!/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