Ce guide couvre les procédures complètes de sauvegarde et de restauration de Mayan EDMS, que l'installation soit en Docker ou en bare-metal (virtualenv). Une section dédiée traite d'Elasticsearch et du stockage chiffré (fork egyptian).
Une installation Mayan complète comprend quatre composants indépendants qui doivent tous être sauvegardés :
| Composant | Contenu | Criticité |
|---|---|---|
| Base de données (PostgreSQL) | Métadonnées, utilisateurs, ACL, tags, index, workflows, paramètres | Critique |
Fichiers média (MEDIA_ROOT) |
Fichiers documents, caches d'images, clé secrète, config | Critique |
| Elasticsearch | Index de recherche full-text | Reconstructible |
| Redis / RabbitMQ | Files de tâches en transit | Optionnel (tâches perdues) |
Elasticsearch est reconstructible via
manage.py search_reindex. Ne pas bloquer une restauration sur son absence. Redis et RabbitMQ peuvent être ignorés si vous acceptez de perdre les tâches en attente au moment de la panne.
/var/lib/mayan/ # MEDIA_ROOT (Docker) ou chemin custom
├── document_file_storage/ # ⭐ Fichiers documents (critique)
├── document_file_page_image_cache/ # Cache images (reconstructible)
├── document_version_page_image_cache/ # Cache images version (reconstructible)
├── converter_assets/ # Assets convertisseur
├── converter_assets_cache/ # Cache convertisseur (reconstructible)
├── document_signatures/ # Signatures GPG
├── download_files/ # Téléchargements temporaires
├── shared_files/ # Fichiers partagés temporaires
├── source_cache/ # Cache sources (reconstructible)
├── static/ # Fichiers statiques (reconstructible via collectstatic)
├── whoosh_index_directory/ # Index Whoosh si backend Whoosh
├── system/
│ └── SECRET_KEY # ⭐ Clé secrète Django (critique)
├── db.sqlite3 # ⭐ Base SQLite (si non-PostgreSQL)
├── config.yml # ⭐ Configuration Mayan
└── error.log # Journal d'erreurs
Minimum vital :
document_file_storage/,system/SECRET_KEY,config.yml+ dump PostgreSQL.
sudo systemctl stop mayan-worker-a mayan-worker-b mayan-worker-c mayan-worker-d mayan-worker-e
sudo systemctl stop mayan-beat
# Laisser le frontend actif si nécessaire (lecture seule pendant la sauvegarde)
# Variables
DB_NAME="mayan"
DB_USER="mayan"
BACKUP_DIR="/backups/mayan"
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p "$BACKUP_DIR"
# Dump compressé
pg_dump -U "$DB_USER" -d "$DB_NAME" \
--format=custom \
--compress=9 \
--file="$BACKUP_DIR/mayan_db_${DATE}.dump"
echo "DB backup: $BACKUP_DIR/mayan_db_${DATE}.dump"
MEDIA_ROOT="/var/lib/mayan" # Adapter selon votre installation
BACKUP_DIR="/backups/mayan"
DATE=$(date +%Y%m%d_%H%M%S)
# Archive complète (exclure les caches reconstructibles)
tar -czf "$BACKUP_DIR/mayan_media_${DATE}.tar.gz" \
--exclude="$MEDIA_ROOT/document_file_page_image_cache" \
--exclude="$MEDIA_ROOT/document_version_page_image_cache" \
--exclude="$MEDIA_ROOT/converter_assets_cache" \
--exclude="$MEDIA_ROOT/source_cache" \
--exclude="$MEDIA_ROOT/static" \
"$MEDIA_ROOT"
echo "Media backup: $BACKUP_DIR/mayan_media_${DATE}.tar.gz"
Pour exclure les caches et réduire la taille de la sauvegarde, les répertoires ci-dessus sont reconstructibles.
document_file_storage/doit toujours être inclus.
sudo systemctl start mayan-worker-a mayan-worker-b mayan-worker-c mayan-worker-d mayan-worker-e
sudo systemctl start mayan-beat
# docker-compose.yml — volumes par défaut
volumes:
app: # → /var/lib/mayan (MEDIA_ROOT)
postgres: # → /var/lib/postgresql/data
elasticsearch:# → /usr/share/elasticsearch/data
redis: # → /data
rabbitmq: # → /var/lib/rabbitmq
postgres-backups: # → /backups (volume dédié aux dumps)
# Dump PostgreSQL directement depuis le conteneur
docker compose exec postgresql \
pg_dump -U mayan mayan \
--format=custom \
--compress=9 \
> /backups/mayan/mayan_db_$(date +%Y%m%d_%H%M%S).dump
# Ou via le service postgresql-backup (si défini dans compose)
docker compose run --rm postgresql-backup
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/backups/mayan"
# Arrêter les workers (pas obligatoire mais recommandé)
docker compose stop worker_a worker_b worker_c worker_d worker_e celery_beat
# Copie du volume app via un conteneur temporaire
docker run --rm \
-v mayan_app:/source \
-v "$BACKUP_DIR":/backup \
alpine \
tar -czf "/backup/mayan_media_${DATE}.tar.gz" \
--exclude="/source/document_file_page_image_cache" \
--exclude="/source/document_version_page_image_cache" \
--exclude="/source/converter_assets_cache" \
--exclude="/source/source_cache" \
--exclude="/source/static" \
-C /source .
# Redémarrer
docker compose start worker_a worker_b worker_c worker_d worker_e celery_beat
Remplacer
mayan_apppar le nom réel de votre volume (docker volume ls).
Si vos volumes Docker sont des bind-mounts vers des répertoires hôtes :
# Vérifier les chemins des volumes dans docker-compose.yml
# Exemple : volumes: - /opt/mayan/media:/var/lib/mayan
rsync -av --delete \
--exclude="document_file_page_image_cache/" \
--exclude="document_version_page_image_cache/" \
--exclude="converter_assets_cache/" \
/opt/mayan/media/ \
/backups/mayan/media_$(date +%Y%m%d_%H%M%S)/
#!/bin/bash
# /usr/local/bin/mayan-backup.sh
set -euo pipefail
BACKUP_DIR="/backups/mayan"
DATE=$(date +%Y%m%d_%H%M%S)
COMPOSE_DIR="/opt/mayan" # Adapter
KEEP_DAYS=14
mkdir -p "$BACKUP_DIR"
echo "[$(date)] Début sauvegarde Mayan EDMS"
# 1. Dump PostgreSQL
echo "[$(date)] Dump PostgreSQL..."
docker compose -f "$COMPOSE_DIR/docker-compose.yml" exec -T postgresql \
pg_dump -U mayan mayan --format=custom --compress=9 \
> "$BACKUP_DIR/db_${DATE}.dump"
# 2. Fichiers média
echo "[$(date)] Sauvegarde média..."
docker run --rm \
-v mayan_app:/source \
-v "$BACKUP_DIR":/backup \
alpine \
tar -czf "/backup/media_${DATE}.tar.gz" \
--exclude="/source/document_file_page_image_cache" \
--exclude="/source/document_version_page_image_cache" \
--exclude="/source/converter_assets_cache" \
--exclude="/source/static" \
-C /source .
# 3. Nettoyage des anciennes sauvegardes
find "$BACKUP_DIR" -name "*.dump" -mtime +$KEEP_DAYS -delete
find "$BACKUP_DIR" -name "*.tar.gz" -mtime +$KEEP_DAYS -delete
echo "[$(date)] Sauvegarde terminée : $BACKUP_DIR"
Planifier via cron :
# Sauvegarde quotidienne à 2h du matin
0 2 * * * /usr/local/bin/mayan-backup.sh >> /var/log/mayan-backup.log 2>&1
# Arrêter tous les services Mayan
sudo systemctl stop mayan-frontend mayan-worker-* mayan-beat
# Vider le répertoire média actuel (garder les permissions)
MEDIA_ROOT="/var/lib/mayan"
sudo rm -rf "$MEDIA_ROOT"/*
DB_NAME="mayan"
DB_USER="mayan"
DUMP_FILE="/backups/mayan/mayan_db_20240101_020000.dump"
# Supprimer et recréer la base
sudo -u postgres psql -c "DROP DATABASE IF EXISTS $DB_NAME;"
sudo -u postgres psql -c "CREATE DATABASE $DB_NAME OWNER $DB_USER;"
# Restaurer
pg_restore -U "$DB_USER" -d "$DB_NAME" --no-owner --role="$DB_USER" "$DUMP_FILE"
echo "Base de données restaurée."
MEDIA_ROOT="/var/lib/mayan"
ARCHIVE="/backups/mayan/mayan_media_20240101_020000.tar.gz"
tar -xzf "$ARCHIVE" -C "$MEDIA_ROOT"
# Corriger les permissions
sudo chown -R mayan:mayan "$MEDIA_ROOT"
source /opt/mayan/venv/bin/activate
cd /opt/mayan
python manage.py migrate --settings=mayan.settings.production
sudo systemctl start mayan-frontend
sudo systemctl start mayan-worker-a mayan-worker-b mayan-worker-c mayan-worker-d mayan-worker-e
sudo systemctl start mayan-beat
cd /opt/mayan
docker compose down
# Ne pas supprimer les volumes : pas de --volumes
ARCHIVE="/backups/mayan/media_20240101_020000.tar.gz"
# Vider le volume et restaurer
docker run --rm \
-v mayan_app:/target \
-v /backups/mayan:/backup \
alpine sh -c "rm -rf /target/* && tar -xzf /backup/media_20240101_020000.tar.gz -C /target"
DUMP_FILE="/backups/mayan/db_20240101_020000.dump"
# Démarrer uniquement PostgreSQL
docker compose up -d postgresql
# Attendre qu'il soit prêt
sleep 5
# Supprimer et recréer la base
docker compose exec postgresql psql -U mayan -c "DROP DATABASE IF EXISTS mayan;"
docker compose exec postgresql psql -U mayan -c "CREATE DATABASE mayan OWNER mayan;"
# Restaurer
docker compose exec -T postgresql \
pg_restore -U mayan -d mayan --no-owner < "$DUMP_FILE"
# Migrations (si changement de version)
docker compose run --rm app \
python manage.py migrate --settings=mayan.settings.production
# Relancer toute la stack
docker compose up -d
# Vérifier que les conteneurs sont up
docker compose ps
# Vérifier les logs
docker compose logs --tail=50 app
# Tester l'accès web
curl -I https://votre-domaine.example.com/
L'index Elasticsearch est entièrement reconstructible depuis la base de données. La sauvegarde de l'index ES est optionnelle mais peut faire gagner des heures de réindexation sur les grandes installations.
C'est la méthode la plus simple et la plus fiable. Après restauration de la base de données :
# Sans Docker
source /opt/mayan/venv/bin/activate
python manage.py search_reindex --settings=mayan.settings.production
# Avec Docker
docker compose run --rm app \
python manage.py search_reindex
Durée estimée : environ 1 000 à 5 000 documents/minute selon la configuration.
Sur 100 000 documents : 20 à 100 minutes.
Pour les grandes installations (>100 000 documents) où la réindexation est trop longue.
# Dans elasticsearch.yml (ou via variables d'environnement Docker)
path.repo: /usr/share/elasticsearch/backups
# Docker : monter le volume de backup
volumes:
- /backups/elasticsearch:/usr/share/elasticsearch/backups
ES_HOST="https://es.example.com:9200"
ES_USER="elastic"
ES_PASS="votre_mot_de_passe"
curl -X PUT "$ES_HOST/_snapshot/mayan_backup" \
-u "$ES_USER:$ES_PASS" \
-H "Content-Type: application/json" \
-d '{
"type": "fs",
"settings": {
"location": "/usr/share/elasticsearch/backups/mayan_backup",
"compress": true
}
}'
DATE=$(date +%Y%m%d_%H%M%S)
curl -X PUT "$ES_HOST/_snapshot/mayan_backup/snapshot_${DATE}?wait_for_completion=true" \
-u "$ES_USER:$ES_PASS" \
-H "Content-Type: application/json" \
-d '{
"indices": "mayan_*",
"ignore_unavailable": true,
"include_global_state": false
}'
curl -X GET "$ES_HOST/_snapshot/mayan_backup/_all" \
-u "$ES_USER:$ES_PASS" | python3 -m json.tool
SNAPSHOT="snapshot_20240101_020000"
# Fermer les indices existants avant restauration
curl -X POST "$ES_HOST/mayan_*/_close" -u "$ES_USER:$ES_PASS"
# Restaurer
curl -X POST "$ES_HOST/_snapshot/mayan_backup/${SNAPSHOT}/_restore?wait_for_completion=true" \
-u "$ES_USER:$ES_PASS" \
-H "Content-Type: application/json" \
-d '{
"indices": "mayan_*",
"ignore_unavailable": true
}'
# Snapshot depuis Docker
docker compose exec elasticsearch \
curl -X PUT "https://localhost:9200/_snapshot/mayan_backup/snapshot_$(date +%Y%m%d)" \
--cacert /usr/share/elasticsearch/config/certs/ca/ca.crt \
-u "elastic:${MAYAN_ELASTICSEARCH_PASSWORD}" \
-H "Content-Type: application/json" \
-d '{"indices": "mayan_*", "include_global_state": false}'
# Réindexer tous les objets
python manage.py search_reindex
# Vérifier le statut du backend
python manage.py search_status
# Initialiser/recréer les indices (après changement de schéma)
python manage.py search_initialize
# Mettre à niveau les indices (après upgrade Mayan)
python manage.py search_upgrade
# Indexer des objets spécifiques
python manage.py search_index_objects --model Document
Cette section concerne uniquement le fork
egyptianqui utiliseEncryptedPassthroughStorage.
Les fichiers documents sont stockés chiffrés sur le filesystem (format MENC : magic header + sel + nonce + ciphertext + tag AES-256-GCM). La base de données ne contient que le nom du fichier, pas la clé de chiffrement.
Conséquences pour la sauvegarde :
document_file_storage/ sont opaques (chiffrés) — ne pas tenter de les lire directement# La clé est configurée dans config.yml ou une variable d'environnement
# DOCUMENTS_FILE_STORAGE_BACKEND_ARGUMENTS.password (ou similaire)
# Option 1 : Copie sécurisée dans un vault (Bitwarden, Vault, etc.)
# Option 2 : Chiffrement de la clé avec GPG et stockage hors site
echo "MOT_DE_PASSE_CHIFFREMENT" | gpg --encrypt --recipient admin@example.com \
> /backups/secure/mayan_encryption_key.gpg
# Option 3 : Variable d'environnement dans un secret manager (Docker Swarm, K8s)
Règle d'or : La clé de chiffrement ne doit jamais être dans la même sauvegarde que les fichiers chiffrés. Stockez-la dans un système séparé (gestionnaire de mots de passe, HSM, coffre-fort physique).
La procédure est identique à une restauration standard, à condition que :
config.yml ou les variables d'environnementDOCUMENTS_FILE_STORAGE_BACKEND pointe bien vers EncryptedPassthroughStoragepassword soient corrects# Vérifier la configuration après restauration
docker compose run --rm app \
python manage.py shell -c "
from mayan.apps.documents.models import DocumentFile
f = DocumentFile.objects.first()
print(f.open().read(10)) # Doit retourner des octets lisibles, pas MENC...
"
Si vous obtenez une InvalidTag ou des données corrompues, le mot de passe est incorrect.
# 1. Installer Docker + Docker Compose sur le nouveau serveur
# 2. Copier les sauvegardes sur le nouveau serveur
scp /backups/mayan/db_20240101.dump user@nouveau-serveur:/backups/
scp /backups/mayan/media_20240101.tar.gz user@nouveau-serveur:/backups/
# 3. Récupérer le docker-compose.yml et config.yml depuis le backup ou le dépôt git
# 4. Créer les volumes Docker
docker volume create mayan_app
docker volume create mayan_postgres
# 5. Restaurer le média
docker run --rm \
-v mayan_app:/target \
-v /backups:/backup \
alpine sh -c "tar -xzf /backup/media_20240101.tar.gz -C /target"
# 6. Démarrer PostgreSQL seul
docker compose up -d postgresql
sleep 10
# 7. Restaurer la base
docker compose exec postgresql psql -U mayan -c "CREATE DATABASE mayan OWNER mayan;"
docker compose exec -T postgresql pg_restore -U mayan -d mayan --no-owner < /backups/db_20240101.dump
# 8. Démarrer toute la stack
docker compose up -d
# 9. Appliquer les migrations si nécessaire
docker compose run --rm app python manage.py migrate
# 10. Réindexer Elasticsearch
docker compose run --rm app python manage.py search_reindex
# 11. Vérifier
docker compose ps
curl -I https://votre-domaine.example.com/
Ne pas attendre une catastrophe pour vérifier que les sauvegardes fonctionnent.
# Test sans restaurer (vérifie la lisibilité du dump)
pg_restore --list /backups/mayan/db_20240101.dump | head -20
# Compter les objets dans le dump
pg_restore --list /backups/mayan/db_20240101.dump | wc -l
# Lister le contenu sans extraire
tar -tzf /backups/mayan/media_20240101.tar.gz | head -20
# Vérifier l'intégrité
tar -tzf /backups/mayan/media_20240101.tar.gz > /dev/null && echo "OK" || echo "CORROMPU"
# Restaurer sur un serveur de test et vérifier :
# 1. L'interface web répond
# 2. Les documents sont accessibles et lisibles
# 3. La recherche retourne des résultats
# 4. Un document peut être téléchargé
# Fréquence recommandée : mensuelle
| Type | Fréquence | Rétention |
|---|---|---|
| Dump PostgreSQL | Quotidienne | 30 jours |
| Archive média complète | Hebdomadaire | 12 semaines |
| Archive média incrémentale (rsync) | Quotidienne | 14 jours |
| Snapshot Elasticsearch | Hebdomadaire | 4 semaines |
# Sauvegarde incrémentale avec liens durs (efficient en espace)
rsync -av --delete \
--link-dest=/backups/mayan/media_latest/ \
--exclude="document_file_page_image_cache/" \
--exclude="document_version_page_image_cache/" \
/var/lib/mayan/ \
/backups/mayan/media_$(date +%Y%m%d)/
# Mettre à jour le lien "latest"
ln -sfn /backups/mayan/media_$(date +%Y%m%d) /backups/mayan/media_latest
| Outil | Documentation |
|---|---|
pg_dump / pg_restore |
PostgreSQL Backup |
| Elasticsearch Snapshot API | ES Snapshot docs |
mayan/apps/dynamic_search/management/ |
Commandes de réindexation |
mayan/apps/storage/backends/encryptedstorage.py |
Implémentation du chiffrement |