Backup and Restore#
How to protect your Mailu data with backups and recover from failures.
Problem#
You need to protect email data, user accounts, configuration, and DKIM keys from hardware failures, human errors, or disasters. You also need a tested procedure to restore service after data loss.
Solution#
Implement a comprehensive backup strategy covering PostgreSQL database, PersistentVolumes (mailboxes), and Kubernetes configuration. Test restore procedures regularly to ensure backups are viable.
What to Backup#
Critical Data (Must Backup)#
Component |
Data |
Backup Method |
Frequency |
Priority |
|---|---|---|---|---|
PostgreSQL |
User accounts, domains, aliases |
pg_dump |
Daily |
Critical |
Dovecot PVC |
User mailboxes (email content) |
Volume snapshots |
Daily |
Critical |
Admin PVC |
DKIM keys, config |
Volume snapshots |
Weekly |
High |
Rspamd PVC |
Spam learning data |
Volume snapshots |
Weekly |
Medium |
Optional Data (Nice to Backup)#
Component |
Data |
Priority |
|---|---|---|
Postfix PVC |
Mail queue (temporary) |
Low (self-healing) |
ClamAV PVC |
Virus signatures (re-downloadable) |
Low |
Webdav PVC |
Calendars, contacts |
Medium |
Configuration (Version Control)#
Component |
Backup Method |
|---|---|
CDK8S source code |
Git repository |
Generated manifests |
Git repository |
Kubernetes secrets |
Encrypted backup or external secret manager |
Backup PostgreSQL Database#
Manual Database Backup#
Create a one-time database backup:
# Dump entire database
kubectl exec -n postgres postgres-1 -- \
pg_dump -U postgres -d mailu --clean --if-exists \
> mailu-db-backup-$(date +%Y%m%d-%H%M%S).sql
# Compress for storage
gzip mailu-db-backup-*.sql
Backup size: Typically 1-10MB for small deployments, scales with user count.
Automated Database Backups#
Create a CronJob for daily database backups:
apiVersion: batch/v1
kind: CronJob
metadata:
name: mailu-db-backup
namespace: mailu
spec:
schedule: "0 2 * * *" # Daily at 2 AM
jobTemplate:
spec:
template:
spec:
serviceAccountName: mailu-backup
containers:
- name: backup
image: postgres:15
command:
- /bin/bash
- -c
- |
set -e
BACKUP_FILE="/backups/mailu-db-$(date +%Y%m%d-%H%M%S).sql"
pg_dump -h postgres-rw -U mailu -d mailu --clean --if-exists > "$BACKUP_FILE"
gzip "$BACKUP_FILE"
echo "Backup completed: ${BACKUP_FILE}.gz"
# Cleanup backups older than 30 days
find /backups -name "mailu-db-*.sql.gz" -mtime +30 -delete
env:
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: postgres-app
key: password
volumeMounts:
- name: backups
mountPath: /backups
volumes:
- name: backups
persistentVolumeClaim:
claimName: mailu-db-backups
restartPolicy: OnFailure
Prerequisites: Create PVC for backup storage:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mailu-db-backups
namespace: mailu
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi # Adjust based on retention period
storageClassName: longhorn
CloudNativePG Automated Backups#
If using CloudNativePG (CNPG), configure S3 backups in the PostgreSQL cluster:
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: postgres
namespace: postgres
spec:
instances: 3
storage:
size: 20Gi
backup:
barmanObjectStore:
destinationPath: s3://backup-bucket/postgres-mailu/
s3Credentials:
accessKeyId:
name: s3-credentials
key: access-key
secretAccessKey:
name: s3-credentials
key: secret-key
wal:
compression: gzip
maxParallel: 2
retentionPolicy: "30d"
# Daily full backup at 2 AM
scheduledBackup:
- name: daily-backup
schedule: "0 2 * * *"
backupOwnerReference: self
target: primary
Advantages: Point-in-time recovery (PITR), automatic WAL archiving, off-site storage.
Backup PersistentVolumes#
Longhorn Volume Snapshots#
If using Longhorn storage, create volume snapshots:
Manual Snapshot#
# Snapshot Dovecot mailboxes
kubectl create -f - <<EOF
apiVersion: longhorn.io/v1beta2
kind: VolumeSnapshot
metadata:
name: dovecot-snapshot-$(date +%Y%m%d)
namespace: longhorn-system
spec:
volume: mailu-dovecot-pvc-xxx # Get from: kubectl get pv
EOF
# List snapshots
kubectl get volumesnapshot -n longhorn-system
Automated Recurring Snapshots#
Configure Longhorn RecurringJob for automatic snapshots:
apiVersion: longhorn.io/v1beta2
kind: RecurringJob
metadata:
name: mailu-daily-snapshot
namespace: longhorn-system
spec:
cron: "0 3 * * *" # Daily at 3 AM
task: "snapshot"
retain: 7 # Keep 7 daily snapshots
concurrency: 2
labels:
app: mailu
Apply to Dovecot PVC:
# Label PVC for automatic snapshots
kubectl label pvc -n mailu mailu-dovecot-pvc recurring-job.longhorn.io/mailu-daily-snapshot=enabled
Longhorn Backup to S3#
Configure Longhorn backup target for off-site backups:
# Set S3 backup target (via Longhorn UI or kubectl)
kubectl patch settings.longhorn.io -n longhorn-system backup-target --type=merge -p '{"value":"s3://backup-bucket@region/"}'
# Create backup from snapshot
kubectl create -f - <<EOF
apiVersion: longhorn.io/v1beta2
kind: Backup
metadata:
name: dovecot-backup-$(date +%Y%m%d)
namespace: longhorn-system
spec:
snapshotName: dovecot-snapshot-YYYYMMDD
labels:
app: mailu
component: dovecot
EOF
Velero Cluster Backups#
For full cluster-level backups, use Velero:
# Install Velero with S3 backend
velero install \
--provider aws \
--bucket mailu-backups \
--secret-file ./credentials-velero \
--backup-location-config region=us-east-1
# Backup Mailu namespace
velero backup create mailu-backup-$(date +%Y%m%d) \
--include-namespaces mailu \
--default-volumes-to-fs-backup
# Schedule daily backups
velero schedule create mailu-daily \
--schedule="0 2 * * *" \
--include-namespaces mailu \
--default-volumes-to-fs-backup \
--ttl 720h # 30 days retention
Backup Kubernetes Secrets#
Warning: Secrets contain sensitive data. Encrypt backups and store securely.
Manual Secret Backup#
# Export all Mailu secrets
kubectl get secrets -n mailu -o yaml > mailu-secrets-backup-$(date +%Y%m%d).yaml
# Encrypt with GPG
gpg --symmetric --cipher-algo AES256 mailu-secrets-backup-*.yaml
# Store encrypted file securely (off-site)
Sealed Secrets#
Use Sealed Secrets for safe secret storage in git:
# Install Sealed Secrets controller
kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/latest/download/controller.yaml
# Create sealed secret
kubectl create secret generic mailu-secrets \
--from-literal=secret-key="..." \
--dry-run=client -o yaml | \
kubeseal -o yaml > sealed-secret-mailu.yaml
# Commit sealed secret to git (safe - encrypted)
git add sealed-secret-mailu.yaml
git commit -m "Add Mailu sealed secret"
Restore Procedures#
Restore PostgreSQL Database#
Full Database Restore#
# Stop Mailu pods to prevent write conflicts
kubectl scale deployment -n mailu mailu-admin --replicas=0
kubectl scale deployment -n mailu mailu-front --replicas=0
# Drop and recreate database (CAUTION: deletes current data)
kubectl exec -n postgres postgres-1 -- psql -U postgres -c "DROP DATABASE mailu;"
kubectl exec -n postgres postgres-1 -- psql -U postgres -c "CREATE DATABASE mailu OWNER mailu;"
# Restore from backup
gunzip < mailu-db-backup-20250110.sql.gz | \
kubectl exec -i -n postgres postgres-1 -- psql -U postgres -d mailu
# Restart Mailu pods
kubectl scale deployment -n mailu mailu-admin --replicas=1
kubectl scale deployment -n mailu mailu-front --replicas=1
CloudNativePG Point-in-Time Recovery#
Restore CNPG cluster to specific timestamp:
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: postgres-restored
namespace: postgres
spec:
instances: 3
bootstrap:
recovery:
source: postgres-backup
recoveryTarget:
targetTime: "2025-01-10 14:00:00" # Restore to this timestamp
externalClusters:
- name: postgres-backup
barmanObjectStore:
destinationPath: s3://backup-bucket/postgres-mailu/
s3Credentials:
accessKeyId:
name: s3-credentials
key: access-key
secretAccessKey:
name: s3-credentials
key: secret-key
Restore PersistentVolumes#
Longhorn Volume Restore#
Restore from Longhorn snapshot:
# Method 1: Restore from snapshot (creates new PVC)
kubectl create -f - <<EOF
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mailu-dovecot-restored
namespace: mailu
spec:
dataSource:
name: dovecot-snapshot-20250110
kind: VolumeSnapshot
apiGroup: longhorn.io
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 200Gi
storageClassName: longhorn
EOF
# Method 2: Restore from backup (Longhorn UI)
# 1. Go to Longhorn UI → Backup
# 2. Find backup: dovecot-backup-20250110
# 3. Click "Restore" → creates new volume
# 4. Create PVC pointing to restored volume
Update deployment to use restored PVC:
# Edit deployment to reference new PVC
kubectl edit deployment -n mailu mailu-dovecot
# Change:
# volumes:
# - name: mail
# persistentVolumeClaim:
# claimName: mailu-dovecot-restored # Updated
Velero Namespace Restore#
Restore entire Mailu namespace from Velero backup:
# List available backups
velero backup get
# Restore from specific backup
velero restore create mailu-restore-$(date +%Y%m%d) \
--from-backup mailu-backup-20250110
# Monitor restore progress
velero restore describe mailu-restore-YYYYMMDD
kubectl get pods -n mailu -w
Restore Kubernetes Secrets#
# Decrypt secret backup
gpg --decrypt mailu-secrets-backup-20250110.yaml.gpg > secrets.yaml
# Apply secrets
kubectl apply -f secrets.yaml
# Clean up decrypted file
rm secrets.yaml
Disaster Recovery Scenarios#
Scenario 1: Accidental Email Deletion#
Symptoms: User reports missing emails.
Recovery:
Restore Dovecot PVC from most recent snapshot (before deletion)
Mount restored volume to temporary pod
Extract deleted user’s mailbox
Copy mailbox to production Dovecot pod
# Create recovery pod with restored PVC
kubectl run -n mailu dovecot-recovery \
--image=alpine --command -- sleep infinity
kubectl set volumes pod/dovecot-recovery -n mailu \
--add --name=mail-restored \
--claim-name=mailu-dovecot-restored
# Copy mailbox
kubectl exec -n mailu dovecot-recovery -- \
tar czf /tmp/user-mailbox.tar.gz /mail/user@example.com
kubectl cp -n mailu dovecot-recovery:/tmp/user-mailbox.tar.gz ./user-mailbox.tar.gz
# Extract to production pod
kubectl cp -n mailu ./user-mailbox.tar.gz mailu-dovecot-xxx:/tmp/
kubectl exec -n mailu mailu-dovecot-xxx -- \
tar xzf /tmp/user-mailbox.tar.gz -C /
Scenario 2: Database Corruption#
Symptoms: Admin UI errors, authentication failures.
Recovery:
Stop all Mailu pods
Restore database from most recent backup
Restart Mailu pods
Verify user can login
(See “Restore PostgreSQL Database” section above)
Scenario 3: Complete Cluster Failure#
Symptoms: Entire cluster lost (hardware failure, cloud provider issue).
Recovery (assumes Velero backups to external S3):
Build new Kubernetes cluster
Install Velero
Restore Mailu namespace from backup
Update DNS to point to new cluster
Verify email flow
# On new cluster
velero install --provider aws --bucket mailu-backups ...
velero restore create mailu-disaster-recovery \
--from-backup mailu-backup-20250110
kubectl get pods -n mailu -w
Backup Verification#
Critical: Test backups regularly to ensure they work!
Monthly Backup Test Procedure#
Restore to test environment:
# Create test namespace
kubectl create namespace mailu-test
# Restore from production backup
velero restore create mailu-test-restore \
--from-backup mailu-backup-latest \
--namespace-mappings mailu:mailu-test
Verify data integrity:
# Check database
kubectl exec -n mailu-test mailu-admin-xxx -- \
python3 -c "from mailu import db; db.init_app(); print('DB OK')"
# Check mailbox
kubectl exec -n mailu-test mailu-dovecot-xxx -- \
ls -lh /mail/
Test functionality:
Login to admin UI
Login to webmail
Send test email
Receive test email
Document results:
echo "Backup test $(date): PASSED" >> backup-test-log.txt
Clean up:
kubectl delete namespace mailu-test
Backup Storage Recommendations#
3-2-1 Backup Rule#
Follow the 3-2-1 backup strategy:
3 copies of data (production + 2 backups)
2 different media types (local PVC + S3)
1 off-site copy (different datacenter/region)
Example Backup Architecture#
Production Mailu
↓
┌─────────────────────────────────────┐
│ Primary Backups (Same Cluster) │
│ - Longhorn snapshots (7 days) │
│ - Database dumps on PVC │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ Secondary Backups (S3, Same Region)│
│ - Longhorn backups to S3 │
│ - CNPG WAL archives │
│ - Velero backups (30 days) │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ Tertiary Backups (S3, Other Region)│
│ - Cross-region S3 replication │
│ - Long-term archives (1 year) │
└─────────────────────────────────────┘
Retention Policies#
Backup Type |
Frequency |
Retention |
Storage |
|---|---|---|---|
Longhorn snapshots |
Daily |
7 days |
Local (Longhorn) |
Database dumps |
Daily |
30 days |
PVC + S3 |
Longhorn backups |
Weekly |
90 days |
S3 (same region) |
Velero backups |
Daily |
30 days |
S3 (same region) |
Long-term archives |
Monthly |
1 year |
S3 (cross-region) |
Backup Monitoring#
Set up alerts for backup failures:
apiVersion: v1
kind: ConfigMap
metadata:
name: backup-monitoring
namespace: monitoring
data:
alerts.yaml: |
groups:
- name: mailu-backups
rules:
- alert: MailuBackupFailed
expr: kube_job_status_failed{namespace="mailu", job_name=~"mailu-db-backup-.*"} > 0
for: 10m
annotations:
summary: "Mailu database backup failed"
description: "Backup job {{ $labels.job_name }} failed"
- alert: MailuNoRecentBackup
expr: time() - max(kube_job_completion_time{namespace="mailu", job_name=~"mailu-db-backup-.*"}) > 86400
annotations:
summary: "No Mailu backup in 24 hours"
See Also#
Upgrade Mailu - Always backup before upgrading
Component Specifications - Storage requirements
Longhorn Documentation - Volume snapshots and backups
Velero Documentation - Cluster-level backups
CloudNativePG Documentation - PostgreSQL backups