Backup Automation¶
Losing email data is catastrophic. This guide sets up automated backups for every piece of your Mailyte deployment — database, mail storage, DKIM keys, SSL certs, and configuration.
What Needs Backing Up¶
| Component | Location | Priority | Frequency |
|---|---|---|---|
| MySQL database | Docker volume mysql_data | Critical | Every 6 hours |
| Mail storage | ./storage/mail_data/ | Critical | Daily |
| DKIM keys | ./storage/dkim_keys/ | Critical | Daily |
| SSL certificates | ./storage/ssl_certs/, ./storage/ssl_private/ | High | Daily |
| Configuration | ./config/ | High | On change |
| Redis data | Docker volume redis_data | Medium | Daily |
| Rspamd data | Docker volume rspamd_data | Medium | Weekly |
| Qdrant vectors | Docker volume qdrant_data | Low | Weekly |
Backup Script¶
Create a comprehensive backup script:
Bash
#!/bin/bash
# scripts/backup.sh — Mailyte automated backup
set -euo pipefail
# Configuration
BACKUP_DIR="/opt/mailyte/backups"
MAILYTE_DIR="/home/confidence/Workspace/Projects/TechiesAfrica/Internal/Mailyte/mailyte-email-server"
RETENTION_DAYS=30
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_PATH="${BACKUP_DIR}/${DATE}"
# S3 configuration (optional)
S3_BUCKET="${AWS_BUCKET:-}"
S3_PREFIX="${AWS_S3_PREFIX:-mailyte}/backups"
echo "Starting backup: ${DATE}"
mkdir -p "${BACKUP_PATH}"
# -----------------------------------------------
# 1. MySQL Database
# -----------------------------------------------
echo "Backing up MySQL..."
docker exec mysql mysqldump \
-u root \
-p"${DB_ROOT_PASSWORD:-rootpassword}" \
--single-transaction \
--routines \
--triggers \
--events \
"${DB_NAME:-mailserver}" \
| gzip > "${BACKUP_PATH}/mysql_${DATE}.sql.gz"
echo "MySQL backup: $(du -sh ${BACKUP_PATH}/mysql_${DATE}.sql.gz | cut -f1)"
# -----------------------------------------------
# 2. Mail Storage
# -----------------------------------------------
echo "Backing up mail storage..."
tar czf "${BACKUP_PATH}/mail_data_${DATE}.tar.gz" \
-C "${MAILYTE_DIR}/storage" mail_data/
echo "Mail storage backup: $(du -sh ${BACKUP_PATH}/mail_data_${DATE}.tar.gz | cut -f1)"
# -----------------------------------------------
# 3. DKIM Keys
# -----------------------------------------------
echo "Backing up DKIM keys..."
tar czf "${BACKUP_PATH}/dkim_keys_${DATE}.tar.gz" \
-C "${MAILYTE_DIR}/storage" dkim_keys/
# -----------------------------------------------
# 4. SSL Certificates
# -----------------------------------------------
echo "Backing up SSL certificates..."
tar czf "${BACKUP_PATH}/ssl_${DATE}.tar.gz" \
-C "${MAILYTE_DIR}/storage" ssl_certs/ ssl_private/
# -----------------------------------------------
# 5. Configuration
# -----------------------------------------------
echo "Backing up configuration..."
tar czf "${BACKUP_PATH}/config_${DATE}.tar.gz" \
-C "${MAILYTE_DIR}" config/ .env docker-compose.yml
# -----------------------------------------------
# 6. Redis (optional)
# -----------------------------------------------
echo "Backing up Redis..."
docker exec redis redis-cli BGSAVE
sleep 2
docker cp redis:/data/dump.rdb "${BACKUP_PATH}/redis_${DATE}.rdb" 2>/dev/null || echo "Redis backup skipped"
# -----------------------------------------------
# 7. Create manifest
# -----------------------------------------------
echo "Creating manifest..."
cat > "${BACKUP_PATH}/manifest.json" <<EOF
{
"date": "${DATE}",
"hostname": "$(hostname)",
"components": {
"mysql": "mysql_${DATE}.sql.gz",
"mail_data": "mail_data_${DATE}.tar.gz",
"dkim_keys": "dkim_keys_${DATE}.tar.gz",
"ssl": "ssl_${DATE}.tar.gz",
"config": "config_${DATE}.tar.gz",
"redis": "redis_${DATE}.rdb"
},
"sizes": {
$(cd "${BACKUP_PATH}" && find . -type f -not -name manifest.json -exec sh -c 'echo " \"$(basename {})\": \"$(du -sh {} | cut -f1)\"," ' \;)
"_total": "$(du -sh ${BACKUP_PATH} | cut -f1)"
}
}
EOF
# -----------------------------------------------
# 8. Sync to S3 (if configured)
# -----------------------------------------------
if [ -n "${S3_BUCKET}" ]; then
echo "Syncing to S3..."
aws s3 sync "${BACKUP_PATH}" "s3://${S3_BUCKET}/${S3_PREFIX}/${DATE}/" \
--storage-class STANDARD_IA
echo "S3 sync complete"
fi
# -----------------------------------------------
# 9. Clean old backups
# -----------------------------------------------
echo "Cleaning backups older than ${RETENTION_DAYS} days..."
find "${BACKUP_DIR}" -maxdepth 1 -type d -mtime +${RETENTION_DAYS} -exec rm -rf {} \;
# S3 lifecycle handles remote retention (see below)
echo "Backup complete: ${BACKUP_PATH}"
echo "Total size: $(du -sh ${BACKUP_PATH} | cut -f1)"
Make it executable:
Cron Schedule¶
Add these entries:
Text Only
# Database backup every 6 hours
0 */6 * * * /opt/mailyte/scripts/backup.sh >> /var/log/mailyte-backup.log 2>&1
# Full backup daily at 2am
0 2 * * * FULL_BACKUP=1 /opt/mailyte/scripts/backup.sh >> /var/log/mailyte-backup.log 2>&1
Log rotation
Add a logrotate config for the backup log:
S3 Sync Configuration¶
AWS CLI Setup¶
Bash
# Install AWS CLI
pip install awscli
# Configure credentials
aws configure
# Or use environment variables:
# AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION
S3 Lifecycle Policy¶
Set up automatic S3 lifecycle rules for cost-effective long-term storage:
JSON
{
"Rules": [
{
"ID": "MailyteBackupLifecycle",
"Status": "Enabled",
"Filter": {
"Prefix": "mailyte/backups/"
},
"Transitions": [
{
"Days": 30,
"StorageClass": "STANDARD_IA"
},
{
"Days": 90,
"StorageClass": "GLACIER"
}
],
"Expiration": {
"Days": 365
}
}
]
}
Apply it:
Bash
aws s3api put-bucket-lifecycle-configuration \
--bucket your-backup-bucket \
--lifecycle-configuration file://lifecycle.json
Cost Estimation¶
| Volume | Daily Backup Size | Monthly S3 Cost (approx.) |
|---|---|---|
| 10K mailboxes | ~5 GB | ~$3 |
| 50K mailboxes | ~25 GB | ~$15 |
| 100K mailboxes | ~50 GB | ~$30 |
Glacier storage is ~$0.004/GB/month, so 90+ day old backups cost almost nothing.
Restore Procedures¶
Restore MySQL¶
Bash
# Stop services that write to the database
docker compose stop api tracking webhooks analytics queue-manager
# Restore
gunzip -c backups/20260325_020000/mysql_20260325_020000.sql.gz | \
docker exec -i mysql mysql -u root -p"${DB_ROOT_PASSWORD}" "${DB_NAME:-mailserver}"
# Restart services
docker compose start api tracking webhooks analytics queue-manager
Restore Mail Storage¶
Bash
# Stop Dovecot to prevent file conflicts
docker compose stop dovecot
# Restore
tar xzf backups/20260325_020000/mail_data_20260325_020000.tar.gz \
-C /path/to/mailyte/storage/
# Fix permissions
chown -R 5000:5000 /path/to/mailyte/storage/mail_data/
# Restart Dovecot
docker compose start dovecot
Restore DKIM Keys¶
Bash
tar xzf backups/20260325_020000/dkim_keys_20260325_020000.tar.gz \
-C /path/to/mailyte/storage/
# Restart Rspamd to pick up keys
docker compose restart rspamd
Restore from S3¶
Bash
# Download a specific backup
aws s3 sync "s3://your-bucket/mailyte/backups/20260325_020000/" \
/opt/mailyte/backups/20260325_020000/
# Then follow the restore procedures above
Monitoring Backups¶
Health Check Script¶
Bash
#!/bin/bash
# scripts/check_backup.sh — verify backup health
BACKUP_DIR="/opt/mailyte/backups"
MAX_AGE_HOURS=12
latest=$(ls -td ${BACKUP_DIR}/*/ 2>/dev/null | head -1)
if [ -z "$latest" ]; then
echo "CRITICAL: No backups found"
exit 2
fi
age_hours=$(( ($(date +%s) - $(stat -c %Y "$latest")) / 3600 ))
if [ $age_hours -gt $MAX_AGE_HOURS ]; then
echo "WARNING: Latest backup is ${age_hours}h old (max: ${MAX_AGE_HOURS}h)"
exit 1
fi
# Check that key files exist
for f in mysql_*.sql.gz mail_data_*.tar.gz dkim_keys_*.tar.gz; do
if ! ls ${latest}/${f} &>/dev/null; then
echo "WARNING: Missing ${f} in latest backup"
exit 1
fi
done
echo "OK: Latest backup is ${age_hours}h old"
exit 0
Prometheus Integration¶
Expose backup age as a metric:
Bash
# Add to your node-exporter textfile collector
echo "mailyte_backup_age_hours $age_hours" > /var/lib/node-exporter/textfile/backup.prom
Alert if backups are stale:
YAML
- alert: BackupStale
expr: mailyte_backup_age_hours > 24
for: 1h
labels:
severity: critical
annotations:
summary: "No successful backup in {{ $value }} hours"
Backup Checklist¶
- Backup script created and tested
- Cron jobs scheduled
- S3 sync configured (if using cloud storage)
- S3 lifecycle policy applied
- Restore procedures tested (at least once!)
- Backup monitoring in place
- Alert on stale backups
- Log rotation configured