< OS 有关 > 在 Ubuntu24 使用 DD 制作快照 并复制到本地运行的方案 Script: vps-backup.sh

2026-05-12 16:41:30247 阅读量

原因:

腾讯 VPS 不支持“快照下载”,还有快照限制数量:1

相关服务:香港站群服务器

如何在Ubuntu 24 VPS上制作快照并复制到本地运行的问题。

在 Ubuntu 24 VPS上制作磁盘镜像

DD 工具

用于低级别的复制和转换数据。

使用前需要注意:

  • 有足够的空间存储镜像
  • 磁盘设备正确被使用
  • 压缩镜像以节省空间
  • 镜像下载到本地
  • 停止VPS上的除 SSH 网络连接 DNS,以外的所有服务

设计思路

  1. 使用 dd 工具备份 VPS 硬盘
  2. 备份生成 tar.gz 文件
  3. 在备份前从列表中“停止”不必要的服务,只保留 SSH,DNS 还有 网络连接
  4. 备份完成后,启动“停止”的服务,恢复正常
  5. 因为 Google Drive 挂载 (google-drive-ocamlfuse) 是独立硬盘,要检测。
  6. 运行前检测磁盘空间。

完整 Script: vps-backup.sh

#!/bin/bash

# VPS disk image backup script by Dave on 20Apirl2025
# Version 0.6
# Purpose: Stop services, create a complete system backup, then restore services
# Usage: sudo bash vps-backup.sh [output file path]

# Check if running with root privileges
if [ "$EUID" -ne 0 ]; then
  echo "Please run this script with root privileges"
  exit 1
fi

# Check parameters
if [ "$#" -lt 1 ]; then
  echo "Usage: $0 [output file path]"
  echo "Example: $0 /root/vps-backup.tar.gz"
  exit 1
fi

OUTPUT_FILE="$1"
OUTPUT_DIR=$(dirname "$OUTPUT_FILE")
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
LOG_FILE="${OUTPUT_DIR}/vps_backup_${TIMESTAMP}.log"

# Ensure output directory exists
mkdir -p "$OUTPUT_DIR"

# Define services to stop
SERVICES_TO_STOP=(
  "mariadb.service"
  "nginx.service"
  "dovecot.service"
  "php8.3-fpm.service" 
  "v2ray.service"
  "[email protected]"
  "tailscaled.service"
  "google-drive.service"
  "cron.service"
)

# Check Google Drive mount
GOOGLE_DRIVE_MOUNTED=false
if mount | grep -q "google-drive-ocamlfuse"; then
  GOOGLE_DRIVE_MOUNTED=true
  # Add Google Drive service to stop list if not already included
  if ! echo "${SERVICES_TO_STOP[@]}" | grep -q "google-drive.service"; then
    SERVICES_TO_STOP+=("google-drive.service")
  fi
fi

# Function to log messages
log() {
  echo "[$(date +"%Y-%m-%d %H:%M:%S")] $1" | tee -a "$LOG_FILE"
}

# Function to stop services
stop_services() {
  log "Starting to stop services..."
  
  for service in "${SERVICES_TO_STOP[@]}"; do
    if systemctl is-active --quiet "$service"; then
      log "Stopping service: $service"
      systemctl stop "$service"
      if [ $? -eq 0 ]; then
        log "Service $service stopped successfully"
      else
        log "Warning: Unable to stop service $service"
      fi
    else
      log "Service $service is already stopped"
    fi
  done
  
  log "All specified services have been stopped"
  
  # Execute sync to ensure data is written to disk
  log "Executing sync command to ensure data is written to disk..."
  sync
  log "Sync completed"
}

