Aller au contenu principal

Sauvegarde OpenProject

Script : /usr/local/bin/backup_openproject.sh — tous les jours à 03:30, rétention 14 jours.

Ce qui est sauvegardé​

Chaque nuit, un dossier /srv/backups/openproject/AAAA-MM-JJ_HHMM/ contenant :

FichierContenu
openproject.dumpBase PostgreSQL (pg_dump -Fc), vérifiée avec pg_restore --list
opdata.tar.gzPièces jointes (/var/openproject/assets)
docker-compose.yml, env, proxy.tar.gzConfiguration — env contient SECRET_KEY_BASE, indispensable
versions.txtVersions des images au moment de la sauvegarde
SHA256SUMSSommes de contrôle

Installation​

  1. Renseigner OP_DIR (chemin absolu du dossier compose, donné par docker compose ls) en tête de script.

  2. Installer et tester :

    sudo install -m 750 backup_openproject.sh /usr/local/bin/backup_openproject.sh
    sudo /usr/local/bin/backup_openproject.sh
  3. Planifier (sudo crontab -e) :

    30 3 * * * /usr/local/bin/backup_openproject.sh >> /var/log/backup_openproject.log 2>&1
attention

Pas de ~ dans OP_DIR : le script tourne en root et ~ n'est pas développé entre guillemets.

Restauration​

sudo ./restore_openproject.sh /srv/backups/openproject/AAAA-MM-JJ_HHMM

Le script vérifie les sommes de contrôle, demande de taper OUI, arrête OpenProject (sauf la base), restaure la base et les pièces jointes puis redémarre.

Restauration sur un nouveau serveur : réinstaller Docker, recopier docker-compose.yml, env (→ .env) et proxy/, faire docker compose up -d db, puis lancer le script de restauration.

Depuis Google Drive : voir Copie hors site.

Scripts​

backup_openproject.sh
#!/usr/bin/env bash
# Sauvegarde OpenProject (docker compose) : base PostgreSQL + pièces jointes + configuration.
#
# Installation sur le serveur OpenProject :
# sudo install -m 750 backup_openproject.sh /usr/local/bin/backup_openproject.sh
# sudo crontab -e puis ajouter :
# 30 3 * * * /usr/local/bin/backup_openproject.sh >> /var/log/backup_openproject.log 2>&1
#
# Test manuel : sudo /usr/local/bin/backup_openproject.sh
set -euo pipefail

# ---------- À adapter ----------
OP_DIR="/chemin/vers/openproject" # dossier qui contient docker-compose.yml et .env
BACKUP_DIR="/srv/backups/openproject" # où stocker les sauvegardes locales
KEEP_DAYS=14 # nombre de jours de sauvegardes gardées en local
DB_USER="postgres"
DB_NAME="openproject"
RCLONE_REMOTE="" # ex. "gdrive-crypt:backups/openproject" (vide = pas de copie hors site)
# --------------------------------

STAMP="$(date +%F_%H%M)"
DEST="$BACKUP_DIR/$STAMP"
log() { echo "[$(date '+%F %T')] $*"; }
fail() { log "ERREUR : $*"; exit 1; }

# Un seul backup à la fois
exec 9>/var/lock/backup_openproject.lock
flock -n 9 || fail "une sauvegarde est déjà en cours"

cd "$OP_DIR" || fail "dossier $OP_DIR introuvable"
mkdir -p "$DEST"
chmod 700 "$BACKUP_DIR" "$DEST"
log "Début de la sauvegarde -> $DEST"

# 1. Base de données (format custom : compressé, restaurable avec pg_restore)
log "Dump PostgreSQL..."
docker compose exec -T db pg_dump -U "$DB_USER" -Fc "$DB_NAME" > "$DEST/openproject.dump" \
|| fail "pg_dump a échoué"
# Vérifie que le dump est lisible
docker compose exec -T db pg_restore --list < "$DEST/openproject.dump" > /dev/null \
|| fail "le dump est illisible"

