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 DockerGPG_RECIPIENT- email-ul cheii GPG cu care se criptează arhiva (generați una cugpg --full-generate-key)RCLONE_REMOTE- remote-ul rclone configurat curclone config(suportă MEGA, Proton Drive, etc.)KEEP_BACKUPS- câte backup-uri să se păstreze în cloudNOTIFY_EMAIL- adresa de email pentru notificări (opțional)- Numele containerelor Docker - domain-locker-db, paperless etc. - verificați cu
docker pscum 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
thinkservercu 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.ascCod: 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
