docs: backup restic remis en place, cron Nextcloud réparé, plan de migration

- Backup : l'ancien Duplicity (2020) ne tournait plus ; remplacé par restic
  vers OVH Swift (script versionné dans backup/restic-backup.sh, cron 03:30),
  premier snapshot complet et restauration testés le 2026-10-03
- Nextcloud : cron en panne depuis le 2026-08-09 (DNS overlay perdu) corrigé ;
  rétention corbeille/versions à "auto, 30"
- Incident Gitea : vérification de non-réinfection, mouvement latéral et
  intégrité de l'hôte (rien trouvé)
- plan-migration.md : état des lieux, décisions à prendre, phases

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Pierre Martin
2026-10-03 13:32:31 +02:00
co-authored by Claude Opus 5.5
parent 7e5f15577a
commit f974203973
4 changed files with 349 additions and 2 deletions
+98
View File
@@ -0,0 +1,98 @@
#!/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"