利用restic备份N1数据
Janz Lv4

利用restic备份N1数据

最近搞定了ipv6,同时家里有台N1没跑满,就想把一些小的服务仍家里N1跑,但是不太放心N1的稳定性,所以决定利用restic定时备份到U盘,遂让Ai帮忙写了一个bash脚本。

需求

  • 按需挂载/卸载U盘

  • 能够停止/开启特定的docker容器

    • 不少容器不支持热备份数据,需要停止后再启动
  • 备份特定的文件夹

  • 仅保留最近的几个版本减少备份的体积

  • 较为完善的检查和通知功能

  • 成功和失败后能通过tg bot通知

依赖

  • docker
  • docker compose
  • curl
  • restic

脚本

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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
#!/bin/bash

# 配置变量
USB_DEVICE="/dev/sda1" # U盘设备路径
MOUNT_POINT="/mnt/sda1" # U盘挂载点
RESTIC_REPO="$MOUNT_POINT/restic-repo" # Restic仓库路径
RESTIC_PASSWORD="<password>" # Restic仓库密码
DOCKERFILES_DIR="/root/.dockerfile" # dockerfiles 文件夹路径
TELEGRAM_API_URL="https://api.telegram.org/bot<token>/sendmessage" # 替换为你的 Telegram Bot API URL
CHAT_ID="<chat_id>" # 替换为你的 Telegram Chat ID
KEEP_LAST_SNAPSHOTS_BUMBER=3 # 保留最新的几个版本

# 要备份的容器及其 Compose 文件(格式:容器名称:compose文件名)
# 如果为空,将备份所有容器
CONTAINERS_TO_BACKUP=(
"memos:compose-memos.yml"
# "container2:docker-compose2.yml"
)

# 要备份的普通文件夹路径
DIRECTORIES_TO_BACKUP=(
"/root/.backups"
)

# 函数: 错误处理
error_exit() {
echo "错误: $@" >&2
send_telegram "备份失败: $@"
exit 1
}

# 函数: 检查命令是否存在
check_command() {
command -v $1 >/dev/null 2>&1 || error_exit "需要$1但未安装。 退出。"
}

# 函数: 发送Telegram通知
send_telegram() {
local message=$1
curl -s -X POST "$TELEGRAM_API_URL" -d chat_id="$CHAT_ID" -d text="$message" >/dev/null 2>&1
}

# 检查必要的命令
check_command restic
check_command docker
check_command docker-compose
check_command curl

# 挂载U盘
mount_usb() {
if ! mountpoint -q "$MOUNT_POINT"; then
mkdir -p "$MOUNT_POINT"
mount "$USB_DEVICE" "$MOUNT_POINT" || error_exit "无法挂载U盘"
else
echo "U盘已经挂载"
fi
}

# 卸载U盘
unmount_usb() {
if mountpoint -q "$MOUNT_POINT"; then
umount "$MOUNT_POINT" || error_exit "无法卸载U盘"
rmdir "$MOUNT_POINT"
else
echo "U盘未挂载"
fi
}

# 获取所有容器和对应的Compose文件
get_all_containers() {
local containers=()
for compose_file in "$DOCKERFILES_DIR"/*.yml; do
if [ -f "$compose_file" ]; then
local file_name=$(basename "$compose_file")
local project_name=$(docker-compose -f "$compose_file" config --services)
for service in $project_name; do
containers+=("$service:$file_name")
done
fi
done
echo "${containers[@]}"
}

# 停止指定的Docker容器
stop_containers() {
local containers=("$@")
for container in "${containers[@]}"; do
IFS=':' read -r -a array <<< "$container"
container_name="${array[0]}"
compose_file="${array[1]}"
echo "停止容器: $container_name"
docker-compose -f "$DOCKERFILES_DIR/$compose_file" stop "$container_name" || error_exit "停止容器 $container_name 失败"
done
}

# 启动之前停止的Docker容器
start_containers() {
local containers=("$@")
for container in "${containers[@]}"; do
IFS=':' read -r -a array <<< "$container"
container_name="${array[0]}"
compose_file="${array[1]}"
echo "启动容器: $container_name"
docker-compose -f "$DOCKERFILES_DIR/$compose_file" start "$container_name" || error_exit "启动容器 $container_name 失败"
done
}

# 执行Restic备份
do_backup() {
export RESTIC_PASSWORD
if [ ! -d "$RESTIC_REPO" ]; then
restic init --repo "$RESTIC_REPO" || error_exit "初始化Restic仓库失败"
fi

for container in "$@"; do
IFS=':' read -r -a array <<< "$container"
container_name="${array[0]}"
echo "备份容器 $container_name 的数据"
container_id=$(docker ps -aqf "name=$container_name")
if [ -z "$container_id" ]; then
echo "警告: 找不到容器 $container_name,跳过"
continue
fi

# 获取容器的挂载点
mounts=$(docker inspect -f '{{range .Mounts}}{{.Source}}{{"\n"}}{{end}}' "$container_id")

# 备份每个挂载点
while IFS= read -r mount; do
if [ -n "$mount" ]; then
echo "备份挂载点: $mount"
restic -r "$RESTIC_REPO" backup "$mount" --tag "$container_name" || error_exit "备份 $container_name$mount 失败"
fi
done <<< "$mounts"
done

for directory in "${DIRECTORIES_TO_BACKUP[@]}"; do
if [ -d "$directory" ]; then
echo "备份文件夹: $directory"
restic -r "$RESTIC_REPO" backup "$directory" --tag "directory_backup" || error_exit "备份目录 $directory 失败"
else
echo "警告: 找不到目录 $directory,跳过"
fi
done
}

# 保留最新的3个快照,减少备份的体积
do_keep_last_snapshots() {
export RESTIC_PASSWORD
if [ ! -d "$RESTIC_REPO" ]; then
restic init --repo "$RESTIC_REPO" || error_exit "初始化Restic仓库失败"
fi
echo "清理仓库,只保留最新 $KEEP_LAST_SNAPSHOTS_BUMBER 的快照"
restic -r "$RESTIC_REPO" forget --keep-last $KEEP_LAST_SNAPSHOTS_BUMBER --prune || error_exit "清理仓库失败"
}

# 主执行流程
main() {
local containers_to_process=()

if [ ${#CONTAINERS_TO_BACKUP[@]} -eq 0 ]; then
echo "CONTAINERS_TO_BACKUP 为空,将备份所有容器"
containers_to_process=($(get_all_containers))
else
containers_to_process=("${CONTAINERS_TO_BACKUP[@]}")
fi

mount_usb
stop_containers "${containers_to_process[@]}"
do_backup "${containers_to_process[@]}"
do_keep_last_snapshots
start_containers "${containers_to_process[@]}"
unmount_usb
echo "备份完成"
send_telegram "备份成功: 所有容器和文件夹的备份已完成"
}

# 执行主函数
main

exit 0

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep