Skip to content

Backups

Backup and recovery procedures for the brennan.page homelab.

Overview

Regular backups are essential for data protection and disaster recovery. This section covers backup strategies, procedures, and recovery processes.

Backup Strategy

Backup Types

  • Full Backups: Complete system backup
  • Incremental Backups: Changes since last backup
  • Differential Backups: Changes since last full backup
  • Snapshot Backups: Point-in-time snapshots

Backup Schedule

  • Daily: Database backups
  • Weekly: Configuration backups
  • Monthly: Full system backups
  • On-demand: Before major changes

Backup Locations

  • Local: Local storage for quick recovery
  • Remote: Offsite storage for disaster recovery
  • Cloud: Cloud storage for long-term retention

Database Backups

PostgreSQL Backups

# Full database backup
docker exec postgres pg_dump -U homelab homelab > /opt/homelab/backups/postgres-$(date +%Y%m%d).sql

# Individual database backup
docker exec postgres pg_dump -U homelab vikunja > /opt/homelab/backups/vikunja-$(date +%Y%m%d).sql
docker exec postgres pg_dump -U homelab hedgedoc > /opt/homelab/backups/hedgedoc-$(date +%Y%m%d).sql
docker exec postgres pg_dump -U homelab linkding > /opt/homelab/backups/linkding-$(date +%Y%m%d).sql
docker exec postgres pg_dump -U homelab navidrome > /opt/homelab/backups/navidrome-$(date +%Y%m%d).sql

# Compressed backup
docker exec postgres pg_dump -U homelab homelab | gzip > /opt/homelab/backups/postgres-$(date +%Y%m%d).sql.gz

# All databases
for db in $(docker exec postgres psql -U homelab -t -c "SELECT datname FROM pg_database WHERE datname NOT IN ('template0', 'template1', 'postgres')"); do
    docker exec postgres pg_dump -U homelab "$db" > "/opt/homelab/backups/${db}-$(date +%Y%m%d).sql"
done

MariaDB Backups

# Full backup
docker exec flarum_mariadb mysqldump -u root -prootpassword123 --all-databases > /opt/homelab/backups/mariadb-$(date +%Y%m%d).sql

# Individual database
docker exec flarum_mariadb mysqldump -u root -prootpassword123 flarum > /opt/homelab/backups/flarum-$(date +%Y%m%d).sql

# Compressed backup
docker exec flarum_mariadb mysqldump -u root -prootpassword123 --all-databases | gzip > /opt/homelab/backups/mariadb-$(date +%Y%m%d).sql.gz

Service Data Backups

Volume Backups

# Backup service volumes
docker run --rm -v service_volume:/data -v /opt/homelab/backups:/backup alpine tar czf /backup/service-$(date +%Y%m%d).tar.gz -C /data .

# Backup all service volumes
for volume in $(docker volume ls --format "{{.Name}}" | grep service); do
    docker run --rm -v "$volume":/data -v /opt/homelab/backups:/backup alpine tar czf "/backup/${volume}-$(date +%Y%m%d).tar.gz" -C /data .
done

Configuration Backups

# Backup Docker Compose files
find /opt/homelab/services -name "docker-compose.yml" -exec cp {} /opt/homelab/backups/configs/ \;

# Backup Caddyfile
cp /opt/homelab/caddy/Caddyfile /opt/homelab/backups/configs/Caddyfile-$(date +%Y%m%d)

# Backup environment files
find /opt/homelab -name "*.env" -exec cp {} /opt/homelab/backups/configs/ \;

Backup Automation

Backup Script

#!/bin/bash
# backup.sh

BACKUP_DIR="/opt/homelab/backups"
DATE=$(date +%Y%m%d)
RETENTION_DAYS=30

# Create backup directory
mkdir -p "$BACKUP_DIR/databases"
mkdir -p "$BACKUP_DIR/volumes"
mkdir -p "$BACKUP_DIR/configs"

