#!/bin/bash # Backup quotidien sans.pub : dumps des bases + restic vers OVH Swift (conteneur sans-pub-restic) # Installé dans /root/restic/backup.sh, lancé par /etc/cron.d/restic-backup set -uo pipefail LOG=/var/log/restic-backup.log DUMP_DIR=/var/backups/sans-pub-db VOLUMES=/var/lib/docker/volumes MAIL_TO=pierre@sans.pub ERRORS=() exec 9>/run/restic-backup.lock flock -n 9 || { echo "$(date -Is) déjà en cours, abandon" >> "$LOG"; exit 1; } exec >> "$LOG" 2>&1 echo "===== $(date -Is) début" . /root/restic/env notify_failure() { local pass pass=$(sed -n 's/^ *SMTP_PASS: *//p' /root/backup/docker-compose.yaml) printf 'From: system@sans.pub\r\nTo: %s\r\nSubject: [sans.pub] ECHEC backup restic\r\n\r\n%s\r\n\r\nVoir %s sur le serveur.\r\n' \ "$MAIL_TO" "$(printf '%s\n' "${ERRORS[@]}")" "$LOG" | # -k : le certificat de poste.io a expiré (2021-02-06) ; connexion vers ce même serveur curl -sS -k --url smtps://smtp.sans.pub:465 --mail-from system@sans.pub --mail-rcpt "$MAIL_TO" \ --user "system@sans.pub:$pass" --upload-file - || echo "envoi du mail d'alerte impossible" } # 1. Dumps logiques des bases (cohérents, contrairement aux fichiers bruts) # Les fichiers bruts d'une base ne sont exclus du backup que si son dump a réussi. umask 077 DB_VOLUMES_DUMPED=() rm -rf "$DUMP_DIR.new" && mkdir -p "$DUMP_DIR.new" for c in $(docker ps --format '{{.Names}}'); do name=${c#srv-captain--}; name=${name%%.*} envs=$(docker inspect "$c" --format '{{range .Config.Env}}{{println .}}{{end}}' | cut -d= -f1) attempts=() if grep -qx MYSQL_ROOT_PASSWORD <<<"$envs"; then # Variantes essayées dans l'ordre : socket puis TCP (certaines bases n'acceptent root@% qu'en TCP, # ex. limesurvey), puis sans --routines (mysql.proc incompatible, ex. writefreely-db) base='export MYSQL_PWD="$MYSQL_ROOT_PASSWORD"; exec mysqldump -uroot --all-databases --single-transaction --triggers' attempts=("$base --routines" "$base --routines -h127.0.0.1" "$base" "$base -h127.0.0.1") elif grep -qx POSTGRES_USER <<<"$envs"; then attempts=('exec pg_dumpall -U "$POSTGRES_USER"') else continue fi out="$DUMP_DIR.new/$name.sql.gz" ok= for cmd in "${attempts[@]}"; do if docker exec "$c" sh -c "$cmd" 2>"$DUMP_DIR.new/$name.err" | gzip -6 > "$out" \ && [ "$(zcat "$out" 2>/dev/null | head -c 1 | wc -c)" -gt 0 ]; then ok=1; break fi done if [ -n "$ok" ]; then rm -f "$DUMP_DIR.new/$name.err" echo "dump OK $name ($(du -h "$out" | cut -f1))" for v in $(docker inspect "$c" --format '{{range .Mounts}}{{if eq .Type "volume"}}{{.Name}} {{end}}{{end}}'); do DB_VOLUMES_DUMPED+=(--exclude "$VOLUMES/$v") done else ERRORS+=("dump ÉCHEC $name (fichiers bruts sauvegardés à la place) : $(tail -c 300 "$DUMP_DIR.new/$name.err")") fi done rm -rf "$DUMP_DIR" && mv "$DUMP_DIR.new" "$DUMP_DIR" # 2. Sauvegarde restic # Exclus : fichiers bruts des bases dumpées avec succès, registres Docker (reconstructibles), caches, logs EXCLUDES=( "${DB_VOLUMES_DUMPED[@]}" --exclude "$VOLUMES/ffd465a8ae19be68cdfcd27ee8d5acfddf8d9269ad8e95dbbaf4562dcaacbeef" --exclude "$VOLUMES/edc80279a0f6294d1fb8efb4955b2a23bd861f394a5532120c29d6149f36490e" --exclude "/captain/data/registry" --exclude "$VOLUMES/*/_data/data/appdata_*/preview" --exclude "$VOLUMES/captain--bitwarden-data/_data/icon_cache" --exclude "$VOLUMES/*/_data/data/nextcloud.log*" --exclude "$VOLUMES/*/_data/data/audit.log*" ) nice -n 10 ionice -c2 -n7 restic backup --tag quotidien --exclude-caches "${EXCLUDES[@]}" \ "$DUMP_DIR" "$VOLUMES" /captain/data /home/cloud/apps_data /root/backup /etc \ || ERRORS+=("restic backup a échoué (code $?)") # 3. Rétention ; purge et vérification le dimanche restic forget --tag quotidien --keep-daily 7 --keep-weekly 4 --keep-monthly 6 \ || ERRORS+=("restic forget a échoué") if [ "$(date +%u)" = 7 ]; then restic prune || ERRORS+=("restic prune a échoué") restic check --read-data-subset=5% || ERRORS+=("restic check a échoué") fi if [ ${#ERRORS[@]} -gt 0 ]; then printf 'ERREUR: %s\n' "${ERRORS[@]}" notify_failure echo "===== $(date -Is) fin AVEC ERREURS" exit 1 fi echo "===== $(date -Is) fin OK"