From 5af94062d25cca1be2cf18b7f348455ff0e59430 Mon Sep 17 00:00:00 2001 From: Serreau Jovann Date: Fri, 10 Apr 2026 16:21:35 +0200 Subject: [PATCH] Add Postgres backup script scripts/backup.sh runs pg_dump inside the ecosplay-auth-db container, writes a gzipped, timestamped dump to /backup/ (overridable via BACKUP_DIR), and keeps the latest N dumps (RETENTION=14 by default). Only the Postgres volume carries state that isn't reproducible from code (users, credentials, TOTP secrets, sessions, brute- force counters), so the rest of the repo is not bundled here. Intended as a base that can later feed a cron job or be piped to off-site storage (S3, SFTP, borg). Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/backup.sh | 82 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100755 scripts/backup.sh diff --git a/scripts/backup.sh b/scripts/backup.sh new file mode 100755 index 0000000..8c628e9 --- /dev/null +++ b/scripts/backup.sh @@ -0,0 +1,82 @@ +#!/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."