# 2. Pièces jointes (volume ou dossier monté sur /var/openproject/assets dans le conteneur web)
log "Archive des pièces jointes..."
WEB_ID="$(docker compose ps -q web)"
[ -n "$WEB_ID" ] || fail "conteneur web introuvable (OpenProject est-il démarré ?)"
ASSETS_SRC="$(docker inspect -f '{{range .Mounts}}{{if eq .Destination "/var/openproject/assets"}}{{if .Name}}{{.Name}}{{else}}{{.Source}}{{end}}{{end}}{{end}}' "$WEB_ID")"
[ -n "$ASSETS_SRC" ] || fail "montage /var/openproject/assets introuvable"
docker run --rm -v "$ASSETS_SRC:/data:ro" -v "$DEST:/backup" alpine \
tar czf /backup/opdata.tar.gz -C /data . \
|| fail "archive des pièces jointes échouée"

# 3. Configuration (.env contient SECRET_KEY_BASE : indispensable pour restaurer)
log "Copie de la configuration..."
cp docker-compose.yml "$DEST/"
[ -f .env ] && cp .env "$DEST/env"
[ -d proxy ] && tar czf "$DEST/proxy.tar.gz" proxy
docker compose images > "$DEST/versions.txt" 2>/dev/null || true

# 4. Sommes de contrôle
( cd "$DEST" && sha256sum ./* > SHA256SUMS )
log "Taille : $(du -sh "$DEST" | cut -f1)"

# 5. Copie hors site (optionnelle)
if [ -n "$RCLONE_REMOTE" ]; then
log "Copie vers $RCLONE_REMOTE/$STAMP ..."
rclone copy "$DEST" "$RCLONE_REMOTE/$STAMP" || fail "copie rclone échouée"
rclone delete "$RCLONE_REMOTE" --min-age "${KEEP_DAYS}d" --rmdirs || log "ATTENTION : nettoyage distant échoué"
fi

# 6. Rotation locale
find "$BACKUP_DIR" -mindepth 1 -maxdepth 1 -type d -mtime +"$KEEP_DAYS" -exec rm -rf {} +
log "Sauvegarde terminée."
restore_openproject.sh
#!/usr/bin/env bash
# Restaure une sauvegarde faite par backup_openproject.sh.
# ATTENTION : remplace la base et les pièces jointes actuelles.
#
# Usage : sudo ./restore_openproject.sh /srv/backups/openproject/2026-11-03_0330
set -euo pipefail

OP_DIR="/chemin/vers/openproject"
DB_USER="postgres"
DB_NAME="openproject"

SRC="${1:?Usage : $0 /chemin/vers/le/dossier/de/sauvegarde}"
[ -f "$SRC/openproject.dump" ] || { echo "openproject.dump introuvable dans $SRC"; exit 1; }
( cd "$SRC" && sha256sum -c --quiet SHA256SUMS ) || { echo "Sommes de contrôle invalides"; exit 1; }

read -r -p "Ceci ÉCRASE la base et les pièces jointes actuelles. Taper OUI pour continuer : " ok
[ "$ok" = "OUI" ] || { echo "Annulé."; exit 1; }

cd "$OP_DIR"
echo "Arrêt d'OpenProject (sauf la base)..."
docker compose stop web worker cron seeder proxy hocuspocus

echo "Restauration de la base..."
docker compose exec -T db dropdb -U "$DB_USER" --if-exists "$DB_NAME"
docker compose exec -T db createdb -U "$DB_USER" "$DB_NAME"
docker compose exec -T db pg_restore -U "$DB_USER" -d "$DB_NAME" --no-owner < "$SRC/openproject.dump"

echo "Restauration des pièces jointes..."
WEB_ID="$(docker compose ps -aq web)"
ASSETS_SRC="$(docker inspect -f '{{range .Mounts}}{{if eq .Destination "/var/openproject/assets"}}{{if .Name}}{{.Name}}{{else}}{{.Source}}{{end}}{{end}}{{end}}' "$WEB_ID")"
docker run --rm -v "$ASSETS_SRC:/data" -v "$SRC:/backup:ro" alpine \
sh -c 'rm -rf /data/* /data/..?* /data/.[!.]* 2>/dev/null; tar xzf /backup/opdata.tar.gz -C /data'

echo "Redémarrage..."
docker compose up -d
echo "Terminé. Vérifie la connexion et une pièce jointe."
echo "Si le .env a été perdu, recopie $SRC/env vers $OP_DIR/.env AVANT de redémarrer."