platform-deployments/scripts/lilith-backup-postgres.sh
Quinn Ftw 3dbd4375d3 chore(conf.d): 🔧 Update configuration service files in conf.d directory
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-02-18 13:42:49 -08:00

150 lines
5.2 KiB
Bash
Executable file

#!/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}"