# Database backups
echo "Backing up databases..."
docker exec postgres pg_dump -U homelab homelab | gzip > "$BACKUP_DIR/databases/postgres-$DATE.sql.gz"

for db in $(docker exec postgres psql -U homelab -t -c "SELECT datname FROM pg_database WHERE datname NOT IN ('template0', 'template1', 'postgres')"); do
    docker exec postgres pg_dump -U homelab "$db" | gzip > "$BACKUP_DIR/databases/${db}-$DATE.sql.gz"
done

# MariaDB backup
if docker ps | grep -q mariadb; then
    docker exec flarum_mariadb mysqldump -u root -prootpassword123 --all-databases | gzip > "$BACKUP_DIR/databases/mariadb-$DATE.sql.gz"
fi

# Volume backups
echo "Backing up volumes..."
for volume in $(docker volume ls --format "{{.Name}}" | grep -E "data|db"); do
    docker run --rm -v "$volume":/data -v "$BACKUP_DIR/volumes":/backup alpine tar czf "/backup/${volume}-$DATE.tar.gz" -C /data .
done

# Configuration backups
echo "Backing up configurations..."
find /opt/homelab/services -name "docker-compose.yml" -exec cp {} "$BACKUP_DIR/configs/" \;
cp /opt/homelab/caddy/Caddyfile "$BACKUP_DIR/configs/Caddyfile-$DATE"

# Clean up old backups
echo "Cleaning up old backups..."
find "$BACKUP_DIR" -name "*$(date -d "$RETENTION_DAYS days ago" +%Y%m%d)*" -delete

echo "Backup completed: $DATE"

Cron Job

# Add to crontab
crontab -e

# Daily backup at 2 AM
0 2 * * * /opt/homelab/scripts/backup.sh >> /opt/homelab/logs/backup.log 2>&1

# Weekly full backup on Sunday at 3 AM
0 3 * * 0 /opt/homelab/scripts/full-backup.sh >> /opt/homelab/logs/backup.log 2>&1

Recovery Procedures

Database Recovery

# Stop services
docker compose down

# Restore PostgreSQL database
docker exec postgres psql -U homelab -d postgres -c "DROP DATABASE IF EXISTS database_name;"
docker exec postgres psql -U homelab -d postgres -c "CREATE DATABASE database_name;"
docker exec postgres psql -U homelab -d database_name < /opt/homelab/backups/database_name-20260116.sql

# Restore from compressed backup
gunzip -c /opt/homelab/backups/database_name-20260116.sql.gz | docker exec -i postgres psql -U homelab -d database_name

# Restore MariaDB database
docker exec flarum_mariadb mysql -u root -prootpassword123 < /opt/homelab/backups/mariadb-20260116.sql

Volume Recovery

# Stop service
docker stop service_name

# Remove current volume
docker volume rm service_volume

# Create new volume
docker volume create service_volume

# Restore from backup
docker run --rm -v service_volume:/data -v /opt/homelab/backups:/backup alpine tar xzf /backup/service_volume-20260116.tar.gz -C /data

# Start service
docker start service_name

Configuration Recovery

# Restore Docker Compose files
cp /opt/homelab/backups/configs/docker-compose.yml /opt/homelab/services/service_name/

# Restore Caddyfile
cp /opt/homelab/backups/configs/Caddyfile-20260116 /opt/homelab/caddy/Caddyfile

# Restart services
docker compose down
docker compose up -d

Disaster Recovery

Complete System Recovery

# 1. System preparation
# Install Docker and Docker Compose
# Create necessary directories
mkdir -p /opt/homelab/{services,backups,logs}

