- Restructured docker-compose for Nginx Proxy Manager (no custom nginx) - Added domain wordly.art configuration - Added Prometheus + Grafana monitoring stack with pre-configured dashboards - Added PostgreSQL backup script to NAS (daily/weekly/monthly rotation) - Added alert rules for backend, system, and Docker metrics - Updated deployment guide for NPM + IONOS DNS homelab setup - Added marketing plan document - PDF translator and watermark support - Enhanced middleware, routes, and translator modules Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
288 lines
8.6 KiB
Bash
288 lines
8.6 KiB
Bash
#!/bin/bash
|
|
# ============================================
|
|
# Wordly.art - PostgreSQL Backup to NAS
|
|
# ============================================
|
|
# CRON: Run daily at 03:00
|
|
# 0 3 * * * /opt/wordly/scripts/backup-to-nas.sh >> /var/log/wordly-backup.log 2>&1
|
|
#
|
|
# Usage:
|
|
# ./backup-to-nas.sh # Default: daily backup
|
|
# ./backup-to-nas.sh --full # Full backup with upload cleanup
|
|
# ./backup-to-nas.sh --restore FILE # Restore from specific backup
|
|
# ============================================
|
|
|
|
set -euo pipefail
|
|
|
|
# ===========================================
|
|
# CONFIGURATION - MODIFY THESE VALUES
|
|
# ===========================================
|
|
# NAS settings (SMB/CIFS or NFS mount point)
|
|
NAS_BACKUP_DIR="/mnt/nas-backups/wordly"
|
|
|
|
# Docker container name for PostgreSQL
|
|
POSTGRES_CONTAINER="wordly-postgres"
|
|
POSTGRES_USER="translate"
|
|
POSTGRES_DB="translate_db"
|
|
POSTGRES_PASSWORD="yLLgkEvt6mvzGDdoqtQvI1vEgMmR-W75ZTPW5StaIAU"
|
|
|
|
# Backup retention
|
|
DAILY_RETENTION=7 # Keep 7 daily backups
|
|
WEEKLY_RETENTION=4 # Keep 4 weekly backups
|
|
MONTHLY_RETENTION=6 # Keep 6 monthly backups
|
|
|
|
# Notification (optional - leave empty to disable)
|
|
NOTIFICATION_WEBHOOK="" # Slack/Discord webhook URL
|
|
|
|
# ===========================================
|
|
# INTERNALS
|
|
# ===========================================
|
|
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
|
DATE_ONLY=$(date +"%Y-%m-%d")
|
|
DAY_OF_WEEK=$(date +"%u") # 1=Mon, 7=Sun
|
|
DAY_OF_MONTH=$(date +"%d")
|
|
BACKUP_NAME="wordly_db_${TIMESTAMP}.sql.gz"
|
|
BACKUP_PATH="${NAS_BACKUP_DIR}/${BACKUP_NAME}"
|
|
LOG_PREFIX="[Wordly Backup ${TIMESTAMP}]"
|
|
|
|
# Colors for terminal output
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
NC='\033[0m'
|
|
|
|
# ===========================================
|
|
# FUNCTIONS
|
|
# ===========================================
|
|
|
|
log() {
|
|
echo "${LOG_PREFIX} $1"
|
|
}
|
|
|
|
log_success() {
|
|
echo -e "${LOG_PREFIX} ${GREEN}$1${NC}"
|
|
}
|
|
|
|
log_error() {
|
|
echo -e "${LOG_PREFIX} ${RED}ERROR: $1${NC}"
|
|
}
|
|
|
|
log_warning() {
|
|
echo -e "${LOG_PREFIX} ${YELLOW}WARNING: $1${NC}"
|
|
}
|
|
|
|
send_notification() {
|
|
local message="$1"
|
|
if [ -n "${NOTIFICATION_WEBHOOK}" ]; then
|
|
curl -s -X POST "${NOTIFICATION_WEBHOOK}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"text\": \"${message}\"}" > /dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
|
|
check_prerequisites() {
|
|
# Check NAS mount
|
|
if [ ! -d "${NAS_BACKUP_DIR}" ]; then
|
|
log_error "NAS backup directory not found: ${NAS_BACKUP_DIR}"
|
|
log "Attempting to mount NAS..."
|
|
|
|
# Try to mount if configured via /etc/fstab
|
|
mount "${NAS_BACKUP_DIR}" 2>/dev/null || true
|
|
|
|
if [ ! -d "${NAS_BACKUP_DIR}" ]; then
|
|
log_error "Cannot mount NAS. Aborting."
|
|
send_notification "Wordly Backup FAILED: NAS not mounted at ${NAS_BACKUP_DIR}"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# Check Docker
|
|
if ! docker ps --format '{{.Names}}' | grep -q "${POSTGRES_CONTAINER}"; then
|
|
log_error "PostgreSQL container '${POSTGRES_CONTAINER}' is not running."
|
|
send_notification "Wordly Backup FAILED: PostgreSQL container not running"
|
|
exit 1
|
|
fi
|
|
|
|
log_success "Prerequisites OK"
|
|
}
|
|
|
|
create_backup() {
|
|
log "Starting backup of '${POSTGRES_DB}'..."
|
|
|
|
# Create backup directory structure
|
|
mkdir -p "${NAS_BACKUP_DIR}/daily"
|
|
mkdir -p "${NAS_BACKUP_DIR}/weekly"
|
|
mkdir -p "${NAS_BACKUP_DIR}/monthly"
|
|
|
|
# Run pg_dump inside Docker container
|
|
docker exec "${POSTGRES_CONTAINER}" pg_dump \
|
|
-U "${POSTGRES_USER}" \
|
|
-d "${POSTGRES_DB}" \
|
|
--format=custom \
|
|
--compress=9 \
|
|
--no-owner \
|
|
--no-acl \
|
|
2>/dev/null | gzip > "${NAS_BACKUP_DIR}/daily/${BACKUP_NAME}"
|
|
|
|
local backup_size=$(du -h "${NAS_BACKUP_DIR}/daily/${BACKUP_NAME}" | cut -f1)
|
|
|
|
if [ -f "${NAS_BACKUP_DIR}/daily/${BACKUP_NAME}" ]; then
|
|
log_success "Backup created: ${BACKUP_NAME} (${backup_size})"
|
|
|
|
# Copy to weekly/monthly if applicable
|
|
if [ "${DAY_OF_WEEK}" = "7" ]; then
|
|
cp "${NAS_BACKUP_DIR}/daily/${BACKUP_NAME}" "${NAS_BACKUP_DIR}/weekly/"
|
|
log "Weekly backup copied"
|
|
fi
|
|
|
|
if [ "${DAY_OF_MONTH}" = "01" ]; then
|
|
cp "${NAS_BACKUP_DIR}/daily/${BACKUP_NAME}" "${NAS_BACKUP_DIR}/monthly/"
|
|
log "Monthly backup copied"
|
|
fi
|
|
|
|
send_notification "Wordly Backup SUCCESS: ${BACKUP_NAME} (${backup_size})"
|
|
else
|
|
log_error "Backup file was not created!"
|
|
send_notification "Wordly Backup FAILED: pg_dump produced no output"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
cleanup_old_backups() {
|
|
log "Cleaning up old backups..."
|
|
|
|
# Daily: keep last N days
|
|
local daily_count=$(ls -1 "${NAS_BACKUP_DIR}/daily/" 2>/dev/null | wc -l)
|
|
if [ "${daily_count}" -gt "${DAILY_RETENTION}" ]; then
|
|
ls -1t "${NAS_BACKUP_DIR}/daily/" | tail -n +$((DAILY_RETENTION + 1)) | while read -r f; do
|
|
rm -f "${NAS_BACKUP_DIR}/daily/${f}"
|
|
log " Deleted daily: ${f}"
|
|
done
|
|
fi
|
|
|
|
# Weekly: keep last N weeks
|
|
local weekly_count=$(ls -1 "${NAS_BACKUP_DIR}/weekly/" 2>/dev/null | wc -l)
|
|
if [ "${weekly_count}" -gt "${WEEKLY_RETENTION}" ]; then
|
|
ls -1t "${NAS_BACKUP_DIR}/weekly/" | tail -n +$((WEEKLY_RETENTION + 1)) | while read -r f; do
|
|
rm -f "${NAS_BACKUP_DIR}/weekly/${f}"
|
|
log " Deleted weekly: ${f}"
|
|
done
|
|
fi
|
|
|
|
# Monthly: keep last N months
|
|
local monthly_count=$(ls -1 "${NAS_BACKUP_DIR}/monthly/" 2>/dev/null | wc -l)
|
|
if [ "${monthly_count}" -gt "${MONTHLY_RETENTION}" ]; then
|
|
ls -1t "${NAS_BACKUP_DIR}/monthly/" | tail -n +$((MONTHLY_RETENTION + 1)) | while read -r f; do
|
|
rm -f "${NAS_BACKUP_DIR}/monthly/${f}"
|
|
log " Deleted monthly: ${f}"
|
|
done
|
|
fi
|
|
|
|
log_success "Cleanup done"
|
|
}
|
|
|
|
verify_backup() {
|
|
log "Verifying backup integrity..."
|
|
|
|
if gzip -t "${NAS_BACKUP_DIR}/daily/${BACKUP_NAME}" 2>/dev/null; then
|
|
log_success "Backup integrity OK"
|
|
else
|
|
log_error "Backup integrity check FAILED!"
|
|
send_notification "Wordly Backup WARNING: Integrity check failed for ${BACKUP_NAME}"
|
|
# Don't delete - let admin investigate
|
|
fi
|
|
}
|
|
|
|
restore_backup() {
|
|
local backup_file="$1"
|
|
|
|
if [ -z "${backup_file}" ]; then
|
|
log_error "Usage: $0 --restore <backup_file>"
|
|
echo ""
|
|
echo "Available backups:"
|
|
echo "=== Daily ==="
|
|
ls -lht "${NAS_BACKUP_DIR}/daily/" 2>/dev/null || echo " (none)"
|
|
echo "=== Weekly ==="
|
|
ls -lht "${NAS_BACKUP_DIR}/weekly/" 2>/dev/null || echo " (none)"
|
|
echo "=== Monthly ==="
|
|
ls -lht "${NAS_BACKUP_DIR}/monthly/" 2>/dev/null || echo " (none)"
|
|
exit 1
|
|
fi
|
|
|
|
# Find the file
|
|
local full_path=""
|
|
for dir in daily weekly monthly; do
|
|
if [ -f "${NAS_BACKUP_DIR}/${dir}/${backup_file}" ]; then
|
|
full_path="${NAS_BACKUP_DIR}/${dir}/${backup_file}"
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [ -z "${full_path}" ]; then
|
|
log_error "Backup file not found: ${backup_file}"
|
|
exit 1
|
|
fi
|
|
|
|
echo ""
|
|
log_warning "RESTORE MODE - This will OVERWRITE the current database!"
|
|
echo " File: ${full_path}"
|
|
echo " Database: ${POSTGRES_DB}"
|
|
echo ""
|
|
read -p "Are you sure? Type 'YES' to confirm: " confirm
|
|
|
|
if [ "${confirm}" != "YES" ]; then
|
|
log "Restore cancelled."
|
|
exit 0
|
|
fi
|
|
|
|
log "Restoring from ${full_path}..."
|
|
|
|
# Create a safety backup first
|
|
log "Creating safety backup before restore..."
|
|
SAFETY_NAME="wordly_db_pre_restore_${TIMESTAMP}.sql.gz"
|
|
docker exec "${POSTGRES_CONTAINER}" pg_dump \
|
|
-U "${POSTGRES_USER}" \
|
|
-d "${POSTGRES_DB}" \
|
|
--format=custom \
|
|
--compress=9 \
|
|
2>/dev/null | gzip > "${NAS_BACKUP_DIR}/daily/${SAFETY_NAME}"
|
|
log "Safety backup: ${SAFETY_NAME}"
|
|
|
|
# Restore
|
|
gunzip -c "${full_path}" | docker exec -i "${POSTGRES_CONTAINER}" \
|
|
pg_restore \
|
|
-U "${POSTGRES_USER}" \
|
|
-d "${POSTGRES_DB}" \
|
|
--clean \
|
|
--if-exists \
|
|
--no-owner \
|
|
--no-acl \
|
|
2>/dev/null || true
|
|
|
|
log_success "Restore completed!"
|
|
log_warning "Restart backend: docker restart wordly-backend"
|
|
}
|
|
|
|
# ===========================================
|
|
# MAIN
|
|
# ===========================================
|
|
|
|
case "${1:-}" in
|
|
--restore)
|
|
restore_backup "${2:-}"
|
|
;;
|
|
--full)
|
|
check_prerequisites
|
|
create_backup
|
|
verify_backup
|
|
cleanup_old_backups
|
|
log_success "Full backup cycle complete!"
|
|
;;
|
|
*)
|
|
check_prerequisites
|
|
create_backup
|
|
verify_backup
|
|
cleanup_old_backups
|
|
log_success "Backup complete!"
|
|
;;
|
|
esac
|