#!/usr/bin/env bash # ============================================================================= # lilith-backup-postgres.sh # ============================================================================= # Backs up all 16 Lilith Platform PostgreSQL databases using pg_dump --format=custom. # # STORAGE # Hot: /tank/backups/lilith/postgres/daily/YYYY-MM-DD_HH/ # Retention: 7 days (pruned at end of each run) # # CONFIG FILE # /var/home/lilith/.config/lilith/backup-postgres.conf # Format — one entry per line: # port:database:user:password # Example: # 25432:lilith_prod:postgres:s3cr3t # # RESTORE EXAMPLE # # List available backups # ls /tank/backups/lilith/postgres/daily/ # # # Restore a specific database (e.g. lilith_prod from 2026-02-18_06) # export PGPASSWORD='s3cr3t' # pg_restore \ # --host=localhost \ # --port=25432 \ # --username=postgres \ # --dbname=lilith_prod \ # --clean \ # --if-exists \ # /tank/backups/lilith/postgres/daily/2026-02-18_06/lilith_prod_25432.dump # # EXIT CODE # 0 — all databases backed up successfully # 1 — one or more databases failed (all are still attempted) # ============================================================================= set -uo pipefail # --------------------------------------------------------------------------- # Constants # --------------------------------------------------------------------------- readonly BACKUP_BASE="/tank/backups/lilith/postgres/daily" readonly CONF_FILE="/var/home/lilith/.config/lilith/backup-postgres.conf" readonly RETENTION_DAYS=7 TIMESTAMP="$(date --utc '+%Y-%m-%d_%H')" readonly TIMESTAMP BACKUP_DIR="${BACKUP_BASE}/${TIMESTAMP}" readonly BACKUP_DIR readonly LOG_PREFIX="[lilith-backup-postgres]" # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- log() { echo "${LOG_PREFIX} $(date --utc '+%Y-%m-%dT%H:%M:%SZ') INFO $*"; } warn() { echo "${LOG_PREFIX} $(date --utc '+%Y-%m-%dT%H:%M:%SZ') WARN $*" >&2; } err() { echo "${LOG_PREFIX} $(date --utc '+%Y-%m-%dT%H:%M:%SZ') ERROR $*" >&2; } # --------------------------------------------------------------------------- # Validate prerequisites # --------------------------------------------------------------------------- if [[ ! -f "${CONF_FILE}" ]]; then err "Config file not found: ${CONF_FILE}" err "Expected format (one line per db): port:database:user:password" exit 1 fi if ! command -v pg_dump &>/dev/null; then err "pg_dump not found in PATH. Install postgresql-client." exit 1 fi # --------------------------------------------------------------------------- # Prepare backup directory # --------------------------------------------------------------------------- mkdir -p "${BACKUP_DIR}" log "Backup directory: ${BACKUP_DIR}" # --------------------------------------------------------------------------- # Trap: log any unexpected exit # --------------------------------------------------------------------------- trap 'err "Script exited unexpectedly at line ${LINENO}"' ERR # --------------------------------------------------------------------------- # Backup loop # --------------------------------------------------------------------------- overall_exit=0 success_count=0 fail_count=0 while IFS=: read -r port database user password || [[ -n "${port:-}" ]]; do # Skip blank lines and comments [[ -z "${port}" || "${port}" == \#* ]] && continue dump_file="${BACKUP_DIR}/${database}_${port}.dump" log "Dumping ${database} (port ${port}, user ${user}) -> ${dump_file}" if PGPASSWORD="${password}" pg_dump \ --host=localhost \ --port="${port}" \ --username="${user}" \ --format=custom \ --compress=6 \ --no-password \ "${database}" \ > "${dump_file}" 2>&1; then dump_size="$(du -sh "${dump_file}" | cut -f1)" log " OK ${database} (${dump_size})" (( success_count++ )) || true else err " FAIL ${database} (port ${port}) — dump failed" # Remove incomplete file to avoid restoring corrupt data rm -f "${dump_file}" (( fail_count++ )) || true overall_exit=1 fi done < "${CONF_FILE}" log "Completed: ${success_count} succeeded, ${fail_count} failed" # --------------------------------------------------------------------------- # Prune old backups (keep RETENTION_DAYS worth of directories) # --------------------------------------------------------------------------- log "Pruning backup directories older than ${RETENTION_DAYS} days..." prune_count=0 while IFS= read -r -d '' old_dir; do log " Removing: ${old_dir}" rm -rf "${old_dir}" (( prune_count++ )) || true done < <(find "${BACKUP_BASE}" \ -mindepth 1 \ -maxdepth 1 \ -type d \ -mtime "+${RETENTION_DAYS}" \ -print0) log "Pruned ${prune_count} old backup directories" # --------------------------------------------------------------------------- # Final status # --------------------------------------------------------------------------- if [[ "${overall_exit}" -eq 0 ]]; then log "All databases backed up successfully" else err "${fail_count} database(s) failed — review logs above" fi exit "${overall_exit}"