# Function to start services
start_services() {
  log "Starting to start services..."
  
  # Start services in reverse order
  for ((i=${#SERVICES_TO_STOP[@]}-1; i>=0; i--)); do
    service="${SERVICES_TO_STOP[$i]}"
    log "Starting service: $service"
    systemctl start "$service"
    if [ $? -eq 0 ]; then
      log "Service $service started successfully"
    else
      log "Warning: Unable to start service $service"
    fi
  done
  
  log "All services have been restored"
}

# Function to get disk information
get_disk_info() {
  log "Getting disk information..."
  
  # Find system partition
  ROOT_PART=$(findmnt -n -o SOURCE /)
  log "Root partition: $ROOT_PART"
  
  if [[ "$ROOT_PART" == /dev/mapper/* ]]; then
    # Handle LVM volumes
    log "LVM volume detected, using lvdisplay for information"
    DISK_INFO=$(lvdisplay | grep "LV Path" | awk '{print $3}')
    ROOT_DISK="$ROOT_PART"
  else
    # For regular partitions
    ROOT_DISK="$ROOT_PART"
    DISK_DEV=$(echo "$ROOT_PART" | sed 's/[0-9]*$//')
    DISK_INFO=$(lsblk -n -o NAME,SIZE,TYPE | grep "disk" | head -n 1)
    log "Derived disk device: $DISK_DEV (not used)"
  fi
  
  log "Using system partition: $ROOT_DISK"
  log "Disk information: $DISK_INFO"
  
  # Return the actual available device path
  echo "$ROOT_DISK"
}

# Function to check disk space
check_disk_space() {
  local source_disk="$1"
  
  log "Checking disk space..."
  
  # Get disk or partition size
  if [ -b "$source_disk" ]; then
    # If it's a block device, use blockdev
    DISK_SIZE=$(blockdev --getsize64 "$source_disk" 2>/dev/null)
    
    # If blockdev fails, try to get size from df
    if [ -z "$DISK_SIZE" ] || [ "$DISK_SIZE" -eq 0 ]; then
      log "Unable to get size using blockdev, attempting to use df..."
      local mount_point="/"
      if [ -n "$source_disk" ]; then
        # If it's a partition, find its mount point
        mount_point=$(findmnt -n -o TARGET "$source_disk" 2>/dev/null || echo "/")
      fi
      DISK_SIZE=$(df -B1 "$mount_point" | awk 'NR==2 {print $3}') # Use used space as estimate
    fi
  else
    # If not a block device, use df to get root partition size as an estimate
    log "Warning: $source_disk is not a block device, using root partition size as estimate"
    DISK_SIZE=$(df -B1 / | awk 'NR==2 {print $3}') # Use used space as estimate
  fi
  
  # Check available space in output directory
  AVAILABLE_SPACE=$(df -B1 "$OUTPUT_DIR" | awk 'NR==2 {print $4}')
  
  log "Size of data to backup: $(numfmt --to=iec-i --suffix=B $DISK_SIZE)"
  log "Available space in output directory: $(numfmt --to=iec-i --suffix=B $AVAILABLE_SPACE)"
  
  # Consider extra metadata and safety margin, needs 1.1 times the space
  REQUIRED_SPACE=$(echo "$DISK_SIZE * 1.1" | bc | cut -d. -f1)
  log "Recommended required space: $(numfmt --to=iec-i --suffix=B $REQUIRED_SPACE)"
  
  if [ "$AVAILABLE_SPACE" -lt "$REQUIRED_SPACE" ]; then
    log "Error: Insufficient disk space"
    log "Need at least $(numfmt --to=iec-i --suffix=B $REQUIRED_SPACE) of available space"
    echo "Error: Insufficient disk space"
    echo "Need at least $(numfmt --to=iec-i --suffix=B $REQUIRED_SPACE) of available space"
    return 1
  else
    log "Disk space check passed"
    return 0
  fi
}

# Function to check and handle Google Drive mount
handle_google_drive() {
  log "Checking Google Drive mount..."
  if [ "$GOOGLE_DRIVE_MOUNTED" = true ]; then
    log "Google Drive mount detected"
    
    # If output directory is on Google Drive, warn user
    if [[ "$OUTPUT_FILE" == *"/ggdrive/"* ]] || mount | grep -q "google-drive-ocamlfuse.*$OUTPUT_DIR"; then
      log "Warning: Output directory is on a Google Drive mount point"
      log "This may cause slow or unstable backup"
      
      # Check available space on Google Drive
      GD_AVAILABLE_SPACE=$(df -B1 "$OUTPUT_DIR" | awk 'NR==2 {print $4}')
      log "Google Drive available space: $(numfmt --to=iec-i --suffix=B $GD_AVAILABLE_SPACE)"
      
      # Prompt user to confirm
      echo "Warning: You are attempting to save the backup to Google Drive"
      echo "Available space: $(numfmt --to=iec-i --suffix=B $GD_AVAILABLE_SPACE)"
      echo "This may cause slow or unstable backup"
      read -p "Continue? (y/n): " answer
      
      if [[ "$answer" != "y" && "$answer" != "Y" ]]; then
        log "User chose to terminate backup"
        exit 1
      fi
      
      log "User confirmed to continue backup to Google Drive"
    else
      log "Output directory is not on Google Drive, will unmount Google Drive normally"
    fi
    
    # Stop Google Drive service
    log "Stopping Google Drive service..."
    if systemctl is-active --quiet "google-drive.service"; then
      systemctl stop "google-drive.service"
      log "Google Drive service stopped"
    fi
    
    # Unmount Google Drive
    log "Unmounting Google Drive mount points..."
    local gd_mount_points=$(mount | grep "google-drive-ocamlfuse" | awk '{print $3}')
    for mount_point in $gd_mount_points; do
      log "Unmounting: $mount_point"
      fusermount -u "$mount_point"
      if [ $? -eq 0 ]; then
        log "Successfully unmounted $mount_point"
      else
        log "Warning: Unable to unmount $mount_point"
      fi
    done
  else
    log "No Google Drive mount detected"
  fi
}

# Function to restore Google Drive mount
restore_google_drive() {
  if [ "$GOOGLE_DRIVE_MOUNTED" = true ]; then
    log "Restoring Google Drive service..."
    systemctl start "google-drive.service"
    if [ $? -eq 0 ]; then
      log "Google Drive service restored"
    else
      log "Warning: Unable to restore Google Drive service"
    fi
  fi
}

# Function to create system backup
create_backup() {
  SOURCE_DISK="$1"
  
  log "Starting to create system backup..."
  log "System root partition: $SOURCE_DISK"
  log "Target file: $OUTPUT_FILE"
  
  # Determine root directory mount point
  ROOT_MOUNT=$(findmnt -n -o TARGET "$SOURCE_DISK" 2>/dev/null || echo "/")
  log "Root directory mount point: $ROOT_MOUNT"
  
  # Ensure output directory exists
  OUTPUT_DIR=$(dirname "$OUTPUT_FILE")
  mkdir -p "$OUTPUT_DIR"
  
  # Modify extension to tar.gz
  TAR_FILE="${OUTPUT_FILE%.*}.tar.gz"
  log "Modified output file to: $TAR_FILE"
  
  # Use tar to create filesystem backup
  log "Starting tar backup execution..."
  log "Excluding temporary directories and mount points..."
  
  # Create exclusion list
  EXCLUDE_LIST=()
  EXCLUDE_LIST+=("--exclude=$TAR_FILE")
  EXCLUDE_LIST+=("--exclude=/proc")
  EXCLUDE_LIST+=("--exclude=/sys")
  EXCLUDE_LIST+=("--exclude=/dev")
  EXCLUDE_LIST+=("--exclude=/run")
  EXCLUDE_LIST+=("--exclude=/mnt")
  EXCLUDE_LIST+=("--exclude=/media")
  EXCLUDE_LIST+=("--exclude=/tmp")
  
  # Check if /home mount point exists, may be using separate partition
  if findmnt /home >/dev/null 2>&1; then
    log "Detected /home as separate mount point, will backup separately"
    EXCLUDE_LIST+=("--exclude=/home")
    HOME_SEPARATE=true
  else
    HOME_SEPARATE=false
  fi
  
  # Execute root filesystem backup
  log "Starting backup of root filesystem..."
  tar -czf "$TAR_FILE" "${EXCLUDE_LIST[@]}" --one-file-system -C "$ROOT_MOUNT" .
  TAR_STATUS=$?
  
  if [ $TAR_STATUS -eq 0 ]; then
    log "Root filesystem backup successful: $TAR_FILE"
    
    # Backup home directory (if separate partition)
    if [ "$HOME_SEPARATE" = true ]; then
      HOME_TAR="${OUTPUT_FILE%.*}_home.tar.gz"
      log "Starting backup of /home partition to: $HOME_TAR"
      tar -czf "$HOME_TAR" --one-file-system -C /home .
      HOME_TAR_STATUS=$?
      
      if [ $HOME_TAR_STATUS -eq 0 ]; then
        log "/home partition backup successful: $HOME_TAR"
      else
        log "Warning: /home partition backup failed, status code: $HOME_TAR_STATUS"
      fi
    fi
    
    # Backup successful, update output file path
    OUTPUT_FILE="$TAR_FILE"
    return 0
  else
    log "Error: Backup failed, status code: $TAR_STATUS"
    return 1
  fi
}

# Calculate backup information
calculate_backup_info() {
  if [ -f "$OUTPUT_FILE" ]; then
    # Calculate backup file size
    BACKUP_SIZE=$(stat -c %s "$OUTPUT_FILE")
    log "Backup file size: $(numfmt --to=iec-i --suffix=B $BACKUP_SIZE)"
    
    # Calculate MD5 checksum
    log "Calculating MD5 checksum..."
    MD5SUM=$(md5sum "$OUTPUT_FILE" | awk '{print $1}')
    log "MD5 checksum: $MD5SUM"
    echo "$MD5SUM" > "${OUTPUT_FILE}.md5"
    log "MD5 checksum saved to ${OUTPUT_FILE}.md5"
    
    return 0
  else
    log "Error: Backup file does not exist"
    return 1
  fi
}

# Main execution flow
main() {
  log "======= VPS Backup Started ======="
  log "Timestamp: $TIMESTAMP"
  
  # Get disk information
  SOURCE_DISK=$(get_disk_info)
  
  if [ -z "$SOURCE_DISK" ]; then
    log "Error: Unable to determine a valid source device"
    log "Please ensure the script has sufficient permissions to access system partitions"
    exit 1
  fi
  
  # Check disk space
  check_disk_space "$SOURCE_DISK"
  if [ $? -ne 0 ]; then
    log "Backup terminated due to insufficient disk space"
    exit 1
  fi
  
  # Handle Google Drive mount
  handle_google_drive
  
  # Stop services
  stop_services
  
  # Create backup
  create_backup "$SOURCE_DISK"
  BACKUP_STATUS=$?
  
  # Restore services
  start_services
  
  # Restore Google Drive mount
  restore_google_drive
  
  # Calculate backup information
  if [ $BACKUP_STATUS -eq 0 ]; then
    log "Backup created successfully, calculating backup information..."
    calculate_backup_info
  fi
  
  log "======= VPS Backup Completed ======="
  
  # Display final results
  if [ $BACKUP_STATUS -eq 0 ]; then
    log "Backup completed successfully!"
    log "Backup file: $OUTPUT_FILE"
    echo ""
    echo "Backup completed successfully!"
    echo "Backup file: $OUTPUT_FILE"
    echo "Log file: $LOG_FILE"
  else
    log "Errors occurred during backup, please check log file: $LOG_FILE"
    echo ""
    echo "Errors occurred during backup, please check log file: $LOG_FILE"
  fi
}

# Execute main function
main

演示:

root@jpt:~/scripts# vi vps-backup.sh
root@jpt:~/scripts# sudo ./vps-backup.sh /path/to/output/vps-backup.img
[2025-04-21 11:50:54] ======= VPS备份开始 =======
[2025-04-21 11:50:54] 时间戳: 20250421_115054
[2025-04-21 11:50:54] 检查磁盘空间...
[2025-04-21 11:50:54] 警告: [2025-04-21 11:50:54] 正在获取磁盘信息...
[2025-04-21 11:50:54] 根分区: /dev/vda2
[2025-04-21 11:50:54] 推导出的磁盘设备: /dev/vda (不使用)
[2025-04-21 11:50:54] 使用系统分区: /dev/vda2
[2025-04-21 11:50:54] 磁盘信息: vda      50G disk
/dev/vda2 不是块设备,使用根分区大小作为估计
[2025-04-21 11:50:54] 需要备份的数据大小: 9.9GiB
[2025-04-21 11:50:54] 输出目录可用空间: 38GiB
[2025-04-21 11:50:54] 建议的所需空间: 11GiB
[2025-04-21 11:50:54] 磁盘空间检查通过
[2025-04-21 11:50:54] 检查Google Drive挂载...
[2025-04-21 11:50:54] 检测到Google Drive挂载
[2025-04-21 11:50:54] 输出目录不在Google Drive上,将正常卸载Google Drive
[2025-04-21 11:50:54] 停止Google Drive服务...
[2025-04-21 11:50:55] Google Drive服务已停止
[2025-04-21 11:50:55] 卸载Google Drive挂载点...
[2025-04-21 11:50:55] 开始停止服务...
[2025-04-21 11:50:55] 停止服务: mariadb.service
[2025-04-21 11:50:56] 服务 mariadb.service 已成功停止
[2025-04-21 11:50:56] 停止服务: nginx.service
[2025-04-21 11:50:56] 服务 nginx.service 已成功停止
[2025-04-21 11:50:56] 停止服务: dovecot.service
[2025-04-21 11:50:57] 服务 dovecot.service 已成功停止
[2025-04-21 11:50:57] 停止服务: php8.3-fpm.service
[2025-04-21 11:50:57] 服务 php8.3-fpm.service 已成功停止
[2025-04-21 11:50:57] 停止服务: v2ray.service
[2025-04-21 11:50:57] 服务 v2ray.service 已成功停止
[2025-04-21 11:50:57] 停止服务: [email protected]
[2025-04-21 11:50:57] 服务 [email protected] 已成功停止
[2025-04-21 11:50:57] 停止服务: tailscaled.service
[2025-04-21 11:50:58] 服务 tailscaled.service 已成功停止
[2025-04-21 11:50:58] 服务 google-drive.service 已经是停止状态
[2025-04-21 11:50:58] 停止服务: cron.service
[2025-04-21 11:50:58] 服务 cron.service 已成功停止
[2025-04-21 11:50:58] 所有指定服务已停止
[2025-04-21 11:50:58] 执行sync命令以确保数据写入磁盘...
[2025-04-21 11:50:58] sync完成
[2025-04-21 11:50:58] 开始创建系统备份...
[2025-04-21 11:50:58] 系统根分区: [2025-04-21 11:50:54] 正在获取磁盘信息...
[2025-04-21 11:50:54] 根分区: /dev/vda2
[2025-04-21 11:50:54] 推导出的磁盘设备: /dev/vda (不使用)
[2025-04-21 11:50:54] 使用系统分区: /dev/vda2
[2025-04-21 11:50:54] 磁盘信息: vda      50G disk
/dev/vda2
[2025-04-21 11:50:58] 目标文件: /path/to/output/vps-backup.img
[2025-04-21 11:50:58] 根目录挂载点: /
[2025-04-21 11:50:58] 修改输出文件为: /path/to/output/vps-backup.tar.gz
[2025-04-21 11:50:58] 开始tar备份执行...
[2025-04-21 11:50:58] 排除临时目录和挂载点...
[2025-04-21 11:50:58] 开始备份根文件系统...
tar: ./var/spool/postfix/dev/log: socket ignored
tar: ./var/spool/postfix/private/dovecot-lmtp: socket ignored
tar: ./var/spool/postfix/private/auth: socket ignored
tar: ./var/snap/lxd/common/lxd-user/unix.socket: socket ignored
tar: ./var/snap/lxd/common/lxd/unix.socket: socket ignored
[2025-04-21 12:00:33] 根文件系统备份成功: /path/to/output/vps-backup.tar.gz
[2025-04-21 12:00:33] 开始启动服务...
[2025-04-21 12:00:33] 启动服务: cron.service
[2025-04-21 12:00:33] 服务 cron.service 已成功启动
[2025-04-21 12:00:33] 启动服务: google-drive.service
[2025-04-21 12:00:33] 服务 google-drive.service 已成功启动
[2025-04-21 12:00:33] 启动服务: tailscaled.service
[2025-04-21 12:00:33] 服务 tailscaled.service 已成功启动
[2025-04-21 12:00:33] 启动服务: [email protected]
[2025-04-21 12:00:33] 服务 [email protected] 已成功启动
[2025-04-21 12:00:33] 启动服务: v2ray.service
[2025-04-21 12:00:33] 服务 v2ray.service 已成功启动
[2025-04-21 12:00:33] 启动服务: php8.3-fpm.service
[2025-04-21 12:00:34] 服务 php8.3-fpm.service 已成功启动
[2025-04-21 12:00:34] 启动服务: dovecot.service
[2025-04-21 12:00:34] 服务 dovecot.service 已成功启动
[2025-04-21 12:00:34] 启动服务: nginx.service
[2025-04-21 12:00:34] 服务 nginx.service 已成功启动
[2025-04-21 12:00:34] 启动服务: mariadb.service
[2025-04-21 12:00:35] 服务 mariadb.service 已成功启动
[2025-04-21 12:00:35] 所有服务已恢复
[2025-04-21 12:00:35] 恢复Google Drive服务...
[2025-04-21 12:00:35] Google Drive服务已恢复
[2025-04-21 12:00:35] 备份创建成功,计算备份信息...
[2025-04-21 12:00:35] 备份文件大小: 4.7GiB
[2025-04-21 12:00:35] 计算MD5校验和...
[2025-04-21 12:01:03] MD5校验和: 55868c61518c9801a0ccdc79024bd3d7
[2025-04-21 12:01:03] MD5校验和已保存到 /path/to/output/vps-backup.tar.gz.md5
[2025-04-21 12:01:03] ======= VPS备份完成 =======
[2025-04-21 12:01:03] 备份成功完成!
[2025-04-21 12:01:03] 备份文件: /path/to/output/vps-backup.tar.gz

备份成功完成!
备份文件: /path/to/output/vps-backup.tar.gz
日志文件: /path/to/output/vps_backup_20250421_115054.log
root@jpt:~/scripts# ll /path/to/output/vps-backup.tar.gz
-rw-r--r-- 1 root root 5044560222 Apr 21 12:00 /path/to/output/vps-backup.tar.gz
root@jpt:~/scripts# 

可以看到 运行时 google drive 是umount, 结束后恢复。

目录是 +- 10GB,备份后是 5GB

传输到网盘

1. 之前有提到 google drive

参考:

< OS 有关 > 利用 google-drive-ocamlfuse 工具,在 Ubuntu 24 系统上 加载 Google DRIVE 网盘_google网盘 ubuntu-CSDN博客

2. 使用 bdgo.sh

参考:   < OS 有关> BaiduPCS-Go 程序的 菜单脚本 Script: BaiduPCS-Go.Menu.sh (bdgo.sh)-CSDN博客

好像 百度网盘 降速了

总结:

这个脚本可以继续完善,比如文件名加入日期、使用选择菜单(用 bdgo google_drive等),但我现在不想。

因为配置 Postfix 看了太多文档,同时用文章记录配置过程,已经写了 16427 字。现在发现没配置 webmail ,这几个小时一直在配置与修复配置错误中交战,还要在文章中更新,大脑已经宕机前,为了保存工作创建这个脚本。

我上传的脚本是英文注释,不再用 AI 转换注释语言,太麻烦。

运行的输出是用旧的中文注释脚本,这个版本已经被替换。

不知道是不是垃圾腾讯原因,我的 VPS 到 百度云 限速到:

这时在复制同样的文件 /path/to/output/vps-backup.tar.gz 到 google drive.

VPS 控制台输出:

30Mbps TCP/IP 实际应该在 3-4MB 的系统文件传输速度,看来全给 GOOGLE

5GB 文件上传到 GOOGLE DRIVER by google-drive-ocamlfuse 用了 27分13秒 , 平均速率 3.14 MB/s

本文地址:https://www.limyvps.com/news/26_20528.html