跳到正文
runningbai
Powered by Pagefind
服务器与运维

Ubuntu 服务器互备实战:安全创建备份账号,用 SCP 定时备份静态资源

Ubuntu 服务器互备实战:安全创建备份账号,用 SCP 定时备份静态资源 做服务器ã

2026/4/23 阿白 最后更新: 2026/4/23 17 分钟阅读

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 是普通用户
  • 不属于 sudodocker 之类高权限组
  • 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 目录:700
  • authorized_keys600

否则 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”

如果你也是类似场景,没必要为了“更高级”硬上复杂方案。
简单、稳定、可维护,比看起来高级更重要。


十、最终使用的备份脚本

下面是我最后整理出来的版本。

这个脚本会做几件事:

  1. 把指定目录打成当天的 tar.gz
  2. 上传到备份服务器
  3. 本地清理 3 天前的旧包
  4. 远端也清理 3 天前的旧包
  5. 日志写到本地 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

因为脚本内部已经自己写日志了,所以这里不用额外重定向。


十五、这套方案适合谁

这套方案我觉得很适合下面这些场景:

  • 静态资源备份
  • 个人项目
  • 小型网站
  • 两三台服务器之间互备
  • 想要简单、直接、可维护的方案

它不是那种企业级重备份系统,但对很多实际场景来说,已经完全够用了。

如果以后你的需求升级,比如:

  • 数据量非常大
  • 需要异地多副本
  • 需要加密去重
  • 要保留更长历史版本

那时候再考虑 rsyncresticborg、对象存储这些更完整的方案也不迟。


十六、最后总结

这次做完之后,我自己的感受很明确:

备份最怕的,不是“脚本写不出来”,而是权限边界不清、历史版本没有、出了问题找不到原因。

所以这套方案里,我最看重的其实不是 scp 本身,而是下面这些原则:

  • 不乱用系统自带账号
  • 自建专用低权限账号
  • 远端只允许密钥登录
  • 本地和远端职责分离
  • 备份文件按日期命名
  • 本地和远端都有清理策略
  • 先手工跑通,再上 cron

这些基础动作做好了,哪怕方案本身很简单,也已经比“直接拿 root + 一个固定 tar.gz 覆盖上传”强得多。

如果你现在也正好在做轻量级服务器互备,这套方案可以直接拿去改。

相关文章

这篇文章有帮助吗?