经历了之前几次瞎搞把环境弄崩的惨痛教训后,我深刻体会到了"数据备份"的重要性。不能总指望云服务商不跑路,自己手里有备份才踏实。
最开始我是手动打包,每次改版之前 tar 一下,特别原始。后来项目多了,手动备份又麻烦又容易忘。干脆写了个 Shell 脚本,每天凌晨自动跑,本地存一份,远端再同步一份,保留最近 30 天的历史版本。
这篇文章就分享一下我的备份脚本是怎么写的,以及一些我认为重要的细节。
备份需求分析
在写脚本之前,我先想清楚了自己需要什么:
- 备份对象:博客源码、数据库、Nginx 配置、SSL 证书。
- 备份频率:每天一次,凌晨 2 点执行。
- 保留策略:本地保留 7 天,远端保留 30 天。
- 异地存储:不能只存在服务器本地,万一服务器挂了照样没。
- 失败告警:备份失败要通知我,不然出问题了我都不知道没备份。
脚本整体结构
我把脚本分成几个部分:
- 定义变量和路径
- 创建临时目录
- 分别备份源码、数据库、配置
- 打包并压缩
- 上传到远端
- 清理过期备份
- 发送通知
下面是完整脚本:
1#!/bin/bash
2
3# ========================================
4# 自动备份脚本
5# ========================================
6
7# 基础配置
8BACKUP_DIR="/data/backup"
9TEMP_DIR="/tmp/backup_$(date +%Y%m%d_%H%M%S)"
10DATE=$(date +%Y%m%d)
11DATETIME=$(date +"%Y-%m-%d %H:%M:%S")
12LOG_FILE="$BACKUP_DIR/backup.log"
13
14# 要备份的内容
15BLOG_DIR="/var/www/myblog"
16NGINX_DIR="/etc/nginx"
17SSL_DIR="/etc/letsencrypt"
18DB_NAME="myblog"
19DB_USER="dbuser"
20DB_PASS="your_password"
21
22# 远端存储配置
23REMOTE_USER="backup"
24REMOTE_HOST="backup.example.com"
25REMOTE_DIR="/backups/myblog"
26
27# 通知配置(可选)
28WEBHOOK_URL="https://your-webhook-url"
29
30# 创建目录
31mkdir -p "$BACKUP_DIR"
32mkdir -p "$TEMP_DIR"
33
34# 记录开始
35echo "[$DATETIME] 开始备份..." >> "$LOG_FILE"
36
37# 1. 备份博客源码
38if [ -d "$BLOG_DIR" ]; then
39 cp -r "$BLOG_DIR" "$TEMP_DIR/blog"
40 echo "[$DATETIME] 博客源码备份完成" >> "$LOG_FILE"
41else
42 echo "[$DATETIME] 错误:博客目录不存在" >> "$LOG_FILE"
43 exit 1
44fi
45
46# 2. 备份数据库
47mysqldump -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" > "$TEMP_DIR/database.sql"
48if [ $? -eq 0 ]; then
49 echo "[$DATETIME] 数据库备份完成" >> "$LOG_FILE"
50else
51 echo "[$DATETIME] 错误:数据库备份失败" >> "$LOG_FILE"
52 exit 1
53fi
54
55# 3. 备份配置文件
56cp -r "$NGINX_DIR" "$TEMP_DIR/nginx"
57cp -r "$SSL_DIR" "$TEMP_DIR/ssl"
58
59# 4. 打包压缩
60BACKUP_FILE="blog_backup_$DATE.tar.gz"
61tar -czf "$BACKUP_DIR/$BACKUP_FILE" -C "$TEMP_DIR" .
62
63if [ $? -eq 0 ]; then
64 echo "[$DATETIME] 打包完成:$BACKUP_FILE" >> "$LOG_FILE"
65else
66 echo "[$DATETIME] 错误:打包失败" >> "$LOG_FILE"
67 exit 1
68fi
69
70# 5. 同步到远端
71rsync -avz --delete "$BACKUP_DIR/" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/"
72
73if [ $? -eq 0 ]; then
74 echo "[$DATETIME] 远端同步完成" >> "$LOG_FILE"
75else
76 echo "[$DATETIME] 错误:远端同步失败" >> "$LOG_FILE"
77 # 同步失败不一定要退出,但至少要知道
78fi
79
80# 6. 清理本地过期备份(保留 7 天)
81find "$BACKUP_DIR" -name "blog_backup_*.tar.gz" -type f -mtime +7 -delete
82
83# 7. 清理远端过期备份(保留 30 天)
84ssh "$REMOTE_USER@$REMOTE_HOST" "find $REMOTE_DIR -name 'blog_backup_*.tar.gz' -type f -mtime +30 -delete"
85
86# 8. 清理临时目录
87rm -rf "$TEMP_DIR"
88
89# 9. 发送成功通知
90curl -s -X POST "$WEBHOOK_URL" \
91 -H "Content-Type: application/json" \
92 -d "{\"msg\":\"博客备份完成:$BACKUP_FILE\"}"
93
94echo "[$DATETIME] 备份流程结束" >> "$LOG_FILE"
几个重要的设计细节
临时目录用时间戳命名
每次备份用一个带时间戳的临时目录,避免并行执行或者上次没清理完导致文件混乱。结尾 rm -rf "$TEMP_DIR" 确保每次都清干净。
错误处理要到位
虽然 Shell 脚本的 set -e 可以让出错时自动退出,但我更喜欢在每个关键步骤后面手动判断 $?。这样我可以把具体错误写到日志里,而不是莫名其妙地中断。
数据库密码不要直接写脚本里
我上面为了演示把 DB_PASS="your_password" 直接写进去了,实际生产环境不建议这么做。更好的做法是:
- 用
.my.cnf配置文件存数据库凭证:
1[mysqldump]
2user=dbuser
3password=your_password
或者通过环境变量传入。
也可以给数据库用户单独授权,只给
SELECT和LOCK TABLES权限,最小权限原则。
rsync 的 --delete 要慎用
我用 rsync --delete 保证远端和本地一致,但这也意味着如果本地误删了备份文件,远端也会被删。所以保留策略我分了两套:本地 7 天,远端 30 天。而且远端清理是通过 ssh 单独执行的 find 命令,不会受 --delete 影响。
远端服务器要独立
我专门用了一台便宜的存储型 VPS 当备份机,和主站不在同一个服务商。这样就算主站服务商出问题,备份也还在。这种异地备份的思想很重要,不要所有鸡蛋放一个篮子。
设置定时任务
脚本写好后,用 crontab 每天凌晨 2 点执行:
1crontab -e
添加:
10 2 * * * /root/scripts/backup.sh > /dev/null 2>&1
或者把输出重定向到日志文件:
10 2 * * * /root/scripts/backup.sh >> /var/log/backup.log 2>&1
注意脚本本身已经写了日志,这里可以简单一点。
定期恢复演练
备份不是终点,能恢复才有意义。我每隔一两个月会从备份包里解压一次,检查一下文件是否完整,数据库能不能正常导入。
测试数据库恢复的命令:
1mysql -u dbuser -p test_db < database.sql
如果从来不测试恢复,真到用的时候才发现备份文件损坏,那就完了。
总结
这个备份脚本虽然看起来不复杂,但已经能满足我个人博客和小型项目的备份需求。核心思路就是:
- 本地打包 + 异地同步
- 分级保留策略
- 关键步骤报错处理
- 定期验证恢复能力
数据无价,备份不是可选项,而是必修课。希望大家都能养成备份的好习惯,不要等到数据丢了才后悔。