#!/usr/bin/env bash # ───────────────────────────────────────────────────────────────────────────── # JNPF PostgreSQL 定时备份 # - docker exec pg_dump 逐库导出(custom 格式 -Fc,已压缩、可 pg_restore 选择性还原) # - 备份产物落 volume 外的宿主目录(BACKUP_DIR),容器/卷损坏也不丢 # - 每份导出用 pg_restore -l 校验可读性 # - 保留最近 RETENTION_DAYS 天,更早的自动删除 # # 用法: # ./backup.sh # 备份全部三库 # DATABASES="jnpf_init" ./backup.sh # # 参数(环境变量): # CONTAINER PG 容器名 默认 cx-pg-customer1(2026-08-08 客户A改名,交付环境用时显式传 CONTAINER=cx-postgres) # PGUSER 超级用户 默认 postgres # DATABASES 待备份库(空格分隔) 默认 "jnpf_init jnpf_flow jnpf_xxljob nacos_config" # BACKUP_DIR 宿主备份目录 默认 /data/pg-backup(务必在 volume 外) # RETENTION_DAYS 保留天数 默认 14 # # ── cron 定时(现场 crontab -e,每天 02:00)───────────────────────────────── # 0 2 * * * /opt/jnpf/offline/migration/backup.sh >> /var/log/jnpf-pg-backup.log 2>&1 # 注意:宿主文件存储(.env JNPF_DATA_DIR 下 jnpf-resources/,上传附件所在)不在 PG 里, # 需另行纳入文件级备份(rsync/tar 均可) # 还原示例: # gunzip -c jnpf_init_YYYYMMDD_HHMMSS.dump.gz > /tmp/x.dump # 若已 gzip # docker exec -i cx-pg-customer1 pg_restore -U postgres -d jnpf_init --clean --if-exists < /tmp/x.dump # (-Fc 自身已压缩,本脚本不再额外 gzip;下方 .dump 可直接 pg_restore) # ───────────────────────────────────────────────────────────────────────────── set -euo pipefail CONTAINER="${CONTAINER:-cx-pg-customer1}" PGUSER="${PGUSER:-postgres}" # nacos_config 一并备份(配置中心内容只存 PG,codex review 2026-07-17 补) DATABASES="${DATABASES:-jnpf_init jnpf_flow jnpf_xxljob nacos_config}" # Windows/Git Bash 护栏:默认 /data/pg-backup 会映射进 Git 安装目录(静默备份到错误位置), # 故 MSYS 环境必须显式传 BACKUP_DIR(如 /d/pg-backup) case "$(uname -s)" in MINGW*|MSYS*) [ -n "${BACKUP_DIR:-}" ] || { printf '\033[1;31m[backup][err]\033[0m %s\n' \ "Windows/Git Bash 下必须显式指定 BACKUP_DIR,如:BACKUP_DIR=/d/pg-backup $0" >&2 exit 1 } ;; esac BACKUP_DIR="${BACKUP_DIR:-/data/pg-backup}" RETENTION_DAYS="${RETENTION_DAYS:-14}" TS="$(date +%Y%m%d_%H%M%S)" log() { printf '\033[1;34m[backup]\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m[backup][warn]\033[0m %s\n' "$*"; } die() { printf '\033[1;31m[backup][err]\033[0m %s\n' "$*" >&2; exit 1; } docker inspect "$CONTAINER" >/dev/null 2>&1 || die "容器 $CONTAINER 不在" mkdir -p "$BACKUP_DIR" fail=0 for db in $DATABASES; do out="$BACKUP_DIR/${db}_${TS}.dump" log "pg_dump ${db} → $(basename "$out")" # -Fc custom 格式;失败则删残缺文件并计入失败 if docker exec "$CONTAINER" pg_dump -U "$PGUSER" -Fc -d "$db" > "$out" 2>/dev/null; then # 校验:pg_restore -l 能列出目录即视为可读 if docker exec -i "$CONTAINER" pg_restore -l < "$out" >/dev/null 2>&1; then log " ✓ ${db} $(du -h "$out" | cut -f1) 校验通过" else warn " ✗ ${db} 导出文件 pg_restore -l 校验失败"; fail=$((fail+1)) fi else warn " ✗ ${db} pg_dump 失败"; rm -f "$out"; fail=$((fail+1)) fi done log "清理 ${RETENTION_DAYS} 天前的旧备份 …" find "$BACKUP_DIR" -maxdepth 1 -name '*.dump' -type f -mtime "+${RETENTION_DAYS}" -print -delete | while read -r f; do log " 删除 $(basename "$f")" done log "当前备份目录占用:$(du -sh "$BACKUP_DIR" 2>/dev/null | cut -f1)" [ "$fail" = 0 ] || die "有 ${fail} 个库备份失败,检查日志" log "备份完成 ✓(${BACKUP_DIR})"