#!/bin/sh # Nightly Postgres backup loop for Meezi. # # Runs inside a small postgres-image container (has pg_dump/gzip). Every day at # ~02:00 Tehran it dumps the whole database, gzips it, and keeps the last # RETAIN_DAYS files in /backups. Designed to be dead-simple and dependency-free: # no cron daemon, just sleep-until-next-run so it survives container restarts. # # Env: # PGHOST, PGUSER, PGPASSWORD, PGDATABASE — connection (from compose) # RETAIN_DAYS — how many daily dumps to keep (default 14) # BACKUP_HOUR — local hour to run (default 2 = 02:00) set -eu RETAIN_DAYS="${RETAIN_DAYS:-14}" BACKUP_HOUR="${BACKUP_HOUR:-2}" OUT_DIR=/backups export TZ="${TZ:-Asia/Tehran}" log() { echo "[pg-backup $(date '+%Y-%m-%d %H:%M:%S %Z')] $*"; } run_backup() { ts=$(date '+%Y%m%d_%H%M%S') tmp="$OUT_DIR/.meezi_${ts}.sql.gz.partial" final="$OUT_DIR/meezi_${ts}.sql.gz" log "starting dump → $final" # pg_dump streams to gzip; .partial then atomic rename so a crash never # leaves a truncated file that looks like a good backup. if pg_dump --no-owner --no-privileges | gzip -9 > "$tmp"; then mv "$tmp" "$final" size=$(wc -c < "$final" 2>/dev/null || echo '?') log "done ($size bytes)" else rm -f "$tmp" log "ERROR: dump failed" return 1 fi # Rotate: delete dumps older than RETAIN_DAYS days. find "$OUT_DIR" -maxdepth 1 -name 'meezi_*.sql.gz' -mtime "+${RETAIN_DAYS}" -print -delete | while read -r f; do log "rotated out $f" done } seconds_until_next_run() { now_h=$(date '+%-H'); now_m=$(date '+%-M'); now_s=$(date '+%-S') now=$(( now_h * 3600 + now_m * 60 + now_s )) target=$(( BACKUP_HOUR * 3600 )) if [ "$now" -lt "$target" ]; then echo $(( target - now )) else echo $(( 86400 - now + target )) fi } log "backup loop started (retain ${RETAIN_DAYS}d, daily at ${BACKUP_HOUR}:00 ${TZ})" # Take one backup immediately on first boot so we never sit a full day with none. run_backup || true while true; do wait_s=$(seconds_until_next_run) log "next backup in ${wait_s}s" sleep "$wait_s" run_backup || true done