Ubuntu 服务器互备实战:安全创建备份账号,用 SCP 定时备份静态资源
Ubuntu 服务器互备实战:安全创建备份账号,用 SCP 定时备份静态资源 做服务器ã
Ubuntu 服务器互备实战:安全创建备份账号,用 SCP 定时备份静态资源
做服务器备份时,很多人第一反应都是:
“直接用 root 跑个脚本,把文件传到另一台机器不就行了?”
这事当然能做,但问题也很明显:
- 直接用 root 互传,风险太大
- 备份账号和运维账号混用,权限边界很模糊
- 时间久了,目录、密钥、脚本、日志会越来越乱
- 一旦出问题,排查和恢复都会很痛苦
前段时间我正好在整理一套轻量级的服务器互备方案,场景比较简单:
把静态资源每天打成压缩包,传到另一台服务器,保留最近 3 天。
不搞太重,也不引入复杂备份系统,但有几个原则必须守住:
- 远端不能直接用 root 接收
- 要用专门的低权限账号
- 只允许 SSH 密钥登录
- 本地和远端都要有清理策略
- 备份文件不能永远只覆盖一个名字
这篇就把这套方案完整整理一下,适合个人项目、小型服务,或者你自己有几台服务器想做简单互备时参考。
一、为什么不要直接用 root 做互备
先说结论:
本地用 root 执行备份任务没问题,但远端不应该直接让 root 接收。
这两个角色要分开看。
本地为什么可以用 root
因为备份动作本身,往往需要读很多普通用户读不到的内容,比如:
- Docker 数据目录
- 配置文件
- 应用目录
- 证书文件
- 数据库导出结果
所以本地的定时任务用 root 跑,其实很常见,也很合理。
远端为什么不建议用 root
因为远端一旦允许 root 接收文件,本质上你就把一把高权限钥匙长期暴露出来了。
如果私钥泄露、脚本写错、权限没收紧,后果会很难看。
所以一个更稳的思路是:
- 源服务器:root 负责打包和上传
- 目标服务器:专门用一个低权限账号接收备份
这样权限边界就清楚很多。
二、别拿系统自带的 backup 用户来用
一开始我本来想直接创建一个叫 backup 的用户,结果发现系统里已经有了:
id backup
getent passwd backup
输出大概是这样:
uid=34(backup) gid=34(backup) groups=34(backup)
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
这说明它不是我自己创建的业务用户,而是 Ubuntu 系统自带的一个系统账号:
- UID 很低,是系统保留用户
- home 在
/var/backups - shell 是
nologin - 默认锁定密码
这类用户不要乱改,也不要直接拿来当你的备份账号。
最稳的做法是:
自己新建一个用途明确的账号。
我最后用的是:
backupsvc
这个名字的好处就是,一眼就知道它是干什么的,也不容易跟系统账号撞名。
三、创建专用备份账号
在目标服务器上执行:
sudo groupadd backupsvc
sudo useradd -m -d /home/backupsvc -s /bin/bash -g backupsvc backupsvc
sudo passwd -l backupsvc
然后检查一下:
id backupsvc
getent passwd backupsvc
sudo passwd -S backupsvc
理想状态是:
backupsvc是普通用户- 不属于
sudo、docker之类高权限组 - home 目录是
/home/backupsvc - 密码状态是
L,表示已锁定
这里锁定密码很重要。
因为这个账号的设计目标就是:只允许 SSH 密钥登录,不允许密码登录。
四、准备 SSH 目录和密钥登录
接着在目标服务器上准备 .ssh 目录:
sudo -u backupsvc mkdir -p /home/backupsvc/.ssh
sudo -u backupsvc chmod 700 /home/backupsvc/.ssh
sudo -u backupsvc touch /home/backupsvc/.ssh/authorized_keys
sudo -u backupsvc chmod 600 /home/backupsvc/.ssh/authorized_keys
sudo chown -R backupsvc:backupsvc /home/backupsvc/.ssh
权限建议一定要对:
.ssh目录:700authorized_keys:600
否则 SSH 经常会因为权限不安全直接拒绝登录。
五、在源服务器生成专用 SSH 密钥
这一点也很重要:
不要复用平时运维登录服务器的私钥。
备份就是备份,最好单独一套密钥。
在源服务器上执行:
ssh-keygen -t ed25519 -f ~/.ssh/backup_to_target_ed25519 -C "backup-to-target"
会生成:
- 私钥:
~/.ssh/backup_to_target_ed25519 - 公钥:
~/.ssh/backup_to_target_ed25519.pub
查看公钥内容:
cat ~/.ssh/backup_to_target_ed25519.pub
把这一整行复制到目标服务器的:
/home/backupsvc/.ssh/authorized_keys
六、先别急着上定时任务,先测通 SSH
这一步非常关键。
很多人上来就写 cron,结果后面排查半天,发现根本不是脚本问题,是 SSH 没通。
在源服务器测试:
ssh -i ~/.ssh/backup_to_target_ed25519 backupsvc@192.168.0.96 'whoami'
如果返回:
backupsvc
说明密钥登录已经正常了。
只有这一步确认无误,后面的备份脚本才有意义。
七、备份目录怎么放
这次我的场景很明确:
备份的是静态资源。
所以我不打算上太复杂的增量同步、快照、去重方案,而是走最简单的一套:
- 每天打一个压缩包
- 传到远端
- 保留最近 3 天
目标服务器上的备份目录我放在:
/home/backupsvc/backups
先创建出来:
sudo -u backupsvc mkdir -p /home/backupsvc/backups
chmod 700 /home/backupsvc/backups
这里要特别注意一个很容易犯的错误:
目录名我最终用的是 backups,不是 backup。
别小看这个小坑,脚本里少一个 s,scp 就会直接失败。
八、为什么我没有继续用固定文件名覆盖
最开始我写脚本时,想的是每天生成一个:
manager.tar.gz
然后直接覆盖上传。
后来一想,这个方案其实不太像“备份”,更像“远程复制”。
它的问题很明显:
- 没有历史版本
- 今天资源如果已经坏了,明天会把坏状态继续覆盖到远端
- 恢复时没有回滚点
- 单看文件名也不知道是哪天生成的
所以最后我改成了按日期命名:
manager_2026-04-23.tar.gz
这样才有“保留最近 3 天”的意义。
九、为什么我没有直接上 rsync,而是用 SCP
说实话,如果是目录级同步,rsync 更适合。
但这次我的需求很简单:
- 每天打一个压缩包
- 上传到另一台机器
- 不需要增量目录同步
- 不需要远端实时镜像
这种情况下,scp 就够用了。
它的优点是:
- 简单
- 易懂
- 不用额外想太多同步策略
- 非常适合“每天传一个 tar.gz”
如果你也是类似场景,没必要为了“更高级”硬上复杂方案。
简单、稳定、可维护,比看起来高级更重要。
十、最终使用的备份脚本
下面是我最后整理出来的版本。
这个脚本会做几件事:
- 把指定目录打成当天的
tar.gz - 上传到备份服务器
- 本地清理 3 天前的旧包
- 远端也清理 3 天前的旧包
- 日志写到本地
backup.log
#!/bin/bash
set -euo pipefail
## 要备份的源目录
MANAGER_CODE_DIR='/home/docker_data/hbgd_manager'
## 本地备份包存放目录
BACKUP_DIR='/home/docker_data/backup/back_data'
## 备份名前缀
FILENAME='manager'
## 当天备份文件名
TODAY_FILE_NAME="${FILENAME}_$(date '+%F')"
ARCHIVE_FILE="$BACKUP_DIR/$TODAY_FILE_NAME.tar.gz"
## 远端配置
SSH_KEY='/root/.ssh/backup_to_target_ed25519'
DEST_USER='backupsvc'
DEST_HOST='192.168.0.96'
DEST_DIR='/home/backupsvc/backups'
## 日志文件
LOG_FILE="$BACKUP_DIR/backup.log"
{
echo "[$(date '+%F %T')] 备份开始"
# 基础检查
[ -d "$MANAGER_CODE_DIR" ] || { echo "源目录不存在: $MANAGER_CODE_DIR"; exit 1; }
[ -d "$BACKUP_DIR" ] || { echo "本地备份目录不存在: $BACKUP_DIR"; exit 1; }
[ -f "$SSH_KEY" ] || { echo "SSH 私钥不存在: $SSH_KEY"; exit 1; }
echo "[$(date '+%F %T')] 开始压缩: $MANAGER_CODE_DIR"
# 生成当天压缩包
rm -f "$ARCHIVE_FILE"
tar czf "$ARCHIVE_FILE" -C "$(dirname "$MANAGER_CODE_DIR")" "$(basename "$MANAGER_CODE_DIR")"
echo "[$(date '+%F %T')] 压缩完成: $ARCHIVE_FILE"
# 确保远端目录存在
ssh -i "$SSH_KEY" -o BatchMode=yes "${DEST_USER}@${DEST_HOST}" "mkdir -p '$DEST_DIR'"
echo "[$(date '+%F %T')] 开始上传到远端"
scp -i "$SSH_KEY" -o BatchMode=yes \
"$ARCHIVE_FILE" \
"${DEST_USER}@${DEST_HOST}:${DEST_DIR}/"
echo "[$(date '+%F %T')] 上传完成"
# 清理本地 3 天前备份
find "$BACKUP_DIR" -type f -name "${FILENAME}_*.tar.gz" -mtime +3 -delete
# 清理远端 3 天前备份
ssh -i "$SSH_KEY" -o BatchMode=yes "${DEST_USER}@${DEST_HOST}" \
"find '$DEST_DIR' -type f -name '${FILENAME}_*.tar.gz' -mtime +3 -delete"
echo "[$(date '+%F %T')] 清理完成"
echo "[$(date '+%F %T')] 备份结束"
} >> "$LOG_FILE" 2>&1
十一、这个脚本里几个值得注意的点
1)用的是 bash,不是 sh
我最后直接用了:
#!/bin/bash
因为脚本里有:
set -euo pipefail
这能显著提升脚本执行的安全性和稳定性。
很多 shell 脚本出问题,不是逻辑太复杂,而是前面失败了后面还在继续跑。
加上这行后,问题会少很多。
2)tar 用了 -C
我没有直接这样写:
tar czf xxx.tar.gz /home/docker_data/hbgd_manager
而是写成:
tar czf "$ARCHIVE_FILE" -C "$(dirname "$MANAGER_CODE_DIR")" "$(basename "$MANAGER_CODE_DIR")"
这样压缩包里的目录层级更干净,不会把整条绝对路径一起打进去。
后面恢复时更顺手。
3)上传前先确保远端目录存在
这个小动作很值钱:
ssh -i "$SSH_KEY" -o BatchMode=yes "${DEST_USER}@${DEST_HOST}" "mkdir -p '$DEST_DIR'"
很多人默认远端目录一定存在,结果第一次跑脚本就失败。
我更喜欢把这一步写进脚本里,省心。
4)日志一定要落盘
脚本所有输出我都统一写到了:
/home/docker_data/backup/back_data/backup.log
这样 cron 出问题时,不需要靠猜,直接翻日志就行。
十二、脚本怎么部署
假设你把脚本放到:
/root/scripts/manager_backup.sh
那就这样操作:
mkdir -p /root/scripts
nano /root/scripts/manager_backup.sh
chmod 700 /root/scripts/manager_backup.sh
chmod 600 /root/.ssh/backup_to_target_ed25519
私钥权限一定要收紧,不然后面 SSH 可能会因为“不安全”直接拒绝使用。
十三、不要直接上 cron,先手工执行一次
这一步我强烈建议做。
先手工跑一次:
/root/scripts/manager_backup.sh
然后分别检查:
本地备份包
ls -lh /home/docker_data/backup/back_data/
远端备份目录
ssh -i /root/.ssh/backup_to_target_ed25519 backupsvc@192.168.0.96 'ls -lh /home/backupsvc/backups/'
日志输出
tail -f /home/docker_data/backup/back_data/backup.log
先手工跑通,再上定时任务,能省掉你后面很多排查时间。
十四、最后加上 cron
确认手工执行没问题后,再加 root 的 crontab:
crontab -e
例如每天凌晨 2:30 执行:
30 2 * * * /root/scripts/manager_backup.sh
因为脚本内部已经自己写日志了,所以这里不用额外重定向。
十五、这套方案适合谁
这套方案我觉得很适合下面这些场景:
- 静态资源备份
- 个人项目
- 小型网站
- 两三台服务器之间互备
- 想要简单、直接、可维护的方案
它不是那种企业级重备份系统,但对很多实际场景来说,已经完全够用了。
如果以后你的需求升级,比如:
- 数据量非常大
- 需要异地多副本
- 需要加密去重
- 要保留更长历史版本
那时候再考虑 rsync、restic、borg、对象存储这些更完整的方案也不迟。
十六、最后总结
这次做完之后,我自己的感受很明确:
备份最怕的,不是“脚本写不出来”,而是权限边界不清、历史版本没有、出了问题找不到原因。
所以这套方案里,我最看重的其实不是 scp 本身,而是下面这些原则:
- 不乱用系统自带账号
- 自建专用低权限账号
- 远端只允许密钥登录
- 本地和远端职责分离
- 备份文件按日期命名
- 本地和远端都有清理策略
- 先手工跑通,再上 cron
这些基础动作做好了,哪怕方案本身很简单,也已经比“直接拿 root + 一个固定 tar.gz 覆盖上传”强得多。
如果你现在也正好在做轻量级服务器互备,这套方案可以直接拿去改。
相关文章
devops
winscp 通过 .pem 文件连接 aws 服务器
winscp 通过 .pem 文件连接 aws 服务器 点击新建站点,然后逐一输入信息
devops
使用 acme.sh 手动 DNS 申请通配符免费证书并导出证书文件
使用 acme.sh 手动 DNS 申请通配符证书并导出证书文件 一、背景 这次需要给一
database
Public Key Retrieval is not allowed
刚刚安装好mysql8,使用dbeaver工具链接提示Public Key Retrieval is not allowed 原因: 这个错误 "Public Key Retrieval is not allowed" 通常发生在连接工具(如 DBeaver, Navicat, Workbench)
这篇文章有帮助吗?
感谢反馈。