#!/usr/bin/env bash # # docker-update.sh -- bezpečná aktualizace Docker Compose projektů # # - zastaví všechny kontejnery, # - stáhne nové image (docker compose pull), # - ověří, že se image skutečně změnil (jinak přeskočí), # - spustí kontejnery z nových image, # - zkontroluje health checks, # - ponechá staré image pro případný rollback (docker image prune -f na konci, # ale jen dangling, ne všechno). # # Použití: # ./docker-update.sh # aktualizuje všechny projekty v # ./docker-update.sh nginx mattermost # aktualizuje vybrané projekty # ./docker-update.sh --help # nápověda # set -euo pipefail usage() { sed -n '2,15p' "$0" | sed 's/^# \{0,1\}//' } for arg in "$@"; do case "$arg" in --help|-h) usage; exit 0 ;; esac done DOCKERY="${DOCKERY:-}" LOG="${LOG:-/tmp/docker-update-$(date +%Y-%m-%d_%H-%M).log}" PRUNE_DANGLING="${PRUNE_DANGLING:-1}" # 1 = docker image prune -f (dangling only) HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-60}" # sekund na health check po startu # Barevné výstupy (pokud terminál umí) if [ -t 1 ]; then RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' else RED=''; GREEN=''; YELLOW=''; NC='' fi log() { printf '%s[%s]%s %s\n' "$GREEN" "$(date '+%H:%M:%S')" "$NC" "$*" | tee -a "$LOG"; } warn() { printf '%s[%s]%s %s\n' "$YELLOW" "$(date '+%H:%M:%S')" "$NC" "$*" | tee -a "$LOG"; } err() { printf '%s[%s]%s %s\n' "$RED" "$(date '+%H:%M:%S')" "$NC" "$*" | tee -a "$LOG"; } # Kontrola, že docker běží if ! docker info >/dev/null 2>&1; then err "Docker není dostupný. Spusť Docker Desktop a zkus znovu." exit 1 fi # Najdi projekty if [ "$#" -gt 0 ]; then PROJECTS=("$@") else PROJECTS=() for d in "$DOCKERY"/*/; do [ -d "$d" ] || continue if compgen -G "$d/docker-compose.y*ml" > /dev/null; then PROJECTS+=("$(basename "$d")") fi done fi if [ "${#PROJECTS[@]}" -eq 0 ]; then err "Nebyly nalezeny žádné Docker Compose projekty v $DOCKERY" exit 1 fi log "Nalezeno ${#PROJECTS[@]} projektů: ${PROJECTS[*]}" # Pre-flight: ulož si, co běží log "=== PRE-FLIGHT ===" BEFORE_RUNNING=$(docker ps --format '{{.Names}}') log "Běžící kontejnery před aktualizací:" printf ' %s\n' $BEFORE_RUNNING UPDATED=() SKIPPED=() FAILED=() for project in "${PROJECTS[@]}"; do cd "$DOCKERY/$project" if ! compgen -G "docker-compose.y*ml" > /dev/null; then warn "[$project] nemá docker-compose soubor, přeskakuji" SKIPPED+=("$project") continue fi log "" log "=== [$project] ===" # Zaznamenej aktuální image ID BEFORE_DIGESTS=$(docker compose ps -q 2>/dev/null \ | xargs -I{} docker inspect --format='{{.Image}}' {} 2>/dev/null \ | sort -u) # Pull log "[$project] docker compose pull" if ! docker compose pull 2>&1 | tee -a "$LOG"; then err "[$project] pull selhal" FAILED+=("$project") continue fi # Ověř, že se image změnil AFTER_DIGESTS=$(docker compose config --images 2>/dev/null \ | xargs -I{} docker image inspect --format='{{index .RepoDigests 0}}' "{}" 2>/dev/null \ | sort -u) CHANGED=0 for d in $AFTER_DIGESTS; do if ! grep -qx "$d" <<<"$BEFORE_DIGESTS"; then CHANGED=1 break fi done if [ "$CHANGED" -eq 0 ]; then log "[$project] žádná změna, přeskakuji up -d" SKIPPED+=("$project") continue fi # Up -d (vždy background kvůli shell guardu) log "[$project] docker compose up -d" if ! docker compose up -d 2>&1 | tee -a "$LOG"; then err "[$project] up -d selhal" FAILED+=("$project") continue fi # Počkej na health check log "[$project] čekám na health check (max ${HEALTH_TIMEOUT}s)" elapsed=0 healthy=0 while [ "$elapsed" -lt "$HEALTH_TIMEOUT" ]; do sleep 5 elapsed=$((elapsed + 5)) unhealthy=$(docker compose ps 2>/dev/null \ | tail -n +2 \ | awk '{print $NF}' \ | grep -vE 'healthy|Up' || true) if [ -z "$unhealthy" ]; then healthy=1 break fi done if [ "$healthy" -eq 1 ]; then log "[$project] OK" UPDATED+=("$project") else warn "[$project] běží, ale ne všechny health checks prošly (zkontroluj ručně)" UPDATED+=("$project:health_unknown") fi done # Prune dangling images (bezpečné) if [ "$PRUNE_DANGLING" = "1" ]; then log "" log "=== DOCKER IMAGE PRUNE -f (dangling only) ===" docker image prune -f 2>&1 | tee -a "$LOG" || true fi # Report log "" log "======================================" log " SHRNUTÍ" log "======================================" log "Aktualizováno: ${#UPDATED[@]} projekt(ů)" for p in "${UPDATED[@]:-}"; do [ -n "$p" ] && log " [OK] $p"; done log "Přeskočeno (beze změny): ${#SKIPPED[@]}" for p in "${SKIPPED[@]:-}"; do [ -n "$p" ] && log " - $p"; done if [ "${#FAILED[@]}" -gt 0 ]; then log "Selhalo: ${#FAILED[@]}" for p in "${FAILED[@]}"; do err " [FAIL] $p"; done fi log "" log "Log: $LOG"