Skip to main content

Overview

A robust backup strategy is essential for protecting your Sakai LMS data and ensuring business continuity. This guide covers backup procedures, restoration processes, and disaster recovery planning.

Backup Strategy

Components to Backup

A complete Sakai backup includes:
  1. Database - All Sakai data (users, courses, content metadata)
  2. File System - Content hosting files, attachments, resources
  3. Configuration Files - sakai.properties, Tomcat configuration
  4. Custom Code - Local customizations, plugins, skins

Backup Schedule

ComponentFrequencyRetention
Database (Full)Daily30 days
Database (Incremental)Hourly7 days
File SystemDaily30 days
ConfigurationWeekly90 days
Point-in-timeEvery 5 minutes24 hours

Database Backup

MySQL/MariaDB Backup

Full Backup

Create a complete database backup:
#!/bin/bash
# Sakai MySQL backup script

BACKUP_DIR="/backup/sakai/mysql"
DATE=$(date +%Y%m%d_%H%M%S)
DB_NAME="sakai"
DB_USER="sakai"
DB_PASS="your_password"

# Create backup directory
mkdir -p $BACKUP_DIR

# Full backup with compression
mysqldump -u $DB_USER -p$DB_PASS \
  --single-transaction \
  --routines \
  --triggers \
  --events \
  --hex-blob \
  $DB_NAME | gzip > $BACKUP_DIR/sakai_full_$DATE.sql.gz

# Verify backup
if [ $? -eq 0 ]; then
  echo "Backup completed: sakai_full_$DATE.sql.gz"
else
  echo "Backup failed!" >&2
  exit 1
fi
Use --single-transaction for InnoDB tables to ensure consistent backups without locking tables. This is critical for production systems.

Binary Log Backup (Point-in-Time Recovery)

Enable binary logging in MySQL configuration:
# /etc/mysql/my.cnf
[mysqld]
log_bin = /var/log/mysql/mysql-bin.log
expire_logs_days = 7
max_binlog_size = 100M
server-id = 1
Backup binary logs:
#!/bin/bash
BINLOG_DIR="/var/log/mysql"
BACKUP_DIR="/backup/sakai/binlogs"
DATE=$(date +%Y%m%d)

mysqladmin flush-logs
rsync -av --remove-source-files $BINLOG_DIR/mysql-bin.* $BACKUP_DIR/$DATE/

Oracle Backup

RMAN Backup

-- Full database backup
RMAN> BACKUP DATABASE PLUS ARCHIVELOG;

-- Incremental backup
RMAN> BACKUP INCREMENTAL LEVEL 1 DATABASE;

-- Validate backup
RMAN> RESTORE DATABASE VALIDATE;

Export Backup

#!/bin/bash
expdp sakai/password@SAKAIDB \
  directory=DATA_PUMP_DIR \
  dumpfile=sakai_full_%U.dmp \
  logfile=sakai_export.log \
  full=yes \
  parallel=4 \
  compression=all

PostgreSQL Backup

#!/bin/bash
BACKUP_DIR="/backup/sakai/postgres"
DATE=$(date +%Y%m%d_%H%M%S)

pg_dump -U sakai -d sakai -F c -f $BACKUP_DIR/sakai_$DATE.dump

# With compression
pg_dump -U sakai -d sakai | gzip > $BACKUP_DIR/sakai_$DATE.sql.gz

File System Backup

Content Hosting Backup

Sakai stores files in the content hosting directory (typically /var/sakai/content):
#!/bin/bash
CONTENT_DIR="/var/sakai/content"
BACKUP_DIR="/backup/sakai/content"
DATE=$(date +%Y%m%d)

# Incremental backup using rsync
rsync -avz --delete \
  --link-dest=$BACKUP_DIR/latest \
  $CONTENT_DIR/ \
  $BACKUP_DIR/$DATE/

# Update latest symlink
ln -snf $BACKUP_DIR/$DATE $BACKUP_DIR/latest
Content hosting can be very large (hundreds of GB to TB). Use incremental backups and consider deduplication to save storage space.

Configuration Backup

Backup critical configuration files:
#!/bin/bash
BACKUP_DIR="/backup/sakai/config"
DATE=$(date +%Y%m%d)
TOMCAT_HOME="/opt/tomcat"
SAKAI_HOME="/var/sakai"

