#!/usr/bin/env bash # # docker-backup.sh — bezpečná záloha Docker projektů a named volumes # # - vytvoří timestamp složku v / # - pro každý projekt v zastaví kontejnery, # zálohuje tar.zst, ověří integritu, znovu spustí # - zvlášť zálohuje Docker named volumes # - ponechá posledních N záloh (default 3) # # Použití: # ./docker-backup.sh # všechny projekty + named volumes # ./docker-backup.sh nginx mattermost # vybrané projekty (volumes vždy) # BACKUP_KEEP=7 ./docker-backup.sh # ponechá 7 záloh # ./docker-backup.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:-}" BACKUP_ROOT="${BACKUP_ROOT:-}" KEEP="${BACKUP_KEEP:-3}" TS="$(date +%Y-%m-%d_%H-%M-%S)" DEST="${BACKUP_ROOT}/${TS}" # Barevné výstupy 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" "$*"; } warn() { printf '%s[%s]%s %s\n' "$YELLOW" "$(date '+%H:%M:%S')" "$NC" "$*"; } err() { printf '%s[%s]%s %s\n' "$RED" "$(date '+%H:%M:%S')" "$NC" "$*"; } # Kontrola if ! docker info >/dev/null 2>&1; then err "Docker není dostupný." exit 1 fi if [ ! -d "$DOCKERY" ]; then err "Dockery adresář neexistuje: $DOCKERY" exit 1 fi if [ ! -d "$BACKUP_ROOT" ]; then err "Backup adresář neexistuje: $BACKUP_ROOT" exit 1 fi mkdir -p "$DEST" # 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 log "Projektů k záloze: ${#PROJECTS[@]}" log "Cíl: $DEST" log "Uchovat posledních: $KEEP záloh" # === FÁZE 1: zastavit všechny dotčené kontejnery najednou === log "" log "=== STOP: zastavuji všechny kontejnery v ${PROJECTS[*]} ===" for project in "${PROJECTS[@]}"; do cd "$DOCKERY/$project" if [ -f docker-compose.yml ] || [ -f docker-compose.yaml ]; then if docker compose ps -q 2>/dev/null | grep -q .; then log " [$project] docker compose stop" docker compose stop 2>&1 || true fi fi done # === FÁZE 2: záloha projektů (paralelně pro velké projekty) === log "" log "=== BACKUP projekty (tar.zst) ===" FAILED=() for project in "${PROJECTS[@]}"; do cd "$DOCKERY/$project" ARCHIVE="$DEST/${project}.tar.zst" log " [$project] tar -c /$project/ → $ARCHIVE" # absolutní cesta (background tar pitfall: vždy absolutní) if /usr/bin/tar --exclude='._*' --exclude='.DS_Store' \ --use-compress-program='/opt/homebrew/bin/zstd -1 -T0' \ -cf "$ARCHIVE" \ "/$project/" 2>>"$DEST/.errors"; then # Ověření if /opt/homebrew/bin/zstd -t "$ARCHIVE" >/dev/null 2>&1; then SIZE=$(stat -f '%z' "$ARCHIVE") log " OK ($(numfmt --to=iec --suffix=B "$SIZE" 2>/dev/null || echo "$SIZE B"))" else err " zstd -t selhal pro $ARCHIVE" FAILED+=("$project") fi else err " tar selhal pro $project" FAILED+=("$project") fi done # === FÁZE 3: named volumes (per-volume záloha přes Docker) === log "" log "=== BACKUP named volumes ===" VOLUMES=$(docker volume ls --format '{{.Name}}' \ | grep -vE '^(host-services|docker-desktop)$' || true) for vol in $VOLUMES; do ARCHIVE="$DEST/${vol}.tar.zst" log " [volume:$vol] → $ARCHIVE" # Nejdřív dekomprimuj na hostiteli, pak pošli do kontejneru (alpine tar neumí zstd) if docker run --rm \ -v "${vol}:/src:ro" \ -v "$DEST:/dest" \ alpine:3.20 \ sh -c "cd /src && tar --exclude='._*' --exclude='.DS_Store' -cf - ." \ > "$DEST/${vol}.tar" 2>>"$DEST/.errors"; then /opt/homebrew/bin/zstd -1 -T0 -f "$DEST/${vol}.tar" -o "$ARCHIVE" 2>>"$DEST/.errors" rm -f "$DEST/${vol}.tar" if /opt/homebrew/bin/zstd -t "$ARCHIVE" >/dev/null 2>&1; then SIZE=$(stat -f '%z' "$ARCHIVE") log " OK ($(numfmt --to=iec --suffix=B "$SIZE" 2>/dev/null || echo "$SIZE B"))" else err " zstd -t selhal pro $ARCHIVE" FAILED+=("volume:$vol") fi else err " docker run selhal pro volume $vol" FAILED+=("volume:$vol") fi done # === FÁZE 4: znovu spustit projekty === log "" log "=== UP: spouštím projekty ===" for project in "${PROJECTS[@]}"; do cd "$DOCKERY/$project" if [ -f docker-compose.yml ] || [ -f docker-compose.yaml ]; then log " [$project] docker compose up -d" docker compose up -d 2>&1 || warn " [$project] up -d selhal (zkontroluj)" fi done # === FÁZE 5: hash report === log "" log "=== SHA-256 hashe ===" for f in "$DEST"/*.tar.zst; do [ -f "$f" ] || continue H=$(shasum -a 256 "$f" | awk '{print $1}') S=$(stat -f '%z' "$f") log " $(basename "$f") sha256=$H size=$S" done # === FÁZE 6: retention === log "" log "=== RETENTION: ponechávám posledních $KEEP záloh ===" # set -u a prázdné pole existing=() while IFS= read -r d; do [ -d "$d" ] && existing+=("$d") done < <(find "$BACKUP_ROOT" -maxdepth 1 -mindepth 1 -type d | sort) # Pokud něco selhalo, nepromazávej if [ "${#FAILED[@]}" -gt 0 ]; then warn "Některé zálohy selhaly (${#FAILED[@]}), RETENTION se neprovádí" else if [ "${#existing[@]}" -gt "$KEEP" ]; then for old in "${existing[@]:0:${#existing[@]}-KEEP}"; do log " mažu starou zálohu: $(basename "$old")" rm -rf "$old" done fi fi log "" log "======================================" log " HOTOVO" log "======================================" log "Záloha: $DEST" if [ "${#FAILED[@]}" -gt 0 ]; then err "Selhalo: ${#FAILED[@]}" for f in "${FAILED[@]}"; do err " ✗ $f"; done exit 1 fi log "Vše OK."