diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b0ae784 --- /dev/null +++ b/.gitignore @@ -0,0 +1,56 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual environments +.venv/ +venv/ +ENV/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Environment variables +.env +.env.local +.env.*.local + +# Testing +.coverage +htmlcov/ +.pytest_cache/ +.mypy_cache/ + +# Database backups (should not be in repo) +*.dump +*.dump.gz +/backups/ + +# Logs +*.log + +# OS +.DS_Store +Thumbs.db diff --git a/scripts/backup-postgres.sh b/scripts/backup-postgres.sh new file mode 100755 index 0000000..b01c097 --- /dev/null +++ b/scripts/backup-postgres.sh @@ -0,0 +1,83 @@ +#!/bin/bash +# PostgreSQL backup script for Debate platform +# Runs daily, keeps 30 days of backups +# Verifies backup integrity after creation + +set -euo pipefail + +# Configuration +BACKUP_DIR="${BACKUP_DIR:-/var/backups/debate/postgres}" +RETENTION_DAYS="${RETENTION_DAYS:-30}" +CONTAINER_NAME="${CONTAINER_NAME:-debate-postgres}" +DB_NAME="${DB_NAME:-debate}" +DB_USER="${DB_USER:-debate}" +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +BACKUP_FILE="${BACKUP_DIR}/${DB_NAME}_${TIMESTAMP}.dump" + +# Logging +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" +} + +# Create backup directory +mkdir -p "$BACKUP_DIR" + +log "Starting backup of database: $DB_NAME" + +# Create backup using pg_dump custom format (-Fc) +# Custom format is compressed and allows selective restore +docker exec "$CONTAINER_NAME" pg_dump \ + -U "$DB_USER" \ + -Fc \ + -b \ + -v \ + "$DB_NAME" > "$BACKUP_FILE" 2>/dev/null + +log "Backup created: $BACKUP_FILE" + +# Verify backup integrity using pg_restore --list +# This reads the archive table of contents without restoring +# We pipe the backup into the container since pg_restore is only available there +log "Verifying backup integrity..." +cat "$BACKUP_FILE" | docker exec -i "$CONTAINER_NAME" pg_restore --list > /dev/null 2>&1 || { + log "ERROR: Backup verification failed!" + rm -f "$BACKUP_FILE" + exit 1 +} + +# Get backup size +BACKUP_SIZE=$(du -h "$BACKUP_FILE" | cut -f1) +log "Backup size: $BACKUP_SIZE" + +# Compress backup (pg_dump -Fc includes compression, but gzip adds more) +gzip -f "$BACKUP_FILE" +log "Compressed: ${BACKUP_FILE}.gz" + +# Clean up old backups +log "Removing backups older than $RETENTION_DAYS days..." +find "$BACKUP_DIR" -name "${DB_NAME}_*.dump.gz" -mtime +$RETENTION_DAYS -delete +REMAINING=$(find "$BACKUP_DIR" -name "${DB_NAME}_*.dump.gz" | wc -l) +log "Remaining backups: $REMAINING" + +# Weekly restore test (every Monday) +if [ "$(date +%u)" -eq 1 ]; then + log "Running weekly restore test..." + TEST_DB="${DB_NAME}_backup_test" + + # Create test database + docker exec "$CONTAINER_NAME" createdb -U "$DB_USER" "$TEST_DB" 2>/dev/null || true + + # Restore to test database + gunzip -c "${BACKUP_FILE}.gz" | docker exec -i "$CONTAINER_NAME" pg_restore \ + -U "$DB_USER" \ + -d "$TEST_DB" \ + --clean \ + --if-exists 2>&1 || true + + # Drop test database + docker exec "$CONTAINER_NAME" dropdb -U "$DB_USER" "$TEST_DB" 2>/dev/null || true + + log "Weekly restore test completed" +fi + +log "Backup completed successfully" diff --git a/scripts/cron/postgres-backup b/scripts/cron/postgres-backup new file mode 100644 index 0000000..93d96b4 --- /dev/null +++ b/scripts/cron/postgres-backup @@ -0,0 +1,5 @@ +# PostgreSQL daily backup at 2 AM +# Install: sudo cp scripts/cron/postgres-backup /etc/cron.d/debate-postgres-backup +# Requires: /var/log/debate directory to exist + +0 2 * * * root /home/mikkel/repos/debate/scripts/backup-postgres.sh >> /var/log/debate/postgres-backup.log 2>&1