tar -czf $BACKUP_DIR/config_$DATE.tar.gz \
  $SAKAI_HOME/sakai.properties \
  $TOMCAT_HOME/conf/ \
  $TOMCAT_HOME/bin/setenv.sh \
  /etc/httpd/conf.d/sakai.conf

Automated Backup Script

Complete Backup Solution

#!/bin/bash
# Comprehensive Sakai backup script

set -e

# Configuration
BACKUP_ROOT="/backup/sakai"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=30
DB_NAME="sakai"
DB_USER="sakai"
DB_PASS="$(cat /root/.mysql_password)"
CONTENT_DIR="/var/sakai/content"
LOG_FILE="$BACKUP_ROOT/backup.log"

# Logging function
log() {
  echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" | tee -a $LOG_FILE
}

log "Starting Sakai backup"

# 1. Database backup
log "Backing up database..."
mkdir -p $BACKUP_ROOT/database
mysqldump -u $DB_USER -p$DB_PASS \
  --single-transaction \
  --routines --triggers --events \
  $DB_NAME | gzip > $BACKUP_ROOT/database/sakai_$DATE.sql.gz

if [ $? -eq 0 ]; then
  log "Database backup completed"
else
  log "ERROR: Database backup failed"
  exit 1
fi

# 2. Content hosting backup
log "Backing up content hosting..."
mkdir -p $BACKUP_ROOT/content/$DATE
rsync -avz --delete \
  --link-dest=$BACKUP_ROOT/content/latest \
  $CONTENT_DIR/ \
  $BACKUP_ROOT/content/$DATE/

ln -snf $BACKUP_ROOT/content/$DATE $BACKUP_ROOT/content/latest
log "Content backup completed"

# 3. Configuration backup
log "Backing up configuration..."
mkdir -p $BACKUP_ROOT/config
tar -czf $BACKUP_ROOT/config/config_$DATE.tar.gz \
  /var/sakai/sakai.properties \
  /opt/tomcat/conf/ \
  /opt/tomcat/bin/setenv.sh

log "Configuration backup completed"

# 4. Cleanup old backups
log "Cleaning up old backups..."
find $BACKUP_ROOT/database -name "*.sql.gz" -mtime +$RETENTION_DAYS -delete
find $BACKUP_ROOT/config -name "*.tar.gz" -mtime +$RETENTION_DAYS -delete
find $BACKUP_ROOT/content -maxdepth 1 -type d -mtime +$RETENTION_DAYS -exec rm -rf {} \;

log "Backup completed successfully"

# 5. Send backup status notification
# mail -s "Sakai Backup Status" [email protected] < $LOG_FILE

Schedule with Cron

# crontab -e
# Daily backup at 2 AM
0 2 * * * /usr/local/bin/sakai-backup.sh

# Hourly incremental database backup
0 * * * * /usr/local/bin/sakai-incremental-backup.sh

Disaster Recovery Testing

Test Restore Procedure

  1. Create a test environment separate from production
  2. Restore database from latest backup
  3. Restore content hosting files
  4. Verify application starts successfully
  5. Test critical functionality (login, content access, submissions)
  6. Document any issues and recovery time
Test your restore procedures at least quarterly. Untested backups are not backups.

Restore Procedures

Database Restore

MySQL/MariaDB Full Restore

#!/bin/bash
BACKUP_FILE="/backup/sakai/database/sakai_20260304_020000.sql.gz"
DB_NAME="sakai"
DB_USER="sakai"
DB_PASS="your_password"

# Stop Tomcat
systemctl stop tomcat

# Drop and recreate database
mysql -u root -p << EOF
DROP DATABASE IF EXISTS $DB_NAME;
CREATE DATABASE $DB_NAME CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
GRANT ALL ON $DB_NAME.* TO '$DB_USER'@'localhost';
FLUSH PRIVILEGES;
EOF

# Restore from backup
zcat $BACKUP_FILE | mysql -u $DB_USER -p$DB_PASS $DB_NAME

# Start Tomcat
systemctl start tomcat

Point-in-Time Recovery (MySQL)

#!/bin/bash
# Restore to specific timestamp

FULL_BACKUP="/backup/sakai/database/sakai_20260304_020000.sql.gz"
BINLOG_DIR="/backup/sakai/binlogs/20260304"
TARGET_TIME="2026-03-04 14:30:00"

