前言

印度孟买某公司开发的 Virtualizor 面板,让不少 VPS 服务商都栽了跟头。从去年的 ColorCrossing 到最近的 CloudCone,黑客利用漏洞入侵服务器并进行勒索,导致大量用户数据全灭。

面对此类「黑天鹅事件」,谁也没法独善其身,是时候完善一下我的数据自动化备份方案了。

选择备份工具

几年前我一直使用 Duplicacy CLI,甚至还写过两篇又臭又长的教程。但它的开源许可证不够清晰,且已停更近一年,我开始寻找更好的替代品。我找到了:

最终的选择是 Kopia ,它支持增量快照、自带端到端 加密、且上手极其简单。

选择存储服务

Kopia 支持几乎所有主流的存储服务和协议:

  • S3 及 S3 兼容存储
  • 本机或网络附加存储
  • Azure Blob 存储
  • Backblaze B2 存储
  • Google Cloud 存储
  • Google 云端硬盘
  • WebDAV/SFTP/Rclone
  • ……

对象存储我选择了 Backblaze B2 按量计费计划 仅需 41.15 元/TB。A 类操作免费且每天赠送一定额度的 B/C 类操作次数,免费出口流量是 3 倍存储量。对于增量备份,成本几乎可以忽略不计了。

b2-pricing.webp

假设我每月存储 100GB,费用大约为 4.11 元/月左右,实际根本用不了这么多。

创建访问机密

在 B2 创建存储桶,在 Application Keys 里生成访问机密,记下 keyIDapplicationKey 即可开始配置。

在 Debian GNU/Linux 上,最省心的安装方式是添加官方的 APT 仓库。

# 导入 GPG 密钥
curl -s https://kopia.io/signing-key | sudo gpg --dearmor -o /etc/apt/keyrings/kopia-keyring.gpg
# 添加软件源
echo "deb [signed-by=/etc/apt/keyrings/kopia-keyring.gpg] http://packages.kopia.io/apt/ stable main" | sudo tee /etc/apt/sources.list.d/kopia.list
# 安装
sudo apt update && sudo apt install kopia
# 验证安装
kopia --version 

配置 Kopia

初始化存储库

执行 repository create 命令初始化存储库:

sudo kopia repository create b2 \
  --bucket=<存储桶名称> \
  --key-id=<keyID> \
  --key=<applicationKey> \
  --prefix=<目录前缀>/

务必牢记设置的密码,一旦遗忘,备份数据将彻底锁定,没有找回或重置密码的可能。

Enter password to create new repository:
Re-enter password for verification:
Initializing repository with:
  block hash:          BLAKE2B-256-128
  encryption:          AES256-GCM-HMAC-SHA256
  key derivation:      scrypt-65536-8-1
  splitter:            DYNAMIC-4M-BUZHASH
Connected to repository.

NOTICE: Kopia will check for updates on GitHub every 7 days, starting 24 hours after first use.
To disable this behavior, set environment variable KOPIA_CHECK_FOR_UPDATES=false
Alternatively you can remove the file "/root/.config/kopia/repository.config.update-info.json".

Retention:
  Annual snapshots:                 3   (defined for this target)
  Monthly snapshots:               24   (defined for this target)
  Weekly snapshots:                 4   (defined for this target)
  Daily snapshots:                  7   (defined for this target)
  Hourly snapshots:                48   (defined for this target)
  Latest snapshots:                10   (defined for this target)
  Ignore identical snapshots:   false   (defined for this target)
Compression disabled.

To find more information about default policy run 'kopia policy get'.
To change the policy use 'kopia policy set' command.

NOTE: Kopia will perform quick maintenance of the repository automatically every 1h0m0s
and full maintenance every 24h0m0s when running as root@vps-micro.

See https://kopia.io/docs/advanced/maintenance/ for more information.

NOTE: To validate that your provider is compatible with Kopia, please run:

$ kopia repository validate-provider

查看连接状态

sudo kopia repository status

设置全局策略

设置全局压缩算法,推荐 zstd 选项,平衡压缩率和速度

sudo kopia policy set --global --compression=zstd

配置全局保留策略,作为备份项目的回退配置

# 保留最新的 8 份快照
sudo kopia policy set --global --keep-latest 8

检查全局策略是否生效

sudo kopia policy list

自动化备份

/home/dejavu/warp 下的 Docker 容器为例,该容器使用了绑定挂载。

# 细致化单个备份项目保留策略
# 对于配置基本不动的容器,保留 3 份快照足矣
sudo kopia policy set /home/dejavu/warp \
  --keep-latest 3 \
  --keep-hourly 0 \
  --keep-daily 0 \
  --keep-weekly 0 \
  --keep-monthly 0 \
  --keep-annual 0

