Script de backup pentru server

Shell scripting, programe și web development.
Avatar utilizator
ThinkRoot
Junior
Junior
Mesaje: 71
Membru din: 09 Feb 2026, 21:01
Contact:

Script de backup pentru server

Mesaj de ThinkRoot »

Am avut nevoie de un script de backup pentru homelabul meu și l-am făcut împreună cu Claude AI.

Scriptul face backup la toate serviciile Docker de pe server (volume și baze de date), criptează arhiva cu GPG și o uploadează în cloud via rclone.

Important: Scriptul este configurat după structura propriului meu server, deci căile către volumele Docker trebuie adaptate la configurația voastră. Verificați cum sunt numite volumele cu:

Cod: Selectaţi tot

docker volume ls
ls /var/lib/docker/volumes/

Ce trebuie modificat înainte de folosire:

  • SERVICES_DIR - calea către directorul cu serviciile Docker
  • GPG_RECIPIENT - email-ul cheii GPG cu care se criptează arhiva (generați una cu gpg --full-generate-key)
  • RCLONE_REMOTE - remote-ul rclone configurat cu rclone config (suportă MEGA, Proton Drive, etc.)
  • KEEP_BACKUPS - câte backup-uri să se păstreze în cloud
  • NOTIFY_EMAIL - adresa de email pentru notificări (opțional)
  • Numele containerelor Docker - domain-locker-db, paperless etc. - verificați cu docker ps cum sunt numite la voi
  • Numele bazei de date PostgreSQL - domain_locker - poate fi diferit la voi
  • Serviciile din secțiunea SQLite - dacă nu aveți toate serviciile (FreshRSS, Wallabag, Linkding etc.) ștergeți liniile corespunzătoare sau adăugați altele noi
  • Înlocuiți toate aparițiile thinkserver cu numele propriului server (search & replace în editorul preferat)

Notă GPG: După generarea cheii, exportați-o și păstrați-o într-un loc sigur - fără ea arhivele nu pot fi decriptate la restore:

Cod: Selectaţi tot

gpg --export-secret-keys --armor "email@exemplu.com" > cheia-gpg-backup.asc

Cod: Selectaţi tot

#!/bin/bash
# =============================================================================
# backup-thinkserver.sh
# Backup complet pentru serviciile Docker de pe thinkserver
# Criptare GPG + upload rclone către cloud
# =============================================================================

set -euo pipefail

# -----------------------------------------------------------------------------
# CONFIGURARE — editează după nevoie
# -----------------------------------------------------------------------------

SERVICES_DIR="/opt/services"
BACKUP_ROOT="/opt/backups"
TIMESTAMP=$(date +%Y-%m-%d_%H%M)
BACKUP_WORK_DIR="${BACKUP_ROOT}/work_${TIMESTAMP}"
ARCHIVE_NAME="thinkserver_${TIMESTAMP}.tar.gz"
ARCHIVE_PATH="${BACKUP_ROOT}/${ARCHIVE_NAME}"
ENCRYPTED_PATH="${ARCHIVE_PATH}.gpg"

# GPG — pune aici email-ul cheii tale (sau fingerprint-ul)
GPG_RECIPIENT="email@example.com"

# rclone remote + cale destinație
# Configurat pe rootlinux și copiat pe thinkserver — vezi instrucțiunile de setup
RCLONE_REMOTE="mega:backups/server"

# Câte arhive să păstrezi în cloud (implicit: 4 = ~o lună dacă rulezi săptămânal)
KEEP_BACKUPS=4

# Notificări email (opțional — lasă gol "" ca să dezactivezi)
NOTIFY_EMAIL=""

# Fișier de log
LOG_FILE="/var/log/backup-thinkserver.log"

# -----------------------------------------------------------------------------
# FUNCȚII UTILITARE
# -----------------------------------------------------------------------------

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}

error_exit() {
    log "EROARE: $*"
    # Cleanup la eroare
    rm -rf "$BACKUP_WORK_DIR"
    rm -f "$ARCHIVE_PATH" "$ENCRYPTED_PATH"
    if [[ -n "$NOTIFY_EMAIL" ]]; then
        echo "Backup thinkserver EȘUAT: $*" | mail -s "[backup] EROARE thinkserver" "$NOTIFY_EMAIL"
    fi
    exit 1
}

