PostgreSQL 数据库容器自动备份
1.目的
确保 Docker 上的 PostgreSQL 数据库能够得到定时、完整、安全的逻辑备份,当发生数据误删、容器故障或人为错误时,能够快速将全集群数据恢复至备份时间点,满足业务连续性要求。
2.技术环境与前置条件
操作系统:Ubuntu Server 24.04 LTS(宿主机)
运行环境:Docker 部署的 PostgreSQL 容器
备份方式:pg_dumpall 全集群逻辑备份(包含所有数据库、角色、表空间)
调度方式:crontab 定时任务
保留策略:本地保留最近 7 天的压缩备份文件
3.操作流程
3.1 前置条件检查
在开始部署前,请逐项确认以下条件:
宿主机已安装 Docker,且
docker命令可正常运行docker --version目标 PostgreSQL 容器已启动,容器名为
TestContainer(以实际为准)docker ps | grep TestContainer已创建具有 超级用户权限(或能读取所有数据库)的数据库账号
TestUser备份目录
/data/backups磁盘空间充足拥有
root权限或具备docker执行权限的操作系统用户
3.2 确认密码文件权限
sudo ls -l /opt/secrets/postgres-password.txt # 权限应为 -rw------- (600)3.3 部署备份脚本
将附录中的完整脚本保存到宿主机,例如 /usr/local/bin/postgres_backup.sh,并赋予执行权限。
sudo nano /usr/local/bin/postgres_backup.sh
sudo chmod +x /usr/local/bin/postgres_backup.sh # 赋予执行权限3.4 配置脚本参数
根据实际环境修改脚本配置区域的以下变量(直接编辑脚本):
变量 | 示例值 | 说明 |
|---|---|---|
CONTAINER_NAME |
| Docker 容器名 |
DB_USER |
| 具有足够权限的数据库用户 |
BACKUP_DIR |
| 备份存放目录(需预先创建) |
RETENTION_DAYS |
| 保留天数,可按需调整 |
SECRET_FILE |
| 密码文件路径,与 3.2 一致 |
保存后,建议使用 bash -n /usr/local/bin/postgres_backup.sh 检查脚本语法。
3.5 手动执行测试
在接入定时任务前,请务必手动运行一次,确保整条链路畅通。
sudo /usr/local/bin/postgres_backup.sh成功标准:
终端输出包含
备份成功: ... .sql.gz备份目录下生成
.sql.gz文件日志文件
/wjw/data/backups/backup.log中无错误记录
若失败,请参照 第6章 常见问题处理。
3.6 设置 Crontab 定时任务
推荐在 业务低峰期(例如凌晨 3:00) 执行备份。
sudo crontab -e添加以下行(每天凌晨 3:00 执行):
0 3 * * * /usr/local/bin/postgres_backup.sh详解:
* * * * * command
┬ ┬ ┬ ┬ ┬
│ │ │ │ └─ 星期 (0-7, 0和7都表示周日)
│ │ │ └─── 月份 (1-12)
│ │ └───── 日期 (1-31)
│ └─────── 小时 (0-23)
└───────── 分钟 (0-59)✅ 验证:可以通过 crontab -l 查看已配置的任务。
3.7 恢复数据库
可参考文章:PostgreSQL数据库恢复
4.脚本关键机制详解
4.1 严格错误控制
set -Eeuo pipefail-e:只要脚本里任何一条命令执行失败(返回非零值),脚本立刻退出。避免带着错误继续运行,例如备份命令失败了还去清理旧文件。-E:配合-e,让函数内部出现的错误也能触发退出。-u:使用未定义的变量时报错退出,防止变量名拼写错误导致不可预期的行为。-o pipefail:管道命令中如果左侧命令失败,整体也判断为失败。例如docker exec … | gzip > file,如果docker exec失败,脚本会感知到。
通俗理解:这是一副“安全手套”,保证脚本在任何意外情况下都会立刻停下来,而不会假装一切正常。
4.2 密码安全“三不”原则
不硬编码:密码绝不出现在脚本里,防止代码泄露导致密码外泄。
不在命令行暴露:不使用
docker exec … pg_dumpall -W "password",因为这会让密码直接出现在ps进程列表中。不写进环境变量文件:不在
~/.bashrc等文件中存放密码。
我们的做法:
将密码存在单独的文件 /opt/secrets/postgres-password.txt,
通过 docker exec -e PGPASSWORD="${DB_PASSWORD}" 以临时环境变量的方式传给容器内的 pg_dumpall。
该环境变量的生命周期仅限于这条 docker exec 命令执行期间,安全性较高。
同时脚本会检查该文件权限是否为 600 或 400,如果不是则报警。
4.3 文件锁防并发
exec 200>"/var/lock/postgres_backup.lock"
flock -n 200 || { log "已有备份任务正在执行..."; exit 1; }作用:若前一次备份还没执行完(比如大数据库备份耗时较长),cron 又到了下一次触发时间,会导致两个备份任务同时运行,争抢 CPU 和 I/O,甚至生成冲突的备份文件。
实现原理:
flock -n尝试获得一个非阻塞的排他锁,如果拿不到锁(说明有另一个实例持有着),则立即退出并写日志。这就像厕所门上的“有人/无人”指示器,保证同时只有一个人进去。
4.4 原子化备份与临时文件
TEMP_FILE="...sql.tmp"
FINAL_FILE="...sql"
docker exec ... > "${TEMP_FILE}" && mv "${TEMP_FILE}" "${FINAL_FILE}"问题场景:如果备份过程中磁盘写满、网络中断或容器重启,直接写正式文件可能会留下一个“半截”的
.sql文件,后续清理脚本无法识别,且恢复时会报错。解决方案:先输出到
.tmp临时文件,只有docker exec完全成功(返回 0)才把它重命名为正式文件。失败时立即删除.tmp。这样备份目录下永远只有完整的备份。
4.5 过期清理与日志审计
find ... -mtime +7 -delete使用 find 命令基于文件修改时间清理 7 天前的备份。所有操作(成功、失败、清理、列表)都通过 log 函数写入日志并带时间戳,方便未来排查“某天为什么没备份”或“磁盘什么时候被清理过”。
5.日常检查与维护
每日检查:登录服务器执行 tail -20 /data/backups/backup.log,确认最近一次 备份成功。
磁盘监控:df -h /data/backups 确保使用率不超过 80%。
权限复查:每季度检查 /opt/secrets/postgres-password.txt 权限是否为 600。
脚本更新:当数据库用户、容器名、备份目录变更时,及时更新脚本并测试。
6.常见问题处理
故障现象 | 可能原因 | 处理方法 |
|---|---|---|
日志提示 | 未创建或路径错误 | 按 5.2 重新创建,检查 |
| 密码文件权限过于开放 |
|
| 容器未启动/用户权限不足/密码错误 | 手动执行 |
| 前一次备份未结束,锁冲突 | 这是正常保护,检查是否有僵死进程,或备份时间是否过长 |
找不到备份文件 | 磁盘满或 | 检查磁盘空间,确认 |
恢复后缺少某些表数据 |
| 确认备份时间在数据变更之后;考虑增加备份频率或配合 WAL 归档 |
7.标准备份操作脚本模板
#!/bin/bash
set -Eeuo pipefail
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH"
# ==================== 配置区域 ====================
CONTAINER_NAME="TestContainer" # Docker 容器名
DB_USER="TestUser" # 数据库用户名
BACKUP_DIR="/data/backups" # 备份存储目录
RETENTION_DAYS=7 # 保留最近多少天的备份
LOCK_FILE="/var/lock/postgres_backup.lock"
LOG_FILE="${BACKUP_DIR}/backup.log"
# ---------- 安全:密码从 Docker Secret 对应的宿主机文件读取 ----------
SECRET_FILE="/opt/secrets/postgres-password.txt" # 与 docker-compose.yml 一致
# ==================== 基础检查 ====================
mkdir -p "${BACKUP_DIR}"
touch "${LOG_FILE}"
log() {
echo "[$(date '+%F %T')] $*" | tee -a "${LOG_FILE}"
}
# 密码文件存在性检查
if [[ ! -f "$SECRET_FILE" ]]; then
log "错误:密码文件 $SECRET_FILE 不存在,备份终止。"
exit 1
fi
# 简单权限提醒(不强行退出,但会写入日志)
file_perm=$(stat -c %a "$SECRET_FILE" 2>/dev/null || echo "unknown")
if [[ "$file_perm" != "600" && "$file_perm" != "400" ]]; then
log "安全提醒:密码文件 $SECRET_FILE 权限为 $file_perm,建议设为 600 或 400。"
fi
# 安全地读取密码(该文件应只包含密码本身,不含额外换行)
DB_PASSWORD=$(<"$SECRET_FILE")
# 防止重复执行
exec 200>"${LOCK_FILE}"
flock -n 200 || {
log "已有备份任务正在执行,当前任务退出。"
exit 1
}
# ==================== 备份逻辑 ====================
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
TEMP_FILE="${BACKUP_DIR}/postgres_backup_${TIMESTAMP}.sql.tmp"
FINAL_FILE="${BACKUP_DIR}/postgres_backup_${TIMESTAMP}.sql"
log "开始备份 PostgreSQL 全集群数据..."
# 注意:这里不再使用 sudo,假设脚本已以 root 或具备 docker 权限的用户运行
if docker exec -e PGPASSWORD="${DB_PASSWORD}" "${CONTAINER_NAME}" \
pg_dumpall -c -U "${DB_USER}" > "${TEMP_FILE}"; then
mv "${TEMP_FILE}" "${FINAL_FILE}"
gzip "${FINAL_FILE}"
log "备份成功: ${FINAL_FILE}.gz (大小: $(du -h "${FINAL_FILE}.gz" | awk '{print $1}'))"
else
rm -f "${TEMP_FILE}"
log "备份失败!"
exit 1
fi
# ==================== 清理旧备份 ====================
log "清理 ${RETENTION_DAYS} 天前的旧备份..."
find "${BACKUP_DIR}" -type f -name "postgres_backup_*.sql.gz" -mtime +${RETENTION_DAYS} -delete
log "当前备份文件列表:"
ls -lh "${BACKUP_DIR}" | grep "postgres_backup_" || true
log "备份任务完成。"