# 2. Restore configurations
cp /opt/homelab/backups/configs/* /opt/homelab/services/

# 3. Restore databases
# Start PostgreSQL
docker compose up -d postgres

# Restore databases
for backup in /opt/homelab/backups/databases/*.sql.gz; do
    db_name=$(basename "$backup" .sql.gz | sed 's/-[0-9]*$//')
    gunzip -c "$backup" | docker exec -i postgres psql -U homelab -d "$db_name"
done

# 4. Restore volumes
for backup in /opt/homelab/backups/volumes/*.tar.gz; do
    volume_name=$(basename "$backup" .tar.gz | sed 's/-[0-9]*$//')
    docker volume create "$volume_name"
    docker run --rm -v "$volume_name":/data -v /opt/homelab/backups:/backup alpine tar xzf "/backup/$(basename "$backup")" -C /data
done

# 5. Start all services
docker compose up -d

# 6. Verify recovery
docker ps
curl -I https://brennan.page

Service-Specific Recovery

# WriteFreely recovery
docker stop writefreely
rm -rf /opt/homelab/services/writefreely/data
docker cp -r /opt/homelab/backups/writefreely-20260116 /opt/homelab/services/writefreely/data
docker start writefreely

# Flarum recovery
docker stop flarum
docker volume rm flarum_flarum_db_data
docker compose up -d
docker exec flarum_mariadb mysql -u root -prootpassword123 flarum < /opt/homelab/backups/flarum-20260116.sql

# FreshRSS recovery
docker stop freshrss
rm -rf /opt/homelab/services/freshrss/data
docker cp -r /opt/homelab/backups/freshrss-20260116 /opt/homelab/services/freshrss/data
docker start freshrss

Backup Verification

Integrity Checks

# Verify backup files
for backup in /opt/homelab/backups/databases/*.sql.gz; do
    echo "Checking $backup..."
    gunzip -t "$backup" && echo "OK" || echo "CORRUPTED"
done

# Test restore
docker exec postgres psql -U homelab -d test_db < /opt/homelab/backups/database_name-20260116.sql

Restoration Tests

# Monthly restoration test
# Create test environment
docker compose -f docker-compose.test.yml up -d

# Test database restore
docker exec postgres psql -U homelab -d test_db < /opt/homelab/backups/database_name-20260116.sql

# Verify data integrity
docker exec postgres psql -U homelab -d test_db -c "SELECT COUNT(*) FROM users;"

# Clean up test environment
docker compose -f docker-compose.test.yml down

Monitoring and Alerts

Backup Monitoring

# Check backup status
ls -la /opt/homelab/backups/databases/ | grep "$(date +%Y%m%d)"

# Check backup size
du -sh /opt/homelab/backups/

# Check backup logs
tail -20 /opt/homelab/logs/backup.log

Alert Configuration

# Backup failure alert
if [ ! -f "/opt/homelab/backups/databases/postgres-$(date +%Y%m%d).sql.gz" ]; then
    echo "Backup failed for $(date +%Y%m%d)" | mail -s "Backup Alert" admin@brennan.page
fi

# Disk space alert
if [ $(df /opt/homelab/backups | awk 'NR==2 {print $5}' | sed 's/%//') -gt 80 ]; then
    echo "Backup disk usage: $(df /opt/homelab/backups | awk 'NR==2 {print $5}')" | mail -s "Disk Space Alert" admin@brennan.page
fi

Best Practices

Backup Strategy

  • 3-2-1 Rule: 3 copies, 2 different media, 1 offsite
  • Regular Testing: Test restores monthly
  • Encryption: Encrypt sensitive backups
  • Documentation: Document backup procedures

Security

  • Access Control: Limit backup access
  • Encryption: Encrypt backup files
  • Secure Storage: Use secure storage locations
  • Audit Logs: Maintain backup audit logs

Performance

  • Compression: Compress backup files
  • Deduplication: Remove duplicate data
  • Scheduling: Schedule backups during low usage
  • Monitoring: Monitor backup performance

Getting Help

Before Reporting Issues

  • Checked backup logs
  • Verified backup files exist
  • Tested backup integrity
  • Checked available storage

Information to Include

  • Backup script output
  • Error messages
  • System status
  • Recent changes
  • Storage usage