首先声明,由于代码能力有限,生成的代码由AI辅助完成,我主要在流程上和代码验证上做了相应的验证和修改。
由于在飞牛NAS上部署了“为知笔记”的容器,担心迁移笔记后万一出现系统或应用问题造成数据丢失,因此规划了这个脚本。此脚本理论上也支持检测和备份已经安装的其他容器应用(我只对迅雷容器验证测试)。
这是一个fnNAS下的容器备份脚本,提供交互菜单选项,手工备份需要手动输入备份目录;
可以自动创建定时备份任务(目前定义的是晚上10点自动备份,备份保留3份循环保存,存储路径是/vol3/1000/dockerbackup;);
可以自动恢复容器,支持异地恢复。恢复步骤注意事项:安装容器(如果已安装好可以忽略),手工停止容器,然后再执行脚本选择恢复,恢复菜单会要求输入存放备份的路径,和选择用哪个版本的备份进行恢复,提示脚本提示恢复完成后通常容器会自动拉起,如果发现恢复不成功,可以再此尝试(不成功的情况非常罕见,在多次多环境验证的情况下只发生1,2次应该是容器停止时出现某些异常造成的)。
对于自动备份的部分,包括定义的备份时间、备份副本数、存储路径、提示信息,都可以到脚本中做与你的环境和需求相适应的变更。
#!/bin/bash
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
# 配置
MAX_BACKUPS=3
AUTO_BACKUP_ROOT="/vol3/1000/dockerbackup" # 自动备份固定目录
# 显示标题
function show_header() {
clear
echo -e "${CYAN}${BOLD}===============================================================${NC}"
echo -e "${CYAN}${BOLD} Docker 容器备份恢复工具${NC}"
echo -e "${CYAN}${BOLD} 支持异地恢复${NC}"
echo -e "${CYAN}${BOLD}===============================================================${NC}"
echo ""
}
# 检查 Docker 状态
function check_docker() {
if ! docker info >/dev/null 2>&1; then
echo -e "${RED}错误: Docker 未运行或当前用户无权限${NC}"
return 1
fi
return 0
}
# 主菜单
function main_menu() {
while true; do
show_header
echo -e "${CYAN}${BOLD}主菜单${NC}"
echo -e "==============================================================="
echo -e "1. 备份容器 (手动)"
echo -e "2. 恢复容器"
echo -e "3. 查看现有备份"
echo -e "4. 安装自动备份定时任务"
echo -e "0. 退出"
echo -e "==============================================================="
read -p "请选择操作 (0-4): " choice
case $choice in
1) backup_menu ;;
2) restore_menu ;;
3) view_backups ;;
4) install_cron_** ;;
0)
echo -e "${GREEN}再见!${NC}"
exit 0
;;
*)
echo -e "${RED}无效选择${NC}"
sleep 1
;;
esac
done
}
# 备份菜单
function backup_menu() {
show_header
echo -e "${CYAN}${BOLD}备份容器${NC}"
echo -e "==============================================================="
if ! check_docker; then
read -p "按任意键返回..." -n 1 -s
echo
return
fi
# 显示运行中的容器
echo -e "${YELLOW}运行中的容器:${NC}"
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
echo ""
read -p "请输入要备份的容器名称: " container_name
if [ -z "$container_name" ]; then
echo -e "${RED}容器名称不能为空${NC}"
read -p "按任意键返回..." -n 1 -s
echo
return
fi
if ! docker inspect "$container_name" &>/dev/null; then
echo -e "${RED}容器不存在: $container_name${NC}"
read -p "按任意键返回..." -n 1 -s
echo
return
fi
# 获取备份路径
read -p "请输入备份存储路径: " backup_root
if [ -z "$backup_root" ]; then
echo -e "${RED}备份路径不能为空${NC}"
read -p "按任意键返回..." -n 1 -s
echo
return
fi
# 创建备份目录
mkdir -p "$backup_root" || {
echo -e "${RED}无法创建备份目录: $backup_root${NC}"
read -p "按任意键返回..." -n 1 -s
echo
return
}
# 执行备份
if backup_container "$container_name" "$backup_root"; then
echo -e "${GREEN}备份完成!${NC}"
else
echo -e "${RED}备份失败!${NC}"
fi
read -p "按任意键返回..." -n 1 -s
echo
}
# 备份容器函数(修复符号链接问题)
function backup_container() {
local container_name=$1
local backup_root=$2
local timestamp=$(date +%Y%m%d_%H%M%S)
local backup_dir="${backup_root}/${container_name}"
local backup_file="${backup_dir}/${container_name}_${timestamp}.tar.gz"
mkdir -p "$backup_dir"
echo -e "${YELLOW}开始备份容器: $container_name${NC}"
echo -e "${GREEN}备份存储路径: $backup_dir${NC}"
# 特殊处理 wizserver 的符号链接问题
if [ "$container_name" = "wizserver" ]; then
echo -e "${CYAN}检测到 wizserver,处理符号链接...${NC}"
# 符号链接路径
local symlink_path="/var/apps/docker-wizserver/shares/wizdata"
# 真实路径(从符号链接解析)
local real_path=$(readlink -f "$symlink_path")
if [ -z "$real_path" ]; then
echo -e "${RED}错误: 无法解析符号链接${NC}"
return 1
fi
echo -e "${YELLOW}符号链接: $symlink_path${NC}"
echo -e "${YELLOW}真实路径: $real_path${NC}"
# 验证真实路径
if [ ! -d "$real_path" ]; then
echo -e "${RED}错误: 真实路径不存在: $real_path${NC}"
return 1
fi
echo -e "${GREEN}真实路径大小: $(du -sh "$real_path")${NC}"
# 使用真实路径进行备份
local backup_path="$real_path"
else
# 其他容器的标准处理
local volumes=$(docker inspect --format='{{range .Mounts}}{{if eq .Type "bind"}}{{.Source}} {{end}}{{end}}' "$container_name")
if [ -z "$volumes" ]; then
echo -e "${RED}未找到数据卷路径${NC}"
return 1
fi
local backup_path="$volumes"
fi
echo -e "${GREEN}备份路径: $backup_path${NC}"
# 停止容器
echo -e "${YELLOW}停止容器...${NC}"
docker stop "$container_name"
# 创建备份
echo -e "${YELLOW}创建备份文件...${NC}"
tar -czf "$backup_file" -C "$backup_path" . 2>/dev/null
# 启动容器
docker start "$container_name"
# 检查备份结果
local backup_size=$(du -h "$backup_file" | cut -f1)
local size_bytes=$(du -b "$backup_file" | cut -f1)
local file_count=$(tar -tzf "$backup_file" 2>/dev/null | wc -l)
echo -e "${GREEN}备份结果:${NC}"
echo -e "文件: $backup_file"
echo -e "大小: $backup_size"
echo -e "文件数: $file_count"
# 验证备份有效性
if [ $size_bytes -lt 1048576 ]; then # 小于1MB
echo -e "${RED}警告: 备份文件过小,可能未包含实际数据${NC}"
return 1
fi
# 清理旧备份
local backups=($(ls -t "${backup_dir}/${container_name}"_*.tar.gz 2>/dev/null))
if [ ${#backups[@]} -gt $MAX_BACKUPS ]; then
for ((i = MAX_BACKUPS; i < ${#backups[@]}; i++)); do
rm -f "${backups[$i]}"
echo -e "${YELLOW}删除旧备份: ${backups[$i]}${NC}"
done
fi
return 0
}
# 恢复菜单
function restore_menu() {
show_header
echo -e "${CYAN}${BOLD}恢复容器${NC}"
echo -e "==============================================================="
# 获取备份根目录
read -p "请输入备份存储路径: " backup_root
if [ -z "$backup_root" ]; then
echo -e "${RED}备份路径不能为空${NC}"
read -p "按任意键返回..." -n 1 -s
echo
return
fi
# 检查备份目录
if [ ! -d "$backup_root" ]; then
echo -e "${RED}备份目录不存在: $backup_root${NC}"
read -p "按任意键返回..." -n 1 -s
echo
return
fi
# 获取有备份的容器列表
local containers=()
for dir in "$backup_root"/*/; do
if [ -d "$dir" ]; then
local container_name=$(basename "$dir")
if [ "$(ls -A "$dir" 2>/dev/null)" ]; then
containers+=("$container_name")
fi
fi
done
if [ ${#containers[@]} -eq 0 ]; then
echo -e "${YELLOW}未找到任何备份${NC}"
read -p "按任意键返回..." -n 1 -s
echo
return
fi
# 显示容器列表
echo -e "${YELLOW}有备份的容器:${NC}"
for i in "${!containers[@]}"; do
local count=$(ls "${backup_root}/${containers[$i]}"/*.tar.gz 2>/dev/null | wc -l)
echo "$((i+1)). ${containers[$i]} ($count 个备份)"
done
read -p "请选择容器编号 (1-${#containers[@]}): " choice
if ! [[ "$choice" =~ ^[0-9]+$ ]] || [ "$choice" -lt 1 ] || [ "$choice" -gt ${#containers[@]} ]; then
echo -e "${RED}无效选择${NC}"
read -p "按任意键返回..." -n 1 -s
echo
return
fi
local container_name="${containers[$((choice-1))]}"
restore_container "$container_name" "$backup_root"
}
# 恢复容器(增强版,支持异地恢复)
function restore_container() {
local container_name=$1
local backup_root=$2
local backup_dir="${backup_root}/${container_name}"
local backups=($(ls -t "$backup_dir"/*.tar.gz 2>/dev/null))
if [ ${#backups[@]} -eq 0 ]; then
echo -e "${RED}未找到备份文件${NC}"
return 1
fi
# 显示备份列表
echo -e "${YELLOW}可用的备份:${NC}"
for i in "${!backups[@]}"; do
local file="${backups[$i]}"
local size=$(du -h "$file" | cut -f1)
local date=$(stat -c %y "$file" | cut -d' ' -f1)
local time=$(stat -c %y "$file" | cut -d' ' -f2 | cut -d'.' -f1)
echo "$((i+1)). $(basename "$file")"
echo " 大小: $size | 时间: $date $time"
done
read -p "请选择备份编号 (1-${#backups[@]}): " backup_choice
if ! [[ "$backup_choice" =~ ^[0-9]+$ ]] || [ "$backup_choice" -lt 1 ] || [ "$backup_choice" -gt ${#backups[@]} ]; then
echo -e "${RED}无效选择${NC}"
return 1
fi
local selected_backup="${backups[$((backup_choice-1))]}"
echo -e "${RED}警告: 这将覆盖容器 $container_name 的现有数据!${NC}"
read -p "确认恢复? (y/N): " confirm
if [[ ! $confirm =~ ^[Yy]$ ]]; then
echo -e "${YELLOW}取消恢复${NC}"
return
fi
# 执行恢复
echo -e "${YELLOW}开始恢复...${NC}"
# 步骤1: 确定恢复路径
local restore_path=""
local path_source=""
# 选项1: 容器存在时使用其当前路径
if docker inspect "$container_name" &>/dev/null; then
restore_path=$(docker inspect --format='{{range .Mounts}}{{if eq .Type "bind"}}{{.Source}}{{break}}{{end}}{{end}}' "$container_name")
path_source="当前容器配置"
fi
# 选项2: 用户手动输入路径
if [ -z "$restore_path" ]; then
echo -e "${YELLOW}容器不存在或无法获取路径${NC}"
echo -e "${CYAN}请输入恢复路径 (例如: /vol3/@appshare/wizdata)${NC}"
read -p "恢复路径: " restore_path
if [ ! -d "$restore_path" ]; then
echo -e "${RED}错误: 路径不存在${NC}"
return 1
fi
path_source="手动输入"
fi
echo -e "${GREEN}恢复路径 ($path_source): $restore_path${NC}"
# 步骤2: 验证备份内容
echo -e "${YELLOW}备份内容预览:${NC}"
tar -tzf "$selected_backup" | head -10
echo -e "${YELLOW}... (共 $(tar -tzf "$selected_backup" | wc -l) 个文件)${NC}"
# 步骤3: 停止容器(如果存在)
if docker inspect "$container_name" &>/dev/null; then
echo -e "${YELLOW}停止容器...${NC}"
docker stop "$container_name"
fi
# 步骤4: 解压备份
echo -e "${YELLOW}解压备份文件到: $restore_path${NC}"
tar -xzf "$selected_backup" -C "$restore_path"
if [ $? -eq 0 ]; then
echo -e "${GREEN}数据恢复成功${NC}"
# 步骤5: 启动容器(如果存在)
if docker inspect "$container_name" &>/dev/null; then
echo -e "${YELLOW}启动容器...${NC}"
docker start "$container_name"
fi
else
echo -e "${RED}数据恢复失败${NC}"
return 1
fi
# 步骤6: 验证恢复
echo -e "${YELLOW}验证恢复结果:${NC}"
echo -e "恢复路径: $restore_path"
echo -e "内容预览:"
ls -la "$restore_path" | head -5
echo -e "大小: $(du -sh "$restore_path")"
echo -e "${GREEN}恢复完成!${NC}"
}
# 查看备份
function view_backups() {
show_header
echo -e "${CYAN}${BOLD}查看现有备份${NC}"
echo -e "==============================================================="
# 获取备份根目录
read -p "请输入备份存储路径: " backup_root
if [ -z "$backup_root" ]; then
echo -e "${RED}备份路径不能为空${NC}"
read -p "按任意键返回..." -n 1 -s
echo
return
fi
# 检查备份目录
if [ ! -d "$backup_root" ]; then
echo -e "${YELLOW}备份目录不存在: $backup_root${NC}"
read -p "按任意键返回..." -n 1 -s
echo
return
fi
echo -e "${GREEN}备份根目录: $backup_root${NC}"
echo ""
local found=0
for container_dir in "$backup_root"/*/; do
if [ -d "$container_dir" ]; then
local container_name=$(basename "$container_dir")
local backups=($(ls -t "$container_dir"/*.tar.gz 2>/dev/null))
if [ ${#backups[@]} -gt 0 ]; then
found=1
echo -e "${GREEN}容器: $container_name${NC}"
echo -e "${BLUE}备份目录: $container_dir${NC}"
for backup in "${backups[@]}"; do
local size=$(du -h "$backup" | cut -f1)
local date=$(stat -c %y "$backup" | cut -d' ' -f1-2)
local full_path=$(realpath "$backup")
echo " - 文件名: $(basename "$backup")"
echo " 完整路径: $full_path"
echo " 大小: $size | 时间: $date"
echo ""
done
echo ""
fi
fi
done
if [ $found -eq 0 ]; then
echo -e "${YELLOW}未找到任何备份文件${NC}"
fi
read -p "按任意键返回..." -n 1 -s
echo
}
# 自动备份函数
function auto_backup() {
local container_name="wizserver" # 固定备份为知笔记容器
local backup_root="$AUTO_BACKUP_ROOT"
echo -e "${CYAN}${BOLD}开始自动备份: $container_name${NC}"
echo -e "${GREEN}备份存储路径: $backup_root${NC}"
# 检查容器是否存在
if ! docker inspect "$container_name" &>/dev/null; then
echo -e "${RED}错误: 容器 $container_name 不存在${NC}"
return 1
fi
# 创建备份目录
mkdir -p "$backup_root" || {
echo -e "${RED}无法创建备份目录: $backup_root${NC}"
return 1
}
# 执行备份
if backup_container "$container_name" "$backup_root"; then
echo -e "${GREEN}自动备份完成!${NC}"
return 0
else
echo -e "${RED}自动备份失败!${NC}"
return 1
fi
}
# 安装定时任务
function install_cron_**() {
show_header
echo -e "${CYAN}${BOLD}安装自动备份定时任务${NC}"
echo -e "==============================================================="
# 获取脚本绝对路径
local script_path=$(realpath "$0")
local cron_entry="0 22 * * * $script_path --auto-backup"
# 检查是否已存在定时任务
if crontab -l | grep -Fq "$script_path --auto-backup"; then
echo -e "${YELLOW}定时任务已存在:${NC}"
crontab -l | grep -F "$script_path --auto-backup"
echo -e "${YELLOW}无需重复安装${NC}"
read -p "按任意键返回..." -n 1 -s
echo
return
fi
# 添加定时任务
(crontab -l 2>/dev/null; echo "$cron_entry") | crontab -
if [ $? -eq 0 ]; then
echo -e "${GREEN}定时任务安装成功!${NC}"
echo -e "定时任务内容:"
echo -e "${BLUE}$cron_entry${NC}"
echo -e "将在每天 22:00 (晚上10点) 自动备份为知笔记容器"
else
echo -e "${RED}定时任务安装失败${NC}"
fi
read -p "按任意键返回..." -n 1 -s
echo
}
# 脚本入口
function main() {
# 检查参数
if [ "$1" = "--auto-backup" ]; then
auto_backup
exit $?
fi
if [ "$1" = "--install-cron" ]; then
install_cron_**
exit $?
fi
# 进入主菜单
main_menu
}
# 确保脚本有执行权限
if [ -f "$0" ]; then
main "$@"
else
echo "错误: 脚本文件不存在"
exit 1
fi