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