添加 acls 规则

进入后台的 ACL 页面,添加一条规则,明确允许 VPS 访问 NAS 的 SMB 服务端口 (445)。

// 规则: 允许 VPS 访问 NAS 的 SMB 445 端口 { "action": "accept", "src": ["tag:vps"], "dst": ["tag:nas:445"] },

Vps 上挂载 nas 共享文件夹

此步骤的目的是让 VPS 可以像读写本地文件夹一样操作 NAS 上的共享目录。

  • 安装 SMB/CIFS 工具 (在 VPS 上执行):

    sudo apt update
    sudo apt install cifs-utils
  • 创建本地挂载点 (在 VPS 上执行):

    sudo mkdir -p /mnt/nas_backup
  • 创建凭证文件 (在 VPS 上执行,推荐): 为了安全,我们将 NAS 的用户名和密码存放在一个受保护的文件中。

    # 创建并编辑文件
    sudo nano /etc/nas-credentials
    
    # 在文件中写入以下内容并保存
    # username=您NAS的用户名
    # password=您NAS的密码
    
    # 锁定文件权限,确保只有 root 能读取
    sudo chmod 600 /etc/nas-credentials
  • **执行挂载 (在 VPS 上执行):
  • 通过 tailscale status 命令获取您 NAS 的 Tailscale IP (例如 100.x.x.x)。
  • 确认您 NAS 上的共享文件夹名称 (例如 vps_ryzen)。

    sudo mount -t cifs //100.x.x.x/vps_ryzen /mnt/nas_backup -o credentials=/etc/nas-credentials
  • 验证挂载: 运行 df -h,如果看到 /mnt/nas_backup 的条目,说明挂载成功。

部署备份脚本

这是执行备份任务的核心。

最终备份脚本 (backup_to_nas.sh):

#!/bin/bash

  

# =================================================================

# Docker 数据卷与指定文件夹备份脚本 - 备份至 NAS (全功能版)

#

# 功能:

# 1. 为每一次备份任务创建一个带时间戳的独立文件夹。

# 2. 同时支持备份 Docker 数据卷和服务器上的任意文件夹。

# 3. 将所有备份文件 (.tar.gz) 存入该次任务的独立文件夹中。

# 4. 自动清理指定天数前的旧备份文件夹。

# 5. 提供详细的日志输出和错误检查。

# =================================================================

  

# --- 可配置部分 ---

  

# 1. 备份文件存放的根目录 (请确保这是您挂载的 NAS 目录)

BACKUP_ROOT_DIR="/mnt/nas_backup"

  

# 2. 需要备份的 Docker 卷名称列表 (用空格隔开)

#    留空则跳过: VOLUMES_TO_BACKUP=""

VOLUMES_TO_BACKUP="typecho_mariadb_data typecho_caddy_data"

  

# 3. 【新增】需要备份的文件夹绝对路径列表 (用空格隔开)

#    - 请确保路径是绝对路径 (以 / 开头)。

#    - 留空则跳过: DIRS_TO_BACKUP=""

#    - 示例: DIRS_TO_BACKUP="/etc/nginx /var/www/my_website"

DIRS_TO_BACKUP="/root/typecho"

  

# 4. 备份文件夹保留天数 (例如,保留最近7天的备份)

RETENTION_DAYS=7

  

# --- 脚本核心逻辑 (一般无需修改) ---

  

# 设置时区

export TZ='Asia/Shanghai'

  

# 本次备份任务的唯一时间戳

DATE=$(date +%Y%m%d_%H%M%S)

  

# 为本次备份创建一个独立的、带时间戳的子目录

CURRENT_BACKUP_DIR="${BACKUP_ROOT_DIR}/${DATE}"

  

echo "====================================="

echo "备份任务开始于: $(date)"

echo "====================================="

echo "本次所有备份文件将保存至: ${CURRENT_BACKUP_DIR}"

  

# 确保本次备份的独立目录存在

mkdir -p ${CURRENT_BACKUP_DIR}

  

# --- 1. 备份 Docker 数据卷 ---

if [ -n "${VOLUMES_TO_BACKUP}" ]; then

  echo ""

  echo "--- 开始处理 Docker 数据卷 ---"

  for VOLUME in ${VOLUMES_TO_BACKUP}; do

    echo "-------------------------------------"

    echo ">> 正在处理卷: ${VOLUME}"

  

    if ! docker volume inspect ${VOLUME} > /dev/null 2>&1; then

      echo "   [错误] 卷 '${VOLUME}' 不存在,已跳过。"

      continue

    fi

  

    BACKUP_FILENAME="${VOLUME}.tar.gz"

    BACKUP_FULL_PATH="${CURRENT_BACKUP_DIR}/${BACKUP_FILENAME}"

    echo "   备份文件将保存为: ${BACKUP_FULL_PATH}"

  

    docker run --rm \

      -v "${VOLUME}:/source_data:ro" \

      -v "${CURRENT_BACKUP_DIR}:/backup_target" \

      alpine \

      tar -czf "/backup_target/${BACKUP_FILENAME}" -C /source_data .

  

    if [ $? -eq 0 ]; then

      echo "   [成功] 卷 '${VOLUME}' 已成功备份。"

    else

      echo "   [错误] 备份卷 '${VOLUME}' 失败!"

    fi

  done

else

  echo "未配置 Docker 数据卷备份,跳过此步骤。"

fi

  

# --- 2. 【新增】备份指定文件夹 ---

