feat: homelab deployment - NPM + IONOS DNS + monitoring + NAS backup
- 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>
This commit is contained in:
287
scripts/backup-to-nas.sh
Normal file
287
scripts/backup-to-nas.sh
Normal file
@@ -0,0 +1,287 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user