#!/usr/bin/env bash # ============================================================= # E-Cosplay Keycloak backup # ------------------------------------------------------------- # Dumps the Keycloak Postgres database to /backup/ as a # timestamped, gzip-compressed SQL file, then rotates old # dumps (keeps the latest N). # # Usage: # ./scripts/backup.sh # default settings # BACKUP_DIR=/mnt/nas RETENTION=30 \ # ./scripts/backup.sh # override via env # # Suggested cron (daily at 03:15): # 15 3 * * * /var/www/e-auth/scripts/backup.sh >>/var/log/e-auth-backup.log 2>&1 # # Note: the repo itself (compose file, realm JSON, themes, # Ansible, sync.sh, etc.) lives in git and does not need to # be backed up here. Only the Postgres volume carries state # that is not reproducible from code (users, credentials, # TOTP secrets, sessions, brute-force counters, ...). # ============================================================= set -euo pipefail BACKUP_DIR=${BACKUP_DIR:-/backup} RETENTION=${RETENTION:-14} PG_CONTAINER=${PG_CONTAINER:-ecosplay-auth-db} PG_USER=${PG_USER:-keycloak} PG_DB=${PG_DB:-keycloak} TS=$(date +%F_%H-%M-%S) OUT="${BACKUP_DIR}/keycloak-${TS}.sql.gz" log() { printf '[backup %s] %s\n' "$(date +%T)" "$*"; } fail() { printf '[backup %s] ERROR: %s\n' "$(date +%T)" "$*" >&2; exit 1; } # ------------------------------------------------------------- # Pre-flight checks # ------------------------------------------------------------- command -v docker >/dev/null || fail "docker not found in PATH" if ! docker ps --format '{{.Names}}' | grep -qx "${PG_CONTAINER}"; then fail "container '${PG_CONTAINER}' is not running" fi # ------------------------------------------------------------- # Dump # ------------------------------------------------------------- log "Preparing ${BACKUP_DIR}" mkdir -p "${BACKUP_DIR}" chmod 700 "${BACKUP_DIR}" log "Dumping database '${PG_DB}' from container '${PG_CONTAINER}'" if ! docker exec "${PG_CONTAINER}" \ pg_dump -U "${PG_USER}" -d "${PG_DB}" \ --no-owner --clean --if-exists \ | gzip -9 > "${OUT}.part"; then rm -f "${OUT}.part" fail "pg_dump failed" fi mv "${OUT}.part" "${OUT}" chmod 600 "${OUT}" SIZE=$(du -h "${OUT}" | cut -f1) log "Wrote ${OUT} (${SIZE})" # ------------------------------------------------------------- # Rotate: keep only the latest ${RETENTION} backups # ------------------------------------------------------------- log "Pruning backups older than the last ${RETENTION}" cd "${BACKUP_DIR}" # shellcheck disable=SC2010 ls -1t keycloak-*.sql.gz 2>/dev/null \ | tail -n +"$((RETENTION + 1))" \ | while read -r old; do log " removing ${old}" rm -f "${old}" done log "Done."