点击展开更新日志

2026

2026-02-13

【创建】本文

nexttime

会有些什么呢(❁´◡`❁)

源起

这篇应该就是2025年最后一篇文章了,也或许有机会入选年度好文😏(当然是自评)。经过差不多一年多时间的折腾,内网环境基本已经稳定,各方面使用也差不多可以满足需求了,于是后面开始折腾备份,中间试过几种备份方案,不过都不甚满意,最近借助GPT的帮助,重新理了一遍,自我感觉还不错,供友友们参考。

内网架构

先来介绍下我的内网环境:

内网物理架构

  • 物理及虚机服务器:

    • 软路由n7505:
      • PVE底层:Debian13
        • iKuai主路由:主要做限速
        • ImmortalWrt:透明代理
        • DockerHost:计算服务器,承载多个Docker服务
    • 交换机:
    • PC主机(代号:Icarus):主力PC,Win11,同时有一块10T机械开了SMB做备份服务器(BackupServer)
    • 迷你主机(代号:Delta):7940HS,Win11,目前做模拟器、日常备机
    • MacminiM4(代号:Beta):最低配,计算服务器,承载多个Docker服务
    • NAS:威联通C462C2,仅做存储。

备份内容&要求

我把我的所有数据分成两部分:

  • 绝对不可丢失数据:不计成本多副本多介质存储,比如照片、工作资料、个人知识库、密码本等
  • 可丢失数据:成本受限仅保留本地,允许丢失,可以重复获取/丢失影响可控,比如涩图和片

备份目标如下,目前仍存在差距:

  1. 所有不可丢失数据存在可恢复副本,本地+NAS+加密上传云盘保存;
  2. 定期备份校验数据可用性;
  3. 以上。

PC主机

D盘(10T)作为备份盘,除有一个Temp目录作为日常使用存放下载的临时大文件,其他目录作为C盘及其他client的备份目录不做手动更改,目录结构:

1
2
3
4
5
6
7
8
D:\
Applications
Desktop
Documents
Downloads
Pictures
...
Temp

备份内容

  • C盘整机备份
  • Documents等独立文件数据

备份工具

  • Macrium/Acronis:每月一次仅保留最近1-3个备份(每天备份磁盘读写太大,做了一年放弃了)
  • GoodSync:手动设置排除

PVE

备份内容

  • pve配置
  • 虚拟机(除DockerHost)

备份工具

  • restic
  • smbclient

(可选)前置操作

青龙面板

为例方便定时任务管理,这里是使用青龙面板做定时任务管理,ssh到客户端执行脚本。

配置SSH登录

为了让青龙能执行远程主机上的脚本,需要先在宿主机配置SSH免密登录,然后将密钥挂载到容器。

  1. 创建密钥对:

    1
    ssh-keygen # 假设文件名为 id_ed25519
  2. 拷贝公钥到服务器:

    1
    ssh-copy-id -i ~/.ssh/id_ed25519 root@<IP>

部署

  1. 创建数据目录:

    1
    mkdir /data/docker/qinglong
  2. 拷贝密钥对:

    1
    2
    3
    mkdir /data/docker/qinglong/ssh
    cp ~/.ssh/id_ed25519 /data/docker/qinglong/ssh
    cp ~/.ssh/known_hosts /data/docker/qinglong/ssh
  3. 准备compose文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    services:
    qinglong:
    image: whyour/qinglong:latest # 基于 Debian 的版本:whyour/qinglong:debian
    container_name: Qinglong
    volumes:
    - ./data:/ql/data
    - ./ssh:/root/.ssh
    ports:
    - "5700:5700"
    environment:
    QlBaseUrl: '/' # 部署路径非必须,以斜杠开头和结尾,比如 /test/
    restart: unless-stopped

    😶‍🌫️:

    • 挂载 .ssh 是为了 ssh 免密登录到客户端执行备份脚本

    • 这里之所以没有加 traefik 标签,是因为是部署在另一台机器上的。。。

  4. 启动部署:

    1
    2
    3
    docker compose up -d

    # 手动进入容器执行免密登录做一次保存,因为第一次连接会做Host Key Verification(主机指纹校验),后续写入到 known_hosts 就不会再出现了。
  5. 访问地址:
    http://localhost:5700

备份配置

我的方案是本地备份+中心备份的方式,客户端使用 restic 备份,通过 SMB/SFTP 的方式同步到中心服务器。

BackupServer

作为备份管理中枢,配置如下:

  1. 创建备份目录:

    1
    2
    3
    4
    D:\BackupServer\repos	# restic仓库
    # 可选,暂时没用到
    D:\BackupServer\config # 配置
    D:\BackupServer\staging # 其他文件
  2. 安装工具:

    • restic · Backups done right! :Windows下的安装方式很简单,下载解压放到舒服的位置,修改程序名为 restic.exe (去掉版本号),添加 PATH 环境变量。
    • Rclone :同上
  3. 创建SMB用户:

    1. 从【设置】-【账户】-【其他用户】添加一个本地账户,选择“没有这个用户的微软账户信息”,然后禁用登录,之后也就不会在登录界面显示影响美观;
    2. 设置 BackupServer 目录共享给SMB用户,完全控制。

PVE

PVE上需要将备份服务器的目录挂载到本地,然后挂给vm,默认情况下vm是不能直接使用SMB挂载的。

  1. 安装工具:

    1
    # 忘了有没有装过,如果报错就装一下,后面再补
  2. 创建挂载目录:

    1
    mkdir -p /mnt/backupserver
  3. 创建登录凭据:

    1
    2
    3
    4
    5
    6
    7
    vim /root/smb_credential

    # 写入刚才创建的SMB用户信息
    # /root/smb_credential
    username=smbShare
    password=smbShare

  4. 挂载SMB目录:

    1
    2
    3
    4
    5
    6
    mount -t cifs //192.168.1.7/BackupServer /mnt/backupserver -o credentials=/root/smb_credential,vers=3.1.1,sec=ntlmssp,iocharset=utf8,serverino,nosharesock,soft,_netdev,nounix,file_mode=0644,dir_mode=0777,rsize=1048576,wsize=1048576,cache=none

    # /etc/fstab
    //192.168.1.7/BackupServer /mnt/backupserver cifs \
    credentials=/root/smb_credentials,vers=3.1.1,iocharset=utf8,sec=ntlmssp,serverino,nosharesock,soft,uid=1000,gid=1000,file_mode=0664,dir_mode=0775,nounix,_netdev 0 0

    这里可能会遇到的一个问题,dmesg 提示报错 STATUS_ACCOUNT_LOCKED_OUT ,这是因为SMB/CIFS 在认证失败多次后,Windows 会触发 Account Lockout Policy,之后任何正确密码都会被拒绝,返回这个错误。

    Win + R → lusrmgr.msc
    Users → 找到你的账户
    右键 → Properties → 取消勾选 Account is locked out

  5. 创建备份目录:

    1
    2
    3
    4
    # restic 仓库
    mkdir /mnt/backupserver/repos/pve
    # 临时备份目录
    mkdir /data/backup/config{pve/network}
  6. 初始化备份仓库:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    vim /root/restic.env

    # /root/restic.env & /root/.bash_profile
    export RESTIC_REPOSITORY="/mnt/backupserver/repos/pve"
    export RESTIC_PASSWORD="<restic_password>"

    # 初始化仓库
    source /root/.bash_profile
    restic init
  7. 创建虚拟机备份:
    到PVE web界面,服务器视图,选择【数据中心】-【存储】,添加备份目录:

    • 类型:目录
    • ID:backup
    • 目录:/data/backup
    • 内容:只选择备份

    再添加备份计划,保存位置选择 backup ,建议每周/每月备份一次即可,比如我的 DockerHost 虚拟机特别大(300G),每次备份时间很长,因此每月备份一次,然后对上面的Docker数据每周单独备份。

  8. 编写备份脚本:

    1
    vim /root/restic_backup_pve.sh
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    #!/usr/bin/env bash
    set -e

    BACKUP_DIR="/data/backup"
    # 读取restic环境变量
    source /root/restic.env

    echo "===== PVE restic backup start $(date) ====="

    # 1. 防止并发执行
    exec 9>/tmp/restic-pve.lock
    flock -n 9 || { echo "Another restic job running"; exit 1; }

    # 2. 清理僵尸锁(非常重要)
    restic unlock || true

    # 3. 备份 PVE 配置(先更新一份)
    rsync -aL /etc/pve/ ${BACKUP_DIR}/config/pve2/
    cp /etc/network/interfaces ${BACKUP_DIR}/config/network/

    # 4. 执行备份(只备份PVE自己的目录)
    restic backup ${BACKUP_DIR}

    # 5. 保留策略:仅保留最近3个
    restic forget --keep-last 3 --prune

    echo "===== PVE restic backup finished $(date) ====="

    添加执行权限并手动执行一次看看是否报错。

    1
    chmod +x restic_backup_pve.sh
  9. 配置定时任务:
    并没有直接使用 crontab 的方式,并不是不好,只是最近装了青龙面板,干脆想着定时任务都放一起方便管理。如果不想用,直接用crontab添加定时任务即可:

    1
    2
    3
    4
    crontab -e

    # 每周五晚上19:00执行备份,主要是想着出问题了有时间排障
    00 19 * * 5 /root/restic_backup_pve.sh

    如果使用青龙,创建定时任务:

    • 名称:PVE 定时备份
    • 命令:ssh -i /root/id_ed25519 root@<PVE_IP> '/root/restic_backup_pve.sh'
    • 定时类型:常规定时
    • 定时规则:00 19 * * 5

DockerHost

这个是创建的LXC虚拟机,系统用的 Debian12

  1. 挂载备份目录:

    1. 在PVE上创建单独的备份目录:

      1
      mkdir /mnt/backupserver/repos/dockerHost
    2. 挂载给容器:

      1
      2
      3
      4
      vim /etc/pve/lxc/<vmid>.conf

      # 添加以下配置
      mp2: /mnt/backupserver/repos/dockerHost,mp=/mnt/backup
    3. 重启容器:

      1
      2
      pct stop <vmid>
      pct start <vmid>
  2. 初始化仓库:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    apt install restic
    vim /root/restic.env

    # /root/restic.env & /root/.bash_profile
    export RESTIC_REPOSITORY="/mnt/backup"
    export RESTIC_PASSWORD="<restic_password>"

    # 初始化仓库
    source /root/.bash_profile
    restic init

  3. 创建备份脚本:

    ⚠Warnging:
    使用此脚本,假设:

    • docker 数据目录:/data/docker
    • 脚本都已经给了执行权限
    1
    2
    3
    4
    5
    6
    # 文件结构
    /
    ├── backup_volumes.sh
    ├── restic.env
    ├── rsync_exclude_patterns.txt
    └── restic_backup_dockerHost.sh
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    #!/bin/bash
    # restic_backup_dockerHost.sh
    # 加载环境变量
    set -e

    source /root/restic.env
    restic unlock || true

    # compose 文件源
    COMPOSE_SOURCE="/data/docker"
    BACKUP_DIR="/data/backup/containers_data/docker_compose"

    # 日志
    log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
    }

    # docker compose backup
    mkdir -p "${BACKUP_DIR}"
    rsync -av --delete --exclude-from='/root/rsync_exclude_patterns.txt' "${COMPOSE_SOURCE}/" "${BACKUP_DIR}/"

    # volumes backup

    bash /root/backup_volumes.sh

    # 执行备份
    restic backup /data/backup

    # 执行保留策略(保留最近 7 天、4 周、6 个月的备份,清理不需要的数据)
    restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    #!/usr/bin/env bash
    # backup_volumes.sh
    set -Eeuo pipefail

    ########################################
    # 配置区
    ########################################

    # 是否 Dry Run(true=只演练不执行删除/停止/启动)
    DRY_RUN=false

    # 备份根目录
    BACKUP_DIR_ROOT="/data/backup/containers_data/volumes"

    # 保留天数(可外部覆盖:RETENTION_DAYS=30 ./backup_volumes.sh)
    RETENTION_DAYS=${RETENTION_DAYS:-7}

    # 日期后缀
    DATE_SUFFIX=$(date +%Y%m%d)
    BACKUP_DIR="${BACKUP_DIR_ROOT}/${DATE_SUFFIX}"

    # 排除卷(支持通配符)
    BLACKLIST=(
    "homepage_logs"
    "immich_model_cache"
    )

    ########################################
    # 工具函数
    ########################################

    log() {
    echo "[$(date '+%F %T')] $*"
    }

    run_cmd() {
    if [ "$DRY_RUN" = true ]; then
    echo "[DRY-RUN] $*"
    else
    eval "$@"
    fi
    }

    is_blacklisted() {
    local vol="$1"
    for pattern in "${BLACKLIST[@]}"; do
    [[ "$vol" == $pattern ]] && return 0
    done
    return 1
    }

    ########################################
    # 创建备份目录
    ########################################
    mkdir -p "$BACKUP_DIR"
    log "备份目录: $BACKUP_DIR"

    ########################################
    # 获取所有命名卷(排除匿名卷)
    ########################################
    mapfile -t VOLUMES < <(docker volume ls -q | grep -vE '^[0-9a-f]{64}$')

    ########################################
    # 找出使用卷的运行中容器
    ########################################
    log "扫描使用 volumes 的容器..."

    RUNNING_CONTAINERS=$(docker ps -q)
    CONTAINERS_TO_STOP=()

    for CID in $RUNNING_CONTAINERS; do
    for VOL in "${VOLUMES[@]}"; do
    if docker inspect "$CID" | grep -q "\"Name\": \"${VOL}\""; then
    CONTAINERS_TO_STOP+=("$CID")
    break
    fi
    done
    done

    ########################################
    # 停止容器(保证数据一致性)
    ########################################
    if [ ${#CONTAINERS_TO_STOP[@]} -gt 0 ]; then
    log "需要暂停的容器: ${CONTAINERS_TO_STOP[*]}"
    run_cmd "docker stop ${CONTAINERS_TO_STOP[*]}"
    else
    log "没有需要暂停的容器"
    fi

    ########################################
    # 开始备份 volumes
    ########################################
    log "开始备份 Docker Volumes..."

    for VOL in "${VOLUMES[@]}"; do

    if is_blacklisted "$VOL"; then
    log "[跳过] $VOL 在黑名单中"
    continue
    fi

    backup_file="${VOL}_${DATE_SUFFIX}.tar.gz"
    log "备份 volume: $VOL"

    if [ "$DRY_RUN" = true ]; then
    echo "[DRY-RUN] docker run backup $VOL"
    continue
    fi

    docker run --rm --pull=never --log-driver=none \
    -v "$VOL":/source:ro \
    -v "$BACKUP_DIR":/dest \
    alpine \
    tar -zcf "/dest/${backup_file}" -C /source .

    if [ -f "$BACKUP_DIR/$backup_file" ]; then
    SIZE=$(du -h "$BACKUP_DIR/$backup_file" | cut -f1)
    log "[成功] $backup_file ($SIZE)"
    else
    log "[失败] $VOL 备份失败!"
    fi

    done

    ########################################
    # 恢复容器运行
    ########################################
    if [ ${#CONTAINERS_TO_STOP[@]} -gt 0 ]; then
    log "恢复容器运行..."
    run_cmd "docker start ${CONTAINERS_TO_STOP[*]}"
    fi

    ########################################
    # 清理旧备份
    ########################################
    log "清理 $RETENTION_DAYS 天前的备份..."

    if [ "$DRY_RUN" = true ]; then
    find "$BACKUP_DIR_ROOT" -maxdepth 1 -type d -mtime +"$RETENTION_DAYS" \
    -name "[0-9]*" -print
    else
    find "$BACKUP_DIR_ROOT" -maxdepth 1 -type d -mtime +"$RETENTION_DAYS" \
    -name "[0-9]*" -exec rm -rf {} \;
    fi

    log "备份流程完成 ✅"

    • 使用前设置 DRY_RUN=true 试一下看看效果,确认没问题了再改成 false 使用。主要功能:
      • 备份使用到的docker volume 卷,支持黑名单
      • 备份前暂停容器,自动跳过已停止容器,备份完成后恢复
      • 自动清理历史备份,默认7天
  4. 配置定时任务
    如果使用 crontab 添加任务即可。
    如果使用青龙,参照上方 PVE 配置即可。

其他

restic 更新

rclone 更新

1
rclone selfupdate