Linux VPS 最穩妥的備份方式是:每天用 restic 把加密快照傳送到伺服器之外的儲存,設定清晰的保留策略,並定期測試恢復。本文在 Debian 12/13、Ubuntu 22.04/24.04 和 Rocky Linux / AlmaLinux 上完成整套配置:安裝 restic、選擇異地倉庫(S3 相容儲存、SFTP 或 rclone)、匯出資料庫、用 systemd 定時器自動執行,並驗證資料確實能恢復。
為什麼備份必須放在 VPS 之外
放在同一臺伺服器上的副本不算備份:任何刪除伺服器的操作都會把副本一起刪掉。在 JUSTG 雲伺服器上,以下幾種正常操作都會永久刪除資料:
- 更換機房(Change location):系統會新裝一臺 VPS,分配新 IP,不復制任何資料。新 VPS 就緒後,舊 VPS 及其全部資料會被刪除。
- 逾期未續費:未付款的服務會在到期日自動暫停;到期 7 天后帳單仍未支付,服務會被取消,資料隨之刪除。
- 重灌系統會清空硬碟;選擇 Immediate 方式取消服務,請求處理後服務和資料會立即刪除。
再加上誤敲的 rm -rf、失敗的升級或伺服器被入侵,結論很簡單:唯一可靠的備份,是你自己做、並存放在別處的備份。在更換機房、重灌系統或取消服務之前,請先手動執行一次備份,並確認新快照已經生成。
步驟 1:安裝 restic
Debian 12/13、Ubuntu 24.04 和 EPEL 自帶的版本已經足夠新:
# Debian 12/13, Ubuntu 24.04
apt update && apt install -y restic
restic version
# Rocky Linux / AlmaLinux 8, 9 (from EPEL)
dnf install -y epel-release && dnf install -y restic
Ubuntu 22.04 自帶的是 restic 0.12,不支援壓縮,也無法使用當前的倉庫格式。在這個系統上,請安裝官方二進位制檔案並校驗 checksum:
# Official static binary (Ubuntu 22.04 or any distro with an old restic)
VER=0.18.1 # check https://github.com/restic/restic/releases for the latest version
cd /tmp
curl -fLO https://github.com/restic/restic/releases/download/v${VER}/restic_${VER}_linux_amd64.bz2
curl -fLO https://github.com/restic/restic/releases/download/v${VER}/SHA256SUMS
sha256sum --ignore-missing -c SHA256SUMS # must print: OK
bunzip2 restic_${VER}_linux_amd64.bz2
install -m 0755 restic_${VER}_linux_amd64 /usr/local/bin/restic
restic version
步驟 2:選擇異地倉庫
倉庫就是存放加密快照的地方。任選一種方案,儲存為 /etc/restic/env(每行一個 NAME=value)。
方案 A:S3 相容物件儲存
任何提供 S3 API 的服務商都可以。建立一個儲存桶和只限該儲存桶的訪問金鑰,並在 URL 中填寫服務商的 endpoint:
# /etc/restic/env (S3-compatible object storage)
RESTIC_REPOSITORY=s3:https://s3.example.com/example-backups/vps1
RESTIC_PASSWORD_FILE=/etc/restic/password
RESTIC_CACHE_DIR=/var/cache/restic
AWS_ACCESS_KEY_ID=REPLACE_WITH_ACCESS_KEY
AWS_SECRET_ACCESS_KEY=REPLACE_WITH_SECRET_KEY
# Optional: monitoring ping URL (Step 11)
HC_PING_URL=
方案 B:透過 SFTP 備份到另一臺伺服器
使用你自己的另一臺伺服器,最好位於不同機房或不同服務商,並建立專用的 backup 使用者,由它擁有 /srv/restic:
# /etc/restic/env (SFTP to a second server)
RESTIC_REPOSITORY=sftp:[email protected]:/srv/restic/vps1
RESTIC_PASSWORD_FILE=/etc/restic/password
RESTIC_CACHE_DIR=/var/cache/restic
HC_PING_URL=
# One-time SSH key login for root on the VPS
ssh-keygen -t ed25519 -N "" -f /root/.ssh/id_ed25519 # skip if the key exists
ssh-copy-id [email protected]
ssh [email protected] true # accept the host key once
方案 C:rclone 後端
rclone 能讓 restic 連線幾十種儲存服務(Backblaze B2、Google Drive、OneDrive、WebDAV 等):
# Install and configure rclone (interactive wizard, run as root)
apt install -y rclone # Rocky/Alma: dnf install -y rclone (EPEL)
rclone config # create a remote, for example named "offsite"
# /etc/restic/env (any rclone backend)
RESTIC_REPOSITORY=rclone:offsite:example-backups/vps1
RESTIC_PASSWORD_FILE=/etc/restic/password
RESTIC_CACHE_DIR=/var/cache/restic
RCLONE_CONFIG=/root/.config/rclone/rclone.conf
HC_PING_URL=
/etc/restic/env 裡的憑據通常也有刪除備份的許可權,攻擊者一旦控制 VPS,就可能清空倉庫。想要更強的保護,可以為儲存桶開啟版本控制或 object lock,或使用只能追加寫入的目標,並且不要把儲存帳號的登入密碼放在伺服器上。步驟 3:建立密碼檔案並初始化倉庫
install -d -m 0700 /etc/restic
openssl rand -base64 32 > /etc/restic/password
chmod 600 /etc/restic/password
nano /etc/restic/env # paste one of the examples above
chmod 600 /etc/restic/env
# Load the settings into the current shell and create the repository
set -a; . /etc/restic/env; set +a
restic init
/etc/restic/password 和 /etc/restic/env 複製到密碼管理器或伺服器以外的安全位置:VPS 一旦不在了,恢復只需要這兩個檔案。步驟 4:確定備份哪些內容、排除哪些內容
只備份配置和資料,不必備份整個作業系統:重灌系統比恢復系統檔案更快,快照也更小。
# /etc/restic/include - one path per line; delete lines that do not exist
/etc
/root
/home
/srv
/var/www
/opt
/usr/local
/var/spool/cron
/var/backups/db
# /etc/restic/excludes
/root/.cache
/home/*/.cache
**/node_modules
**/*.tmp
/var/www/*/cache
- 需要備份:
/etc、使用者主目錄、網站根目錄、應用資料、crontab 以及資料庫匯出目錄。 - 無需備份:
/proc、/sys、/dev、/run、/tmp、swap 檔案和各類快取。它們不在列表裡,restic 根本不會讀取。 - 資料庫原始檔案(
/var/lib/mysql、/var/lib/postgresql)在資料庫執行時直接複製,可能得到不一致的副本。請備份匯出檔案(步驟 5)。 - Docker:不要整體備份
/var/lib/docker,應備份 compose 檔案以及存放資料的卷或目錄。
步驟 5:每次備份前先匯出資料庫
下面的指令碼把壓縮後的匯出檔案寫入 /var/backups/db,該目錄已在備份列表中。請按伺服器實際情況設定 MYSQL 和 POSTGRES:
#!/bin/bash
# /usr/local/sbin/db-dump.sh - consistent database dumps before each backup
set -euo pipefail
umask 077
MYSQL=yes # yes / no
POSTGRES=no # yes / no
OUT=/var/backups/db
mkdir -p "$OUT"
if [ "$MYSQL" = yes ]; then
DUMP=$(command -v mariadb-dump || command -v mysqldump)
"$DUMP" --all-databases --single-transaction --quick \
--routines --events --triggers | gzip > "$OUT/mysql-all.sql.gz.tmp"
mv "$OUT/mysql-all.sql.gz.tmp" "$OUT/mysql-all.sql.gz"
fi
if [ "$POSTGRES" = yes ]; then
cd /tmp
runuser -u postgres -- pg_dumpall | gzip > "$OUT/postgres-all.sql.gz.tmp"
mv "$OUT/postgres-all.sql.gz.tmp" "$OUT/postgres-all.sql.gz"
fi
--single-transaction 能在不鎖住網站的情況下得到一致的 InnoDB 匯出。在 Debian 和 Ubuntu 上,root 透過本地 socket 連線 MariaDB/MySQL,無需密碼;如果你的伺服器需要密碼,請寫入 /root/.my.cnf(許可權 600),不要寫在命令列裡。
步驟 6:建立備份指令碼
一個指令碼完成資料庫匯出、備份、按策略清理和每週檢查,並向可選的監控 URL 彙報結果(步驟 10)。任何一步失敗都會讓指令碼報錯退出,問題不會被悄悄忽略:
#!/bin/bash
# /usr/local/sbin/restic-backup.sh - dump databases, back up, apply retention
set -euo pipefail
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
set -a; . /etc/restic/env; set +a
fail() {
[ -n "${HC_PING_URL:-}" ] && curl -fsS -m 10 --retry 3 "$HC_PING_URL/fail" >/dev/null || true
}
trap fail ERR
/usr/local/sbin/db-dump.sh
restic backup \
--files-from /etc/restic/include \
--exclude-file /etc/restic/excludes \
--exclude-caches \
--tag daily
restic forget --prune \
--keep-daily 7 --keep-weekly 4 --keep-monthly 6
# Sundays: verify the repository and read back 5% of the data
if [ "$(date +%u)" = 7 ]; then
restic check --read-data-subset=5%
fi
if [ -n "${HC_PING_URL:-}" ]; then
curl -fsS -m 10 --retry 3 "$HC_PING_URL" >/dev/null
fi
為指令碼加上執行許可權,並手動執行第一次備份。首次執行會上傳全部資料,之後只上傳變化部分。
chmod 700 /usr/local/sbin/db-dump.sh /usr/local/sbin/restic-backup.sh
/usr/local/sbin/restic-backup.sh # first run by hand, watch for errors
restic snapshots
步驟 7:用 systemd 定時器(或 cron)定時執行
推薦使用 systemd 定時器:日誌會寫入 journal,錯過的任務會在開機後補跑(Persistent=true),還能降低任務的 CPU 和磁碟優先順序,不拖慢網站。
# /etc/systemd/system/restic-backup.service
[Unit]
Description=restic backup to off-site repository
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/restic-backup.sh
Nice=10
IOSchedulingClass=idle
# /etc/systemd/system/restic-backup.timer
[Unit]
Description=Daily restic backup
[Timer]
OnCalendar=*-*-* 03:30:00
RandomizedDelaySec=30min
Persistent=true
[Install]
WantedBy=timers.target
systemctl daemon-reload
systemctl enable --now restic-backup.timer
systemctl list-timers restic-backup.timer
# Test the service once and read its log
systemctl start restic-backup.service
journalctl -u restic-backup.service -n 50 --no-pager
如果你更習慣 cron,可以用下面這行代替定時器(二者只選其一):
# /etc/cron.d/restic-backup (only if you do not use the systemd timer)
30 3 * * * root /usr/local/sbin/restic-backup.sh >> /var/log/restic-backup.log 2>&1
步驟 8:用 restic forget --prune 設定保留策略
restic forget 決定保留哪些快照,--prune 刪除已不被任何快照引用的資料。指令碼中的策略會保留:
| 引數 | 保留內容 |
|---|---|
--keep-daily 7 | 最近 7 天中每天最後一個快照 |
--keep-weekly 4 | 最近 4 周每週一個快照 |
--keep-monthly 6 | 最近 6 個月每月一個快照 |
啟用新策略之前,先用 --dry-run 預覽效果:
set -a; . /etc/restic/env; set +a
restic snapshots
# Preview what a policy would remove, without deleting anything
restic forget --dry-run --keep-daily 7 --keep-weekly 4 --keep-monthly 6
restic stats --mode raw-data
指令碼還會在每週日執行 restic check,檢查倉庫結構;--read-data-subset=5% 每次下載並校驗不同的 5% 資料,讓損壞在你真正需要備份之前就被發現。需要更放心時,也可以隨時手動執行 restic check。
步驟 9:測試恢復
從未恢復過的備份只是一種希望。每月一次,把真實資料恢復到臨時目錄並進行比對:
set -a; . /etc/restic/env; set +a
restic snapshots
restic ls latest /etc/nginx
# Restore one directory to a scratch location, then compare
restic restore latest --target /tmp/restore-test --include /etc/nginx
diff -r /etc/nginx /tmp/restore-test/etc/nginx && echo "restore OK"
# Check that the database dump is complete (last line: "-- Dump completed on ...")
restic dump latest /var/backups/db/mysql-all.sql.gz | zcat | tail -n 1
rm -rf /tmp/restore-test
最壞的情況也要演練一次,例如在一臺備用 VPS 上:一臺全新伺服器,手裡只有儲存好的那兩個檔案。
# On the NEW server: install restic, then restore the two saved files
install -d -m 0700 /etc/restic
nano /etc/restic/password && nano /etc/restic/env
set -a; . /etc/restic/env; set +a
restic restore latest --target /srv/restore
rsync -a /srv/restore/var/www/ /var/www/ # copy back what you need
zcat /srv/restore/var/backups/db/mysql-all.sql.gz | mariadb # or: mysql
restic restore latest --target /:它會覆蓋正在使用的檔案,包括可能不適合新伺服器的系統配置。請恢復到單獨的目錄,再把需要的內容複製回去。步驟 10:備份失敗時及時收到提醒
備份往往是悄無聲息地失敗的:金鑰過期、儲存桶滿了、密碼被改了。“dead man's switch”類服務可以解決這個問題:指令碼每次成功後訪問一個 URL,若沒有按時收到訪問,就會向你發出提醒。你可以自建(例如開源的 Healthchecks),也可以使用任何類似服務,然後把檢查 URL 寫入 /etc/restic/env:
HC_PING_URL=https://hc.example.com/ping/REPLACE-WITH-YOUR-CHECK-ID
指令碼成功時訪問該 URL,出錯時訪問 $HC_PING_URL/fail。把檢查週期設為一天,並留出幾個小時的寬限時間。如果不用這類服務,至少每週檢視一次 restic snapshots 和 journalctl -u restic-backup.service。
FAQ
需要多大的儲存空間?
首次快照大約等於你的資料量(壓縮後通常更小),之後每天只增加少量變化資料。執行幾周後執行 restic stats --mode raw-data,即可看到實際用量。
restic 提示倉庫被鎖定,怎麼辦?
說明上一次執行被中斷,留下了過期的鎖。先確認沒有 restic 程序在執行(pgrep -a restic),再執行 restic unlock,然後重新執行備份。
在 512 MB 方案上 restic 佔用記憶體太多,能降低嗎?
可以在 /etc/restic/env 中加入 GOGC=20,讓 restic 更頻繁地釋放記憶體;同時精簡備份路徑,並確認 VPS 已配置 swap。如果記憶體仍然不夠,可以在服務頁面升級方案。需要幫助請提交工單:JUSTG 提供 7×24 小時 AI 客服(工單),需要人工處理的問題會由工程師跟進。