# 1. Restore full backup
zcat $FULL_BACKUP | mysql -u sakai -p sakai

# 2. Apply binary logs up to target time
mysqlbinlog --stop-datetime="$TARGET_TIME" \
  $BINLOG_DIR/mysql-bin.* | mysql -u sakai -p sakai

Oracle Restore

-- RMAN restore
RMAN> SHUTDOWN IMMEDIATE;
RMAN> STARTUP MOUNT;
RMAN> RESTORE DATABASE;
RMAN> RECOVER DATABASE;
RMAN> ALTER DATABASE OPEN;

Content Hosting Restore

#!/bin/bash
BACKUP_DIR="/backup/sakai/content/20260304"
CONTENT_DIR="/var/sakai/content"

# Stop Tomcat
systemctl stop tomcat

# Backup current content (safety)
mv $CONTENT_DIR $CONTENT_DIR.pre-restore

# Restore from backup
rsync -avz $BACKUP_DIR/ $CONTENT_DIR/

# Fix permissions
chown -R tomcat:tomcat $CONTENT_DIR

# Start Tomcat
systemctl start tomcat

Configuration Restore

#!/bin/bash
CONFIG_BACKUP="/backup/sakai/config/config_20260304.tar.gz"

# Extract configuration
tar -xzf $CONFIG_BACKUP -C /

# Restart services
systemctl restart tomcat

Special Considerations

Site Statistics Database

If using a separate statistics database:
# Backup site statistics
mysqldump -u stats_user -p \
  --single-transaction \
  sakai_stats | gzip > /backup/sakai/stats_$DATE.sql.gz
You may choose not to restore statistics in disaster recovery scenarios as they are not critical for system operation.

Search Index

If using Elasticsearch for search:
# Elasticsearch snapshot
curl -X PUT "localhost:9200/_snapshot/sakai_backup/snapshot_$DATE?wait_for_completion=true"

# Restore snapshot
curl -X POST "localhost:9200/_snapshot/sakai_backup/snapshot_$DATE/_restore"
Sakai can rebuild the search index from database content, so search index backups are optional.

Content-Review Integration

For Turnitin or other content-review integrations:
# Disable indexing in QA/restore environments
contentreview.enableContentReview=false
When restoring production data to QA/staging environments, disable content-review indexing to prevent duplicate submissions.

Offsite Backup

Cloud Storage Sync

#!/bin/bash
# Sync backups to AWS S3
BACKUP_DIR="/backup/sakai"
S3_BUCKET="s3://my-sakai-backups"

aws s3 sync $BACKUP_DIR $S3_BUCKET \
  --storage-class GLACIER \
  --exclude "*.tmp" \
  --delete

Encryption for Offsite Storage

#!/bin/bash
# Encrypt backup before upload
BACKUP_FILE="sakai_full_$DATE.sql.gz"
GPG_RECIPIENT="[email protected]"

gpg --encrypt --recipient $GPG_RECIPIENT $BACKUP_FILE
aws s3 cp ${BACKUP_FILE}.gpg s3://my-sakai-backups/

Recovery Time Objectives

ScenarioRTO TargetRPO Target
Database corruption2 hours5 minutes
Complete server failure4 hours1 hour
Data center disaster24 hours24 hours
Accidental deletion1 hour1 hour
RTO = Recovery Time Objective (time to restore service)
RPO = Recovery Point Objective (acceptable data loss)

Best Practices

3-2-1 Rule

Maintain 3 copies of data, on 2 different media types, with 1 copy offsite.

Test Regularly

Test restore procedures quarterly. Update documentation based on test results.

Monitor Backups

Set up alerts for backup failures. Verify backup integrity automatically.

Document Everything

Maintain runbooks for recovery procedures. Include contact information for vendors.

Troubleshooting

Backup Failures

Problem: Database backup fails with “Lock wait timeout” Solution: Use --single-transaction for InnoDB tables or schedule backups during low-usage periods. Problem: Content backup takes too long Solution: Use incremental backups with rsync or implement block-level snapshots.

Restore Issues

Problem: Restored database has different character encoding Solution: Ensure database is created with correct charset:
CREATE DATABASE sakai CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
Problem: Content files have wrong permissions after restore Solution: Fix ownership recursively:
chown -R tomcat:tomcat /var/sakai/content
chmod -R 755 /var/sakai/content

Build docs developers (and LLMs) love