排除无关文件

通过 .kopiaignore 文件,排除掉日志、缓存等无用数据,比如:

# 忽略日志文件
*.log
logs/
# 忽略临时目录
tmp/
temp/
# 排除缓存
.cache/

检查具体备份项目的策略

sudo kopia policy get /home/dejavu/warp

自动化脚本

Actions 是 Kopia 内置的一种 钩子(Hooks)机制。能在快照执行前后自动触发命令,但对我而言,这种「黑盒」机制调试起来不够直观,本文不使用该方式。

我选择编写一个简单的 Shell 脚本,配合 Crontab 定时执行。

# 创建日志目录
sudo mkdir -p /root/kopia/logs
# 创建备份脚本
sudo vim /root/kopia/backup-warp.sh

示例脚本如下:

#!/bin/bash

SRC_DIR="/home/dejavu/warp"
LOG_DIR="/root/kopia/logs"
CURRENT_DATE=$(date +%Y%m%d)
LOG_FILE="$LOG_DIR/${CURRENT_DATE}-warp.log"

# 必须修改 Kopia 存储库密码(使用单引号包裹)
export KOPIA_PASSWORD=''

# [可选] 禁用 Kopia 检查更新(适合国内机器)
# export KOPIA_CHECK_FOR_UPDATES=false

log() {
    echo "[$(date '+%H:%M:%S')] $1" >> "$LOG_FILE"
}

# 回退
fallback_service() {
    # 检查容器状态
    if ! docker compose -f "$SRC_DIR/compose.yml" ps --services --filter "status=running" | grep -q "warp"; then
        log "恢复容器启动..."
        docker compose -f "$SRC_DIR/compose.yml" up -d >> "$LOG_FILE" 2>&1
    fi
}
trap fallback_service EXIT

log "=== 备份开始 ==="

cd "$SRC_DIR" || { log "致命错误:找不到目录 $SRC_DIR"; exit 1; }

log "1. 停止容器..."
docker compose down >> "$LOG_FILE" 2>&1
if [ $? -ne 0 ]; then
    log "❌ 容器停止失败,跳过备份以保数据安全。"
    exit 1
fi

# 4.3 创建快照 (耗时操作)
log "开始创建快照..."
kopia snapshot create "$SRC_DIR" --description "Weekly Backup" >> "$LOG_FILE" 2>&1
SNAPSHOT_STATUS=$?

# 启动服务
log "3. 正在恢复服务..."
docker compose up -d >> "$LOG_FILE" 2>&1

if [ $SNAPSHOT_STATUS -eq 0 ]; then
    log "✅ 快照创建成功"
else
    log "❌ 快照创建失败!"
    exit 1
fi

# 维护任务
log "执行存储库维护 (GC)..."
kopia maintenance run --full >> "$LOG_FILE" 2>&1

log "=== 备份结束 ==="

设置脚本权限

sudo chmod 700 /root/kopia/backup-warp.sh

手动执行一次

sudo /root/kopia/backup-warp.sh

检查首次运行生成的日志

cat /root/kopia/logs/20260201-warp.log

查看了 B2 存储桶,快照已同步,日志输出也符合预期:

[10:26:27] === 备份开始 ===
[10:26:27] 1. 停止容器...
 Container warp Stopping
 Container warp Stopped
 Container warp Removing
 Container warp Removed
 Network warp-tunnel Removing
 Network warp-tunnel Resource is still in use
[10:26:37] 开始创建快照...
Snapshotting root@vps-micro:/home/dejavu/warp ...
 * 0 hashing, 40 hashed (23.4 MB), 0 cached (0 B), uploaded 193 B, estimated 23.4 MB (100.0%) 0s left
Created snapshot with root ka463aa955f638a00aed18d636e77e60c and ID 72c3275ef74463396ad6092d49c84ff0 in 1s
Running quick maintenance...
Compacting an eligible uncompacted epoch...
Advancing epoch markers...
Finished quick maintenance.
[10:26:46] 3. 正在恢复服务...
 Container warp Creating
 Container warp Created
 Container warp Starting
 Container warp Started
[10:26:47] ✅ 快照创建成功
[10:26:47] 执行存储库维护 (GC)...
Running full maintenance...
GC found 0 unused contents (0 B)
GC found 0 unused contents that are too recent to delete (0 B)
GC found 47 in-use contents (1.3 MB)
GC found 5 in-use system-contents (2.7 KB)
GC undeleted 0 contents (0 B)
Compacting an eligible uncompacted epoch...
Advancing epoch markers...
Attempting to compact a range of epoch indexes ...
Cleaning up unneeded epoch markers...
Cleaning up old index blobs which have already been compacted...
Cleaned up 0 logs.
Finished full maintenance.
[10:26:56] === 备份结束 ===

备份自动通知 [可选]

这是个可选配置,但感觉蛮实用的。我们使用 Apprise 推送通知,下面以 Telegram 为例。

使用 Docker Compose 启动一个 Apprise 服务

services:
  apprise:
    image: caronc/apprise:latest
    container_name: apprise
    restart: unless-stopped
    user: "1000"
    networks:
      - apprise
    environment:
      APPRISE_STATEFUL_MODE: simple
      APPRISE_WORKER_COUNT: "1"
    volumes:
      - ./config:/config
      - ./plugin:/plugin
      - ./attach:/attach
networks:
  apprise:
    name: apprise
    driver: bridge
    enable_ipv6: true

修改自动化脚本

#!/bin/bash

SRC_DIR="/home/dejavu/warp"
LOG_DIR="/root/kopia/logs"
CURRENT_DATE=$(date +%Y%m%d)
LOG_FILE="$LOG_DIR/${CURRENT_DATE}-warp.log"

# ================= 配置区域 =================
# Kopia 存储库密码
export KOPIA_PASSWORD=''

# [可选] 禁用 Kopia 检查更新
# export KOPIA_CHECK_FOR_UPDATES=false

# Apprise 通知配置
NOTIFICATION_URL="tgram://<botToken>/<group>/"
APPRISE_NET="apprise"
APPRISE_API="http://apprise:8000/notify"
# ===========================================

log() {
    echo "[$(date '+%H:%M:%S')] $1" >> "$LOG_FILE"
}

# 发送通知函数
send_notify() {
    local STATUS_ICON=$1
    local STATUS_MSG=$2
    local DETAIL_MSG=$3
    local DATE_STR=$(date +%Y-%m-%d)
    
    local TITLE="Kopia备份 - Warp"
    local BODY="${STATUS_ICON} ${DATE_STR}-warp-${STATUS_MSG}\n\n${DETAIL_MSG}"

    local JSON_PAYLOAD=$(cat <<EOF
{
    "urls": "$NOTIFICATION_URL",
    "title": "$TITLE",
    "body": "$BODY"
}
EOF
)

    log "正在发送通知..."
    docker run --rm --network "$APPRISE_NET" curlimages/curl:8.18.0 \
        -s -o /dev/null -X POST \
        -H "Content-Type: application/json" \
        -d "$JSON_PAYLOAD" \
        "$APPRISE_API"
}

# 回退函数
fallback_service() {
    if ! docker compose -f "$SRC_DIR/compose.yml" ps --services --filter "status=running" | grep -q "warp"; then
        log "恢复容器启动..."
        docker compose -f "$SRC_DIR/compose.yml" up -d >> "$LOG_FILE" 2>&1
    fi
}
trap fallback_service EXIT

log "=== 备份开始 ==="

cd "$SRC_DIR" || { log "致命错误:找不到目录 $SRC_DIR"; exit 1; }

log "1. 停止容器..."
docker compose down >> "$LOG_FILE" 2>&1
if [ $? -ne 0 ]; then
    log "❌ 容器停止失败,跳过备份以保数据安全。"
    send_notify "❌" "备份中断" "原因:容器无法正常停止,未执行备份。"
    exit 1
fi

# 创建快照
log "开始创建快照..."
# 捕获输出以便在通知中显示部分信息
SNAPSHOT_OUTPUT=$(kopia snapshot create "$SRC_DIR" --description "Weekly Backup" 2>&1 | tee -a "$LOG_FILE")
SNAPSHOT_STATUS=${PIPESTATUS[0]} 

# 启动服务
log "正在恢复服务..."
docker compose up -d >> "$LOG_FILE" 2>&1

# 获取单纯的日志文件名 (例如: 20260202-warp.log)
LOG_FILENAME=$(basename "$LOG_FILE")

if [ $SNAPSHOT_STATUS -eq 0 ]; then
    log "✅ 快照创建成功"
    
    # 提取快照 ID
    SNAP_ID=$(grep "Created snapshot with root" "$LOG_FILE" | tail -n 1 | awk '{print $5}')
    [ -z "$SNAP_ID" ] && SNAP_ID="ID提取异常"
    
    # === 修改点:这里的日志位置变量改为了 $LOG_FILENAME ===
    send_notify "✅" "备份成功!" "快照ID: $SNAP_ID\n服务已恢复运行。\n日志位置: $LOG_FILENAME"
else
    log "❌ 快照创建失败!"
    # 失败时也只显示文件名,保持整洁
    send_notify "❌" "备份失败!" "Kopia 返回了错误代码。\n请立即检查服务器日志: $LOG_FILENAME"
    exit 1
fi

# 维护任务
log "执行存储库维护 (GC)..."
kopia maintenance run --full >> "$LOG_FILE" 2>&1

log "=== 备份结束 ==="

示例效果:

Kopia备份 - Warp
✅ 2026-02-02-warp-备份成功!
快照ID: ke5e0d06612cb9850a1172cf0242983fb
服务已恢复运行。
日志位置: 20260202-warp.log

配置定时任务

通过 Crontab 自动化执行任务

sudo crontab -e

末尾添加一项任务

# 由于我的服务器时区设置为 UTC 世界协调时
# 预期:北京时间每周一的 01:00 执行
0 17 * * 0 /bin/bash /root/kopia/backup-warp.sh

恢复备份

新建一个测试目录,验证一遍恢复流程。

mkdir -p /tmp/warp_test_restore

常规恢复

如果需要找回最近的数据,直接执行:

# 格式:snapshot restore <源路径> <恢复目标>
sudo kopia snapshot restore /home/dejavu/warp /tmp/warp_test_restore/latest
# 验证恢复
ls -lah /tmp/warp_test_restore/latest

回溯到特定时间点的历史快照版本:

# 查看快照历史
sudo kopia snapshot list /home/dejavu/warp
# 示例输出:
root@vps-micro:/home/dejavu/warp
  2026-02-01 10:26:40 UTC ka463aa955f638a00aed18d636e77e60c 23.4 MB drwxrwxr-x files:40 dirs:6 (latest-1)
# 使用 ID 指定快照版本
sudo kopia snapshot restore ka463aa955f638a00aed18d636e77e60c /tmp/warp_test_restore/history
# 验证恢复
ls -lah /tmp/warp_test_restore/history

目前,看起来一切都工作的很好。

灾难恢复

现在我们要考虑极端情况,若服务器数据被完全损坏,如何在新环境下进行灾难恢复呢?

  1. 在新机器上安装 Kopia

  2. 使用相同的 B2 配置连接现有的存储桶

    sudo kopia repository connect b2 \
      --bucket=<存储桶名称> \
      --key-id=<keyID> \
      --key=<applicationKey> \
      --prefix=<目录前缀>/
    
  3. 新机器的主机名可能不同,需查看所有已存在快照

    sudo kopia snapshot list --all
    # 找到之前机器的<用户>@<主机名>:<备份目录>
    root@vps-micro:/home/dejavu/warp
      2026-02-01 10:26:40 UTC ka463aa955f638a00aed18d636e77e60c 23.4 MB drwxrwxr-x files:40 dirs:6 (latest-1)
    
  4. 恢复数据

    # 用法:snapshot restore <用户>@<旧主机名>:<原路径> <目标路径>
    sudo kopia snapshot restore ka463aa955f638a00aed18d636e77e60c root@vps-micro:/home/dejavu/warp /tmp/warp_restore_test
    
  5. 若想让新机器 彻底接管 旧机器的 增量备份历史,连接时需「伪装」身份。

    # 断开并使用 override 参数重新连接
    sudo kopia repository disconnect
    sudo kopia repository connect b2 \
      --bucket=<存储桶名称> \
      --key-id=<keyID> \
      --key=<applicationKey> \
      --prefix=<目录前缀>/
      --override-hostname=<原主机名> \
      --override-username=<原用户名>
    

验证完成,删除测试路径

sudo rm -rf /tmp/warp_test_restore

总结

至此,我们的自动化备份方案就实施完成了,后续添加新的自动备份任务,只需要:

# 细致化策略
sudo kopia policy set /home/dejavu/wakapi \
  --keep-latest 3 \
  --keep-hourly 0 \
  --keep-daily 0 \
  --keep-weekly 0 \
  --keep-monthly 0 \
  --keep-annual 0
# 验证策略
sudo kopia policy get /home/dejavu/wakapi
# 编辑自动化任务
sudo vim /root/kopia/backup-wakapi.sh
# 设置权限
sudo chmod 700 /root/kopia/backup-wakapi.sh
# 首次备份
sudo /bin/bash /root/kopia/backup-wakapi.sh
# 定时任务
sudo crontab -e

当然,你还可以考虑做这些优化:

  • 错峰备份,分散单日存储桶操作数,降低成本
  • 多存储服务商、多区域冗余,提升抗灾害能力