notify_success() {
    local size
    size=$(du -sh "$ENCRYPTED_PATH" 2>/dev/null | cut -f1 || echo "N/A")
    log "Backup finalizat cu succes. Dimensiune arhivă: ${size}"
    if [[ -n "$NOTIFY_EMAIL" ]]; then
        echo "Backup thinkserver OK. Dimensiune: ${size}" \
            | mail -s "[backup] OK thinkserver ${TIMESTAMP}" "$NOTIFY_EMAIL"
    fi
}

require_cmd() {
    command -v "$1" &>/dev/null || error_exit "Comandă lipsă: $1 — instalează cu: apt install $1"
}

# -----------------------------------------------------------------------------
# VERIFICĂRI PRELIMINARE
# -----------------------------------------------------------------------------

log "====== Început backup thinkserver (${TIMESTAMP}) ======"

require_cmd docker
require_cmd tar
require_cmd gpg
require_cmd rclone

mkdir -p "$BACKUP_WORK_DIR" "$BACKUP_ROOT"

# Verifică că cheia GPG există
gpg --list-keys "$GPG_RECIPIENT" &>/dev/null \
    || error_exit "Cheia GPG pentru '${GPG_RECIPIENT}' nu a fost găsită. Rulează: gpg --import cheia-ta.asc"

# -----------------------------------------------------------------------------
# 1. CONFIGURAȚII DOCKER COMPOSE
# -----------------------------------------------------------------------------

log "Salvare configurații Docker Compose..."
mkdir -p "${BACKUP_WORK_DIR}/compose-configs"

# Copiază toate docker-compose.yml și .env din /opt/services/*/
for svc_dir in "${SERVICES_DIR}"/*/; do
    svc_name=$(basename "$svc_dir")
    dst="${BACKUP_WORK_DIR}/compose-configs/${svc_name}"
    mkdir -p "$dst"
    [[ -f "${svc_dir}/docker-compose.yml" ]] && cp "${svc_dir}/docker-compose.yml" "$dst/"
    [[ -f "${svc_dir}/.env" ]]              && cp "${svc_dir}/.env" "$dst/"
    [[ -f "${svc_dir}/docker-compose.yaml" ]] && cp "${svc_dir}/docker-compose.yaml" "$dst/"
done

log "Configurații salvate."

# -----------------------------------------------------------------------------
# 2. SERVICII CU SQLITE (copiere directă)
# -----------------------------------------------------------------------------

backup_volume() {
    local name="$1"
    local src="$2"

if [[ -d "$src" ]]; then
    log "  → ${name} (volum)"
    mkdir -p "${BACKUP_WORK_DIR}/${name}"
    cp -a "${src}/." "${BACKUP_WORK_DIR}/${name}/"
else
    log "  ! ${name}: directorul '${src}' nu există — sărit"
fi
}

DOCKER_VOLUMES="/var/lib/docker/volumes"

log "Backup servicii (Docker named volumes)..."

backup_volume "freshrss"    "${DOCKER_VOLUMES}/freshrss_freshrss_data/_data"
backup_volume "wallabag"    "${DOCKER_VOLUMES}/wallabag_wallabag_data/_data"
backup_volume "linkding"    "${DOCKER_VOLUMES}/linkding_linkding_data/_data"
backup_volume "booktracker" "${SERVICES_DIR}/booktracker/data"
backup_volume "scrutiny"    "${DOCKER_VOLUMES}/scrutiny_scrutiny_config/_data"

# Vaultwarden — mai sensibil, salvăm tot ce contează
log "  → vaultwarden (date sensibile)"
VW_DATA="${DOCKER_VOLUMES}/vaultwarden_vaultwarden_data/_data"
if [[ -d "$VW_DATA" ]]; then
    mkdir -p "${BACKUP_WORK_DIR}/vaultwarden"
    cp -a "${VW_DATA}/." "${BACKUP_WORK_DIR}/vaultwarden/"
    log "  → vaultwarden OK"
else
    log "  ! vaultwarden: volumul nu există — verifică calea"
fi

# -----------------------------------------------------------------------------
# 3. PAPERLESS-NGX (documente + SQLite/PostgreSQL intern)
# -----------------------------------------------------------------------------

log "Backup Paperless-ngx..."
PAPERLESS_DIR="${SERVICES_DIR}/paperless"
mkdir -p "${BACKUP_WORK_DIR}/paperless"

# Date interne Paperless (SQLite + export media)
for subdir in data media export; do
    [[ -d "${PAPERLESS_DIR}/${subdir}" ]] && \
        cp -a "${PAPERLESS_DIR}/${subdir}" "${BACKUP_WORK_DIR}/paperless/"
done

# Export structurat din container (opțional dar recomandat)
if docker ps --format '{{.Names}}' | grep -q "paperless"; then
    log "  → Export document_exporter din container Paperless..."
    PAPERLESS_CONTAINER=$(docker ps --format '{{.Names}}' | grep paperless | grep -v redis | grep -v db | head -1)
    docker exec "$PAPERLESS_CONTAINER" \
        python3 manage.py document_exporter /usr/src/paperless/export \
        >> "$LOG_FILE" 2>&1 \
        && log "  → Export Paperless OK" \
        || log "  ! Export Paperless eșuat — continuăm cu copia directă"
fi

# -----------------------------------------------------------------------------
# 4. POSTGRESQL — Domain Locker
# -----------------------------------------------------------------------------

log "Backup PostgreSQL (Domain Locker)..."
mkdir -p "${BACKUP_WORK_DIR}/domain-locker"

# Detectează containerul postgres al domain-locker
DL_DB_CONTAINER=$(docker ps --format '{{.Names}}' | grep -E "domain.locker.*(db|postgres)|postgres.*domain" | head -1 || true)

if [[ -n "$DL_DB_CONTAINER" ]]; then
    log "  → Container DB găsit: ${DL_DB_CONTAINER}"

# FIX: extrage DB name separat și validează — subshell inline în pg_dump
# poate returna gol sau mai multe valori dacă psql eșuează
DL_DBNAME=$(docker exec "$DL_DB_CONTAINER" \
    psql -U postgres -t -A -c \
    "SELECT datname FROM pg_database WHERE datistemplate = false AND datname != 'postgres' LIMIT 1;" \
    2>/dev/null | tr -d '[:space:]' || true)

if [[ -z "$DL_DBNAME" ]]; then
    log "  ! Nu s-a putut detecta numele DB Domain Locker — sărit"
else
    log "  → pg_dump baza: ${DL_DBNAME}"
    docker exec "$DL_DB_CONTAINER" \
        pg_dump -U postgres "$DL_DBNAME" \
        > "${BACKUP_WORK_DIR}/domain-locker/domainlocker_${TIMESTAMP}.sql" \
        || error_exit "pg_dump a eșuat pentru Domain Locker (DB: ${DL_DBNAME})"
    log "  → pg_dump OK"
fi
else
    log "  ! Containerul PostgreSQL pentru Domain Locker nu rulează — sărit"
fi

# -----------------------------------------------------------------------------
# 5. CADDY (configurație reverse proxy)
# -----------------------------------------------------------------------------

log "Backup configurație Caddy..."
mkdir -p "${BACKUP_WORK_DIR}/caddy"

for caddy_path in \
    "${SERVICES_DIR}/caddy" \
    "/etc/caddy" \
    "/root/.config/caddy"; do
    if [[ -d "$caddy_path" ]]; then
        cp -a "${caddy_path}/." "${BACKUP_WORK_DIR}/caddy/"
        log "  → Copiat din ${caddy_path}"
        break
    fi
done

# -----------------------------------------------------------------------------
# 6. MANIFEST BACKUP
# -----------------------------------------------------------------------------

log "Generare manifest..."
{
    echo "# Backup thinkserver"
    echo "Data: ${TIMESTAMP}"
    echo "Host: $(hostname)"
    echo ""
    echo "## Servicii incluse:"
    find "$BACKUP_WORK_DIR" -maxdepth 1 -mindepth 1 -type d | sort | while read -r d; do
        echo "  - $(basename "$d") ($(du -sh "$d" | cut -f1))"
    done
    echo ""
    echo "## Servicii Docker active:"
    docker ps --format "  - {{.Names}} ({{.Image}})" 2>/dev/null || echo "  (eroare la listare)"
} > "${BACKUP_WORK_DIR}/MANIFEST.txt"

cat "${BACKUP_WORK_DIR}/MANIFEST.txt" >> "$LOG_FILE"

# -----------------------------------------------------------------------------
# 7. ARHIVARE TAR.GZ
# -----------------------------------------------------------------------------

log "Arhivare (tar.gz)..."
tar -czf "$ARCHIVE_PATH" \
    -C "$BACKUP_WORK_DIR" . \
    || error_exit "Arhivarea tar a eșuat"

ARCHIVE_SIZE=$(du -sh "$ARCHIVE_PATH" | cut -f1)
log "Arhivă creată: ${ARCHIVE_NAME} (${ARCHIVE_SIZE})"

# Cleanup director temporar
rm -rf "$BACKUP_WORK_DIR"

# -----------------------------------------------------------------------------
# 8. CRIPTARE GPG
# -----------------------------------------------------------------------------

log "Criptare GPG pentru '${GPG_RECIPIENT}'..."
gpg --batch --yes \
    --encrypt \
    --recipient "$GPG_RECIPIENT" \
    --output "$ENCRYPTED_PATH" \
    "$ARCHIVE_PATH" \
    || error_exit "Criptarea GPG a eșuat"

# Șterge arhiva necriptată
rm -f "$ARCHIVE_PATH"

ENCRYPTED_SIZE=$(du -sh "$ENCRYPTED_PATH" | cut -f1)
log "Arhivă criptată: ${ARCHIVE_NAME}.gpg (${ENCRYPTED_SIZE})"

# -----------------------------------------------------------------------------
# 9. UPLOAD RCLONE
# -----------------------------------------------------------------------------

log "Upload rclone către ${RCLONE_REMOTE}..."
rclone copy "$ENCRYPTED_PATH" "$RCLONE_REMOTE" \
    --progress \
    --log-file "$LOG_FILE" \
    --log-level INFO \
    || error_exit "Upload rclone a eșuat"

log "Upload finalizat."

# Șterge arhiva locală după upload reușit
rm -f "$ENCRYPTED_PATH"

# -----------------------------------------------------------------------------
# 10. ROTAȚIE BACKUP-URI VECHI ÎN CLOUD
# -----------------------------------------------------------------------------

log "Rotație backup-uri în cloud (păstrăm ultimele ${KEEP_BACKUPS})..."
# Listează fișierele sortate, șterge cele mai vechi dacă depășesc limita
# || true necesar: grep returnează exit 1 dacă nu găsește nimic (e.g. prima rulare)
REMOTE_FILES=$(rclone lsf "$RCLONE_REMOTE" --files-only 2>/dev/null | grep "^thinkserver_" | sort || true)
REMOTE_COUNT=$(echo "$REMOTE_FILES" | grep -c "." || true)

if [[ "$REMOTE_COUNT" -gt "$KEEP_BACKUPS" ]]; then
    DELETE_COUNT=$(( REMOTE_COUNT - KEEP_BACKUPS ))
    TO_DELETE=$(echo "$REMOTE_FILES" | head -n "$DELETE_COUNT")
    log "  → Șterg ${DELETE_COUNT} backup-uri vechi:"
    while IFS= read -r old_file; do
        log "    - ${old_file}"
        rclone delete "${RCLONE_REMOTE}/${old_file}" >> "$LOG_FILE" 2>&1
    done <<< "$TO_DELETE"
else
    log "  → Număr backup-uri în limite (${REMOTE_COUNT}/${KEEP_BACKUPS}), nimic de șters"
fi

# -----------------------------------------------------------------------------
# FINALIZARE
# -----------------------------------------------------------------------------

notify_success
log "====== Backup finalizat cu succes (${TIMESTAMP}) ======"
exit 0

Scriptul poate fi descărcat și de pe MEGA

Scrie răspuns