obmp-docker/scripts/pg-backup.sh

106 lines
4.2 KiB
Bash
Raw Normal View History

#!/usr/bin/env bash
#
# pg-backup.sh — logical backup of the OpenBMP PostgreSQL database.
#
# Performs a `pg_dump` of the `openbmp` database inside the obmp-psql
# container, writes a timestamped compressed dump to a backup directory,
# and prunes dumps older than the configured retention.
#
# Usage:
# ./pg-backup.sh
#
# Configuration (environment variables, all optional):
# OBMP_DATA_ROOT Base data dir. Default: /var/openbmp
# Backups go to ${OBMP_DATA_ROOT}/backups unless
# OBMP_BACKUP_DIR is set.
# OBMP_BACKUP_DIR Explicit backup directory. Overrides the default.
# OBMP_PG_CONTAINER Postgres container name. Default: obmp-psql
# OBMP_PG_DB Database name. Default: openbmp
# OBMP_PG_USER Database user. Default: openbmp
# OBMP_BACKUP_RETENTION_DAYS Prune dumps older than N days. Default: 14
#
# Output format:
# pg_dump custom format (-Fc), gzip-level compressed by pg_dump itself.
# Restore with `pg_restore` — see docs/backup-restore.md.
#
# This script is idempotent and safe to run repeatedly. It does not stop
# the database; pg_dump takes a consistent MVCC snapshot of a live DB.
#
# Make it executable once:
# chmod +x scripts/pg-backup.sh
#
# ----------------------------------------------------------------------
# Scheduling via cron
# ----------------------------------------------------------------------
# Run `crontab -e` and add (daily at 02:30, log to a file):
#
# 30 2 * * * OBMP_DATA_ROOT=/var/openbmp /home/user/obmp-docker/scripts/pg-backup.sh >> /var/openbmp/backups/pg-backup.log 2>&1
#
# The script must be able to reach the Docker daemon, so run it as a user
# in the `docker` group (or root). For systemd-based hosts a
# systemd timer is an equally good alternative to cron.
# ----------------------------------------------------------------------
set -euo pipefail
# --- Configuration -----------------------------------------------------
OBMP_DATA_ROOT="${OBMP_DATA_ROOT:-/var/openbmp}"
BACKUP_DIR="${OBMP_BACKUP_DIR:-${OBMP_DATA_ROOT}/backups}"
PG_CONTAINER="${OBMP_PG_CONTAINER:-obmp-psql}"
PG_DB="${OBMP_PG_DB:-openbmp}"
PG_USER="${OBMP_PG_USER:-openbmp}"
RETENTION_DAYS="${OBMP_BACKUP_RETENTION_DAYS:-14}"
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
DUMP_NAME="openbmp-${TIMESTAMP}.dump"
DUMP_PATH="${BACKUP_DIR}/${DUMP_NAME}"
DUMP_TMP="${DUMP_PATH}.partial"
log() { printf '%s [pg-backup] %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*"; }
fail() { log "ERROR: $*" >&2; exit 1; }
# --- Pre-flight checks -------------------------------------------------
command -v docker >/dev/null 2>&1 || fail "docker command not found in PATH"
if ! docker inspect -f '{{.State.Running}}' "${PG_CONTAINER}" 2>/dev/null | grep -q true; then
fail "container '${PG_CONTAINER}' is not running"
fi
mkdir -p "${BACKUP_DIR}" || fail "cannot create backup directory ${BACKUP_DIR}"
# --- Backup ------------------------------------------------------------
# Write to a .partial file first, then atomically rename on success so a
# crashed/interrupted run never leaves a truncated dump that looks valid.
log "starting backup of database '${PG_DB}' from container '${PG_CONTAINER}'"
if docker exec "${PG_CONTAINER}" \
pg_dump -U "${PG_USER}" -d "${PG_DB}" -Fc --no-owner --no-privileges \
> "${DUMP_TMP}"; then
mv -f "${DUMP_TMP}" "${DUMP_PATH}"
else
rm -f "${DUMP_TMP}"
fail "pg_dump failed; no backup written"
fi
DUMP_SIZE="$(du -h "${DUMP_PATH}" | cut -f1)"
log "backup complete: ${DUMP_PATH} (${DUMP_SIZE})"
# --- Prune old backups -------------------------------------------------
# Only prune files matching our own naming pattern, so nothing else in the
# directory (logs, manual dumps) is touched.
log "pruning dumps older than ${RETENTION_DAYS} days"
PRUNED=0
while IFS= read -r -d '' old; do
rm -f "${old}"
log " removed $(basename "${old}")"
PRUNED=$((PRUNED + 1))
done < <(find "${BACKUP_DIR}" -maxdepth 1 -type f \
-name 'openbmp-*.dump' -mtime "+${RETENTION_DAYS}" -print0)
log "pruned ${PRUNED} old dump(s)"
# Also clean up any stale .partial files from previous crashed runs.
find "${BACKUP_DIR}" -maxdepth 1 -type f -name 'openbmp-*.dump.partial' \
-mtime +1 -delete 2>/dev/null || true
log "done"