Backup and Restore Operations Guide¶
Overview¶
This guide covers comprehensive backup and restoration procedures for all services in the infrastructure, including automated backup strategies, disaster recovery planning, and point-in-time recovery.
Current Backup Infrastructure¶
Existing Scripts¶
- Supabase Backup (
/srv/dockerdata/_scripts/supabase-backup.sh) - Performs logical dumps using
pg_dumpall - Backs up configuration files and bootstrap SQL
- 14-day retention policy
-
Target:
/srv/backups/supabase/ -
Pocketbase Backup (
/srv/dockerdata/_scripts/pocketbase-backup.sh) - Stops container for SQLite consistency
- Archives all PocketBase data directories
- 14-day retention policy
-
Target:
/srv/backups/pocketbase/ -
Gitea Backup (
/srv/dockerdata/_scripts/gitea-backup.sh) - Uses Gitea's built-in dump command
- Backs up repositories, LFS data, database, and configuration
- 14-day retention policy
- Target:
/srv/backups/gitea/ - Automated via systemd timer (2 AM daily)
Service-Specific Backup Procedures¶
Supabase PostgreSQL¶
Manual Backup¶
# Run existing backup script
/srv/dockerdata/_scripts/supabase-backup.sh
# Alternative: Direct pg_dump for specific database
docker exec supa-db pg_dump -U postgres -d postgres > backup-$(date +%Y%m%d%H%M%S).sql
# Physical backup using pg_basebackup
docker exec supa-db pg_basebackup -U postgres -D /tmp/backup -Ft -z -P
docker cp supa-db:/tmp/backup ./physical-backup-$(date +%Y%m%d%H%M%S)
Restoration Process¶
# Stop all Supabase services except database
cd /srv/dockerdata/supabase
docker compose stop supabase-auth supabase-rest supabase-realtime supabase-storage supabase-meta supabase-studio
# Restore from logical backup
gunzip -c /srv/backups/supabase/db/pgdumpall-TIMESTAMP.sql.gz | \
docker exec -i supa-db psql -U postgres
# Verify restoration
docker exec supa-db psql -U postgres -c "\l"
docker exec supa-db psql -U postgres -c "\dt+ public.*"
# Restart services
docker compose up -d
Traefik¶
Backup¶
# Manual backup
cd /srv/dockerdata
tar -czf /srv/backups/traefik-$(date +%Y%m%d%H%M%S).tgz \
traefik/docker-compose.yml \
traefik/traefik.yml \
traefik/dynamic/ \
traefik/.env
# Backup certificates (handle with care)
sudo tar -czf /srv/backups/traefik-certs-$(date +%Y%m%d%H%M%S).tgz \
traefik/letsencrypt/acme.json
Restoration¶
# Stop Traefik
cd /srv/dockerdata/traefik
docker compose down
# Restore configuration
tar -xzf /srv/backups/traefik-TIMESTAMP.tgz -C /srv/dockerdata
# Restore certificates (if needed)
tar -xzf /srv/backups/traefik-certs-TIMESTAMP.tgz -C /srv/dockerdata
chmod 600 /srv/dockerdata/traefik/letsencrypt/acme.json
# Start Traefik
docker compose up -d
Gitea¶
Backup¶
# Automated backup (recommended)
/srv/dockerdata/_scripts/gitea-backup.sh
# Manual backup using Gitea's built-in command
docker exec -u git gitea gitea dump -c /data/gitea/conf/app.ini \
-w /tmp -f /tmp/gitea-dump-$(date +%Y%m%d%H%M%S).zip
# Extract dump from container
docker cp gitea:/tmp/gitea-dump-*.zip /srv/backups/gitea/app/
# Verify backup integrity
unzip -t /srv/backups/gitea/app/gitea-dump-*.zip
Restoration¶
# Stop Gitea service
cd /srv/dockerdata/gitea
docker compose down
# Extract the backup
cd /tmp
unzip /srv/backups/gitea/app/gitea-dump-TIMESTAMP.zip
# Restore database (SQLite)
docker run --rm -v gitea_gitea_data:/data -v /tmp:/backup \
alpine sh -c "rm -f /data/gitea/gitea.db && cp /backup/gitea-db.sql /data/gitea/"
# Restore repositories
docker run --rm -v gitea_gitea_data:/data -v /tmp:/backup \
alpine sh -c "rm -rf /data/git && tar -xzf /backup/gitea-repo.zip -C /data/"
# Restore configuration
docker run --rm -v gitea_gitea_data:/data -v /tmp:/backup \
alpine sh -c "cp /backup/app.ini /data/gitea/conf/"
# Restore LFS data if exists
if [[ -f /tmp/data-lfs.zip ]]; then
docker run --rm -v gitea_gitea_data:/data -v /tmp:/backup \
alpine sh -c "rm -rf /data/git/lfs && unzip /backup/data-lfs.zip -d /data/git/"
fi
# Fix permissions
docker run --rm -v gitea_gitea_data:/data \
alpine chown -R 1000:1000 /data
# Start Gitea
docker compose up -d
# Verify restoration
docker exec gitea gitea admin user list
Paperless-AI¶
Backup¶
# Automated backup via infrastructure script (recommended)
/srv/dockerdata/rclone/backup-infrastructure.sh
# Manual backup of Docker volume
docker run --rm -v paperless-ai_data:/source -v /srv/backups:/backup \
alpine tar -czf /backup/paperless-ai-data-$(date +%Y%m%d%H%M%S).tar.gz -C /source .
# Backup configuration files
cd /srv/dockerdata
tar -czf /srv/backups/paperless-ai-config-$(date +%Y%m%d%H%M%S).tgz \
paperless-ai/docker-compose.yml \
paperless-ai/.env
Restoration¶
# Stop Paperless-AI service
cd /srv/dockerdata/paperless-ai
docker compose down
# Restore configuration
tar -xzf /srv/backups/paperless-ai-config-TIMESTAMP.tgz -C /srv/dockerdata
# Restore data volume
docker run --rm -v paperless-ai_data:/dest -v /srv/backups:/backup \
alpine tar -xzf /backup/paperless-ai-data-TIMESTAMP.tar.gz -C /dest
# Start service
docker compose up -d
# Verify restoration
docker logs paperless-ai
Watchtower¶
Backup¶
# Backup configuration only (no persistent data)
cd /srv/dockerdata
tar -czf /srv/backups/watchtower-$(date +%Y%m%d%H%M%S).tgz \
watchtower/docker-compose.yml \
watchtower/config.json \
watchtower/.env
Restoration¶
# Restore configuration
tar -xzf /srv/backups/watchtower-TIMESTAMP.tgz -C /srv/dockerdata
# Start Watchtower
cd /srv/dockerdata/watchtower
docker compose up -d
# Verify operation
docker logs watchtower
Metube¶
Backup¶
# Note: Downloads directory can be large - backup selectively
# Backup configuration only
cd /srv/dockerdata
tar -czf /srv/backups/metube-config-$(date +%Y%m%d%H%M%S).tgz \
metube/docker-compose.yml \
metube/config/ \
metube/.env
# Manual backup of important downloads (optional)
# WARNING: This can be very large
rsync -av --progress /srv/dockerdata/metube/downloads/ /srv/backups/metube-downloads/
Restoration¶
# Stop Metube service
cd /srv/dockerdata/metube
docker compose down
# Restore configuration
tar -xzf /srv/backups/metube-config-TIMESTAMP.tgz -C /srv/dockerdata
# Restore downloads (if backed up)
rsync -av --progress /srv/backups/metube-downloads/ /srv/dockerdata/metube/downloads/
# Start service
docker compose up -d
# Verify access
curl -I https://metube.rbnk.uk
Generic Docker Volume Backup¶
# Backup any Docker volume
VOLUME_NAME="volume_name"
docker run --rm -v $VOLUME_NAME:/data -v /srv/backups:/backup \
alpine tar -czf /backup/${VOLUME_NAME}-$(date +%Y%m%d%H%M%S).tgz -C /data .
# Restore Docker volume
docker run --rm -v $VOLUME_NAME:/data -v /srv/backups:/backup \
alpine tar -xzf /backup/${VOLUME_NAME}-TIMESTAMP.tgz -C /data
Automated Backup Setup¶
Existing Automated Backups¶
Gitea has its own systemd timer already configured:
- gitea-backup.timer: Runs daily at 2 AM
- gitea-backup.service: Executes /srv/dockerdata/_scripts/gitea-backup.sh
- Installation: /srv/dockerdata/gitea/systemd/install-systemd-services.sh
Create Systemd Service and Timer¶
# Create backup service
sudo tee /etc/systemd/system/dockerdata-backup.service > /dev/null << 'EOF'
[Unit]
Description=Docker Data Backup Service
After=docker.service
Requires=docker.service
[Service]
Type=oneshot
ExecStart=/srv/dockerdata/_scripts/run-all-backups.sh
StandardOutput=journal
StandardError=journal
SyslogIdentifier=dockerdata-backup
[Install]
WantedBy=multi-user.target
EOF
# Create timer
sudo tee /etc/systemd/system/dockerdata-backup.timer > /dev/null << 'EOF'
[Unit]
Description=Run Docker Data Backup at 3 AM daily
Requires=dockerdata-backup.service
[Timer]
OnCalendar=daily
OnCalendar=*-*-* 03:00:00
Persistent=true
[Install]
WantedBy=timers.target
EOF
# Create master backup script
sudo tee /srv/dockerdata/_scripts/run-all-backups.sh > /dev/null << 'EOF'
#!/bin/bash
set -euo pipefail
LOGFILE="/var/log/dockerdata-backup.log"
exec 1> >(tee -a "$LOGFILE")
exec 2>&1
echo "=== Starting backup run at $(date) ==="
# Send notification that backup is starting
notify "Backup Started" "Daily backup job has started" 2 backup-info-daily
# Track failures
FAILED_SCRIPTS=""
# Run all backup scripts
for script in /srv/dockerdata/_scripts/*-backup.sh; do
if [[ -x "$script" ]]; then
echo "Running: $script"
if ! "$script"; then
echo "WARNING: $script failed with exit code $?"
FAILED_SCRIPTS="${FAILED_SCRIPTS}$(basename $script), "
fi
fi
done
# Send completion notification
if [ -z "$FAILED_SCRIPTS" ]; then
notify "Backup Success" "All backup jobs completed successfully" 3 backup-info-daily
else
notify "Backup Failed" "Failed scripts: ${FAILED_SCRIPTS%%, }" 5 backup-error-daily
fi
echo "=== Backup run completed at $(date) ==="
EOF
sudo chmod +x /srv/dockerdata/_scripts/run-all-backups.sh
# Enable and start timer
sudo systemctl daemon-reload
sudo systemctl enable dockerdata-backup.timer
sudo systemctl start dockerdata-backup.timer
# Check timer status
sudo systemctl list-timers dockerdata-backup.timer
Backup Notifications¶
The backup system integrates with the notification stack to provide real-time status updates:
Notification Events¶
- Backup Start: Notifies when backup jobs begin
- Backup Success: Confirms successful completion
- Backup Failure: Alerts on any failures with specific script names
- Storage Warnings: Alerts when backup storage exceeds thresholds
Configure Backup Notifications¶
# Notifications are sent via the notify command
# Format: notify "title" "message" priority topic
# Topics used:
# - backup-info-daily: Regular status updates
# - backup-error-daily: Failure alerts
# - backup-warning-storage: Storage warnings
# To receive notifications, configure your preferred channels in:
# - Apprise: https://apprise.rbnk.uk
# - ntfy: Subscribe to topics via mobile app or web
Backup Verification¶
Automated Verification Script¶
# Create verification script
sudo tee /srv/dockerdata/_scripts/verify-backups.sh > /dev/null << 'EOF'
#!/bin/bash
set -euo pipefail
BACKUP_ROOT="/srv/backups"
VERIFY_DIR="/tmp/backup-verify-$$"
ERRORS=0
# Function to verify tar archives
verify_tar() {
local file=$1
echo "Verifying tar archive: $file"
if tar -tzf "$file" > /dev/null 2>&1; then
echo "✓ Archive is valid"
return 0
else
echo "✗ Archive is corrupted!"
return 1
fi
}
# Function to verify SQL dumps
verify_sql() {
local file=$1
local tempfile="${file%.gz}"
echo "Verifying SQL dump: $file"
# Decompress and check syntax
if gunzip -c "$file" > "$tempfile" 2>/dev/null; then
# Basic SQL syntax check
if head -n 100 "$tempfile" | grep -q "PostgreSQL database dump"; then
echo "✓ SQL dump appears valid"
rm -f "$tempfile"
return 0
else
echo "✗ SQL dump format invalid!"
rm -f "$tempfile"
return 1
fi
else
echo "✗ Failed to decompress SQL dump!"
return 1
fi
}
# Check Supabase backups
echo "=== Verifying Supabase Backups ==="
for backup in $(find $BACKUP_ROOT/supabase -name "*.sql.gz" -mtime -1); do
verify_sql "$backup" || ((ERRORS++))
done
for backup in $(find $BACKUP_ROOT/supabase -name "*.tgz" -mtime -1); do
verify_tar "$backup" || ((ERRORS++))
done
# Check other service backups
echo "=== Verifying Other Service Backups ==="
for backup in $(find $BACKUP_ROOT -name "*.tgz" -mtime -1 | grep -v supabase); do
verify_tar "$backup" || ((ERRORS++))
done
# Cleanup
rm -rf "$VERIFY_DIR"
# Report
if [[ $ERRORS -eq 0 ]]; then
echo "✓ All backups verified successfully"
exit 0
else
echo "✗ Found $ERRORS backup errors!"
exit 1
fi
EOF
sudo chmod +x /srv/dockerdata/_scripts/verify-backups.sh
Disaster Recovery Plan¶
Full System Recovery Procedure¶
-
Prepare New System
-
Restore Configuration
-
Restore Services in Order
# 1. Restore Traefik first (for networking) cd /srv/dockerdata/traefik docker compose up -d # 2. Restore Supabase database cd /srv/dockerdata/supabase docker compose up -d supa-db # Wait for database to be ready sleep 30 # 3. Restore database content gunzip -c /srv/backups/supabase/db/latest.sql.gz | \ docker exec -i supa-db psql -U postgres # 4. Start remaining Supabase services docker compose up -d # 5. Restore other services in priority order # Priority 1: Essential services for service in gitea monitoring; do if [[ -f "/srv/dockerdata/$service/docker-compose.yml" ]]; then cd "/srv/dockerdata/$service" docker compose up -d sleep 10 # Wait for service to start fi done # Priority 2: Application services for service in paperless-ai n8n open-webui litellm; do if [[ -f "/srv/dockerdata/$service/docker-compose.yml" ]]; then cd "/srv/dockerdata/$service" docker compose up -d sleep 5 fi done # Priority 3: Utility services for service in watchtower metube glance rclone; do if [[ -f "/srv/dockerdata/$service/docker-compose.yml" ]]; then cd "/srv/dockerdata/$service" docker compose up -d fi done
Point-in-Time Recovery (PostgreSQL)¶
-
Enable WAL Archiving
# Add to PostgreSQL configuration docker exec supa-db bash -c "cat >> /var/lib/postgresql/data/postgresql.conf << EOF wal_level = replica archive_mode = on archive_command = 'test ! -f /archive/%f && cp %p /archive/%f' EOF" # Create archive directory docker exec supa-db mkdir -p /archive docker exec supa-db chown postgres:postgres /archive # Restart PostgreSQL docker compose restart supa-db -
Perform PITR
# Stop PostgreSQL docker compose stop supa-db # Restore base backup docker run --rm -v supabase_db_data:/data -v /srv/backups:/backup \ alpine tar -xzf /backup/pg-base-backup.tgz -C /data # Copy WAL files docker run --rm -v supabase_db_data:/data -v /srv/backups/wal:/wal \ alpine cp /wal/* /data/pg_wal/ # Create recovery.conf docker run --rm -v supabase_db_data:/data alpine sh -c "cat > /data/recovery.conf << EOF restore_command = 'cp /archive/%f %p' recovery_target_time = '2024-01-18 12:00:00' EOF" # Start PostgreSQL docker compose start supa-db
Offsite Backup Strategy¶
Rclone Containerized Service¶
Rclone is deployed as a containerized service providing automated cloud backups with web GUI management:
- Web Interface: https://rclone.rbnk.uk
- Configuration:
/srv/dockerdata/rclone/config/rclone.conf - Data Directory:
/srv/dockerdata/rclone/data/ - Backup Source: Read-only mount of
/srv/dockerdata
See Rclone Service Documentation for detailed configuration and operations.
Legacy Rclone Setup (Host-based)¶
For systems still using host-based rclone:
# Install rclone
curl https://rclone.org/install.sh | sudo bash
# Configure remote (interactive)
rclone config
# Create sync script
sudo tee /srv/dockerdata/_scripts/offsite-sync.sh > /dev/null << 'EOF'
#!/bin/bash
set -euo pipefail
RCLONE_REMOTE="backblaze:dockerdata-backups"
LOCAL_BACKUP="/srv/backups"
echo "Starting offsite sync at $(date)"
# Sync with bandwidth limit
rclone sync "$LOCAL_BACKUP" "$RCLONE_REMOTE" \
--bwlimit 10M \
--transfers 4 \
--checkers 8 \
--log-level INFO \
--log-file /var/log/rclone-backup.log
# Verify remote
rclone size "$RCLONE_REMOTE"
echo "Offsite sync completed at $(date)"
EOF
sudo chmod +x /srv/dockerdata/_scripts/offsite-sync.sh
# Add to cron (run at 5 AM)
echo "0 5 * * * /srv/dockerdata/_scripts/offsite-sync.sh" | sudo crontab -
Backup Monitoring¶
# Create monitoring script
sudo tee /srv/dockerdata/_scripts/backup-monitor.sh > /dev/null << 'EOF'
#!/bin/bash
WEBHOOK_URL="https://discord.com/api/webhooks/YOUR_WEBHOOK_HERE"
BACKUP_DIR="/srv/backups"
MAX_AGE_HOURS=26
send_alert() {
local message=$1
curl -H "Content-Type: application/json" \
-d "{\"content\": \"🚨 Backup Alert: $message\"}" \
"$WEBHOOK_URL"
}
# Check for recent backups
for service in supabase traefik pocketbase gitea; do
latest=$(find "$BACKUP_DIR/$service" -type f -name "*.tgz" -o -name "*.sql.gz" | \
xargs ls -t | head -1)
if [[ -z "$latest" ]]; then
send_alert "No backups found for $service!"
continue
fi
age_hours=$(( ($(date +%s) - $(stat -c %Y "$latest")) / 3600 ))
if [[ $age_hours -gt $MAX_AGE_HOURS ]]; then
send_alert "Backup for $service is $age_hours hours old!"
fi
done
# Check backup disk space
usage=$(df -h "$BACKUP_DIR" | awk 'NR==2 {print $5}' | sed 's/%//')
if [[ $usage -gt 80 ]]; then
send_alert "Backup disk usage is at ${usage}%!"
fi
EOF
sudo chmod +x /srv/dockerdata/_scripts/backup-monitor.sh
# Add to cron (check every 6 hours)
echo "0 */6 * * * /srv/dockerdata/_scripts/backup-monitor.sh" | sudo crontab -
Testing and Maintenance¶
Quarterly Restoration Test¶
- Create test environment
- Restore from backup
- Verify functionality
- Document any issues
- Update procedures as needed
Backup Maintenance Checklist¶
- [ ] Verify all services have recent backups
- [ ] Check backup sizes for anomalies
- [ ] Test restoration on non-production system
- [ ] Review and update retention policies
- [ ] Verify offsite sync is working
- [ ] Check available disk space
- [ ] Review backup logs for errors
- [ ] Update documentation with any changes
Emergency Contacts and Resources¶
- Backup storage locations:
/srv/backups/ - Offsite backups: Configured in rclone
- Recovery documentation: This file
- System architecture:
/srv/dockerdata/docs/architecture/ - Service credentials: Stored in
.envfiles (check permissions!)