if [ -n "${DIRS_TO_BACKUP}" ]; then

  echo ""

  echo "--- 开始处理指定文件夹 ---"

  for DIR_PATH in ${DIRS_TO_BACKUP}; do

    echo "-------------------------------------"

    echo ">> 正在处理文件夹: ${DIR_PATH}"

  

    # 检查文件夹是否存在且可读

    if [ ! -d "${DIR_PATH}" ]; then

      echo "   [错误] 文件夹 '${DIR_PATH}' 不存在,已跳过。"

      continue

    fi

    if [ ! -r "${DIR_PATH}" ]; then

      echo "   [错误] 文件夹 '${DIR_PATH}' 没有读取权限,已跳过。"

      continue

    fi

  

    # 从完整路径中提取基本名称作为文件名 (例如 /etc/nginx -> nginx)

    # 并替换路径中的斜杠'/'为下划线'_',以创建更唯一的文件名 (例如 /home/user/app -> home_user_app)

    DIR_BASENAME=$(echo "${DIR_PATH}" | sed 's|^/||; s|/$||; s|/|_|g')

    BACKUP_FILENAME="${DIR_BASENAME}.tar.gz"

    BACKUP_FULL_PATH="${CURRENT_BACKUP_DIR}/${BACKUP_FILENAME}"

    echo "   备份文件将保存为: ${BACKUP_FULL_PATH}"

  

    # 使用 tar 命令直接打包文件夹

    # -C 选项可以在打包前切换到父目录,从而避免在压缩包中包含完整的绝对路径

    # 例如,打包 /var/www/html,我们切换到 /var/www 然后打包 html 文件夹

    PARENT_DIR=$(dirname "${DIR_PATH}")

    TARGET_DIR=$(basename "${DIR_PATH}")

    tar -czf "${BACKUP_FULL_PATH}" -C "${PARENT_DIR}" "${TARGET_DIR}"

  

    if [ $? -eq 0 ]; then

      echo "   [成功] 文件夹 '${DIR_PATH}' 已成功备份。"

    else

      echo "   [错误] 备份文件夹 '${DIR_PATH}' 失败!"

    fi

  done

else

    echo "未配置文件夹备份,跳过此步骤。"

fi

  
  

# --- 3. 清理旧备份 ---

echo ""

echo "-------------------------------------"

echo ">> 开始清理旧的备份 (保留最近 ${RETENTION_DAYS} 天的备份文件夹)..."

  

# 查找并删除整个旧的备份文件夹

find "${BACKUP_ROOT_DIR}" -mindepth 1 -maxdepth 1 -type d -mtime +${RETENTION_DAYS} -print -exec rm -rf {} +

  

echo "   旧备份清理完毕。"

echo ""

echo "所有备份任务完成于: $(date)"

echo "====================================="

部署操作:

  1. 将上述代码保存到 VPS 的 /root/backup_to_nas.sh 文件中。
  2. 根据您自己的情况,修改 BACKUP_DIRVOLUMES_TO_BACKUP 变量。
  3. 授予执行权限:chmod +x /root/backup_to_nas.sh
  4. 手动测试:运行 /root/backup_to_nas.sh,并去 NAS 上检查是否生成了备份文件。

设置定时任务 (Cron)

  • 编辑 Crontab:

    crontab -e
  • 添加任务: 在文件末尾添加以下一行,设置每天凌晨2点执行备份。

    0 2 * * * /root/backup_to_nas.sh > /var/log/docker_backup.log 2>&1
  • 保存并退出。系统会自动加载新任务。

在配置过程中,遇到并解决了一系列典型问题,记录如下:

  1. 问题: 运行脚本时报错 $: \r: command not found

    • 原因: 脚本文件是在 Windows 系统下编辑后上传到 Linux 的,包含了 Windows 风格的换行符 (\r\n)。
    • 解决方案: 在 VPS 上使用 dos2unixsed 命令转换文件格式。

      # 方法一 (推荐)
      dos2unix /root/backup_to_nas.sh
      # 方法二 (通用)
      sed -i 's/\r$//' /root/backup_to_nas.sh
  2. 问题: 脚本提示 [错误] 卷 'mariadb_data' 不存在

    • 原因: 使用 docker-compose 时,它会自动为卷名加上项目名(即文件夹名)作为前缀。例如,项目文件夹叫 typecho,卷名叫 mariadb_data,那么实际的卷名是 typecho_mariadb_data
    • 解决方案: 运行 docker volume ls 命令查看确切的卷名,并更新到脚本的 VOLUMES_TO_BACKUP 变量中。
  3. 问题: 挂载 SMB 共享时报错 mount error(115): Operation now in progress

    • 原因: 连接超时。这通常是由于防火墙阻止了连接。在本次实践中,是 Tailscale 的 ACL 策略默认禁止了设备间的端口访问。
    • 解决方案: 登录 Tailscale 后台,修改 ACL 规则,明确添上一条允许 VPS (tag:vps) 访问 NAS (tag:nas) 的 445 端口的规则。
  4. 问题: 备份成功,但在 NAS 上看不到文件。

    • 原因: 脚本中的 BACKUP_DIR 路径填写错误。填写的是 NAS 上的路径 (/mnt/Storage1/vps_ryzen),但脚本是在 VPS 上运行,它应该写入到 VPS 上的挂载点 (/mnt/nas_backup)。
    • 解决方案: 将脚本中的 BACKUP_DIR 修改为 VPS 上的正确挂载点路径。