Docker 镜像备份/恢复管理脚本
年初的时候做了个 飞牛NAS Docker 容器镜像备份 的脚本,实际使用中有些不方便的,做了点升级,实现指定或者批量备份容器中镜像,自动根据项目文件夹中的docker-compose.yml(yaml)单个或者批量恢复项目。





具体脚本代码如下,可以保存成 xxx.sh,比如docker.sh,通过命令 bash ./docker.sh 执行,需要root权限。
#!/bin/bash
# =============================================
# Docker 镜像备份/恢复管理脚本
# 作者:心若随风
# =============================================
# 权限检查
if [ "$(id -u)" -ne 0 ]; then
echo -e "\033[31m错误:必须使用 root 权限运行此脚本!\033[0m"
exit 1
fi
# 样式
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
BLUE='\033[0;34m'; CYAN='\033[38;5;51m'; NC='\033[0m'
CHECKMARK="✅"; CROSS="❌"; PROCESS="⚙️"; INFO='\033[0;36m'
# 全局变量
found_projects=()
PAGE_SIZE=0
BASE_PATH=""
IMAGE_BAK_DIR="$(pwd)/Image_Bak"
# 工具函数
ensure_dir_exists() { [ ! -d "$1" ] && mkdir -p "$1"; }
get_auto_page_size() {
local lines=$(tput lines 2>/dev/null || echo 24)
PAGE_SIZE=$((lines-16))
((PAGE_SIZE<5)) && PAGE_SIZE=5
((PAGE_SIZE>20)) && PAGE_SIZE=20
}
print_title() {
clear
echo -e "${CYAN}=====================================================${NC}"
echo -e "${CYAN} Docker 镜像备份/恢复管理脚本 by 心若随风 ${NC}"
echo -e "${CYAN}=====================================================${NC}\n"
}
# 扫描 Docker 项目
scan_projects() {
found_projects=()
echo -e "${BLUE}🔍 正在扫描 [$BASE_PATH] 下的 Docker Compose 项目...${NC}"
while IFS= read -r -d '' dir; do
if [ -f "$dir/docker-compose.yml" ] || [ -f "$dir/docker-compose.yaml" ]; then
found_projects+=("$dir")
fi
done < <(find "$BASE_PATH" -maxdepth 2 -type d -print0 2>/dev/null)
[ ${#found_projects[@]} -eq 0 ] && { echo -e "${RED}${CROSS} 未找到 Docker Compose 项目!${NC}"; exit 1; }
}
# 获取项目镜像
get_images_from_compose() {
local dir="$1"
local compose_file="$dir/docker-compose.yml"
[ ! -f "$compose_file" ] && compose_file="$dir/docker-compose.yaml"
docker compose -f "$compose_file" config 2>/dev/null | awk '/image:/ {print $2}'
}
# ======================== 备份 ========================
# 全部本地镜像
backup_all_images() {
ensure_dir_exists "$IMAGE_BAK_DIR"
echo -e "\n${BLUE}开始备份所有本地镜像...${NC}"
mapfile -t all_images < <(docker image ls --format "{{.Repository}}:{{.Tag}}" | sort -u)
if [ ${#all_images[@]} -eq 0 ]; then
echo -e "${YELLOW}⚠️ 当前没有本地镜像,无法备份${NC}"
read -p "按任意键返回主菜单..."
return
fi
for img in "${all_images[@]}"; do
[ "$img" = "<none>:<none>" ] && continue
safe_name=$(echo "$img" | tr '/:' '__')
tar_file="$IMAGE_BAK_DIR/${safe_name}.tar"
[ -f "$tar_file" ] && rm -f "$tar_file"
echo -e "${PROCESS} 备份镜像: ${YELLOW}$img${NC}"
docker save -o "$tar_file" "$img" && echo -e "${GREEN}${CHECKMARK} 已保存: $tar_file${NC}" || echo -e "${RED}${CROSS} 保存失败: $img${NC}"
done
read -p "按任意键返回主菜单..."
}
# 单个镜像
backup_single_image() {
ensure_dir_exists "$IMAGE_BAK_DIR"
mapfile -t all_images < <(docker image ls --format "{{.Repository}}:{{.Tag}}" | sort -u)
if [ ${#all_images[@]} -eq 0 ]; then
echo -e "${YELLOW}⚠️ 当前没有本地镜像可备份${NC}"
read -p "按任意键返回主菜单..."
return
fi
echo -e "\n${BLUE}当前所有本地镜像:${NC}"
local idx=1
for img in "${all_images[@]}"; do
echo " $idx) $img"
((idx++))
done
read -p "请选择镜像编号: " num
img="${all_images[$((num-1))]}"
[ -z "$img" ] && { echo -e "${RED}${CROSS} 无效选择${NC}"; read -p "按任意键返回主菜单..."; return; }
safe_name=$(echo "$img" | tr '/:' '__')
tar_file="$IMAGE_BAK_DIR/${safe_name}.tar"
[ -f "$tar_file" ] && rm -f "$tar_file"
docker save -o "$tar_file" "$img" && echo -e "${GREEN}${CHECKMARK} 备份成功: $tar_file${NC}" || echo -e "${RED}${CROSS} 备份失败: $img${NC}"
read -p "按任意键返回主菜单..."
}
# ======================== 导入镜像 ========================
import_images() {
local dir="$1"
mapfile -t images < <(get_images_from_compose "$dir")
[ ${#images[@]} -eq 0 ] && return
for img in "${images[@]}"; do
safe_name=$(echo "$img" | tr '/:' '__')
tar_file="$IMAGE_BAK_DIR/${safe_name}.tar"
local_exist=0; bak_exist=0
docker image inspect "$img" >/dev/null 2>&1 && local_exist=1
[ -f "$tar_file" ] && bak_exist=1
echo ""
echo "================================================"
echo "镜像: $img"
echo "================================================"
if [ $local_exist -eq 1 ] && [ $bak_exist -eq 1 ]; then
echo "1) 使用容器内镜像"
echo "2) 从备份导入"
echo "3) 在线拉取最新镜像"
read -p "请选择 [1]: " choice
choice=${choice:-1}
case $choice in
2) docker load -i "$tar_file" ;;
3) docker pull "$img" ;;
*) echo "使用容器内镜像" ;;
esac
elif [ $local_exist -eq 1 ]; then
echo "1) 使用容器内镜像"
echo "2) 在线拉取最新镜像"
read -p "请选择 [1]: " choice
choice=${choice:-1}
[[ "$choice" == "2" ]] && docker pull "$img"
elif [ $bak_exist -eq 1 ]; then
echo "1) 从备份导入"
echo "2) 在线拉取最新镜像"
read -p "请选择 [1]: " choice
choice=${choice:-1}
[[ "$choice" == "1" ]] && docker load -i "$tar_file"
[[ "$choice" == "2" ]] && docker pull "$img"
else
echo "本地镜像和备份均不存在,开始在线拉取..."
docker pull "$img"
fi
done
}
# ======================== 单个项目恢复 ========================
restore_single_project() {
local dir="$1"
local project_name=$(basename "$dir")
echo -e "\n${PROCESS} 开始恢复项目:${YELLOW}$project_name${NC}"
import_images "$dir"
cd "$dir" || { echo -e "${RED}${CROSS} 无法进入项目目录 [$dir]${NC}"; return; }
docker compose up -d
[ $? -eq 0 ] && echo -e "${GREEN}${CHECKMARK} 项目 [$project_name] 恢复完成${NC}" || echo -e "${RED}${CROSS} 恢复失败 [$project_name]${NC}"
cd - >/dev/null
}
# ======================== 批量恢复 ========================
restore_all_projects() {
# ==============================
# 列出即将恢复的项目
# ==============================
echo ""
echo -e "${RED}═══════════════════════════════════════${NC}"
echo -e "${RED} 即将恢复以下项目${NC}"
echo -e "${RED}═══════════════════════════════════════${NC}"
local idx=1
for dir in "${found_projects[@]}"; do
echo " $idx) $(basename "$dir")"
((idx++))
done
echo ""
echo -e "${YELLOW}共 ${#found_projects[@]} 个项目${NC}"
echo ""
# ==============================
# 二次确认
# ==============================
read -p "请输入 YES 确认恢复全部项目: " confirm
if [ "$confirm" != "YES" ]; then
echo -e "${YELLOW}已取消操作。${NC}"
read -p "按任意键返回主菜单..."
return
fi
# ==============================
# 批量恢复逻辑
# ==============================
for dir in "${found_projects[@]}"; do
local images=($(get_images_from_compose "$dir"))
[ ${#images[@]} -eq 0 ] && continue
for img in "${images[@]}"; do
safe_name=$(echo "$img" | tr '/:' '__')
tar_file="$IMAGE_BAK_DIR/${safe_name}.tar"
if docker image inspect "$img" >/dev/null 2>&1; then
echo -e "${INFO} [$img] 已存在本地,跳过导入${NC}"
elif [ -f "$tar_file" ]; then
docker load -i "$tar_file" && echo -e "${GREEN}${CHECKMARK} 已从备份导入 [$img]${NC}" || echo -e "${RED}${CROSS} 导入失败 [$img]${NC}"
else
echo -e "${YELLOW}未找到本地备份,在线拉取 [$img]${NC}"
docker pull "$img" && echo -e "${GREEN}${CHECKMARK} 在线拉取成功 [$img]${NC}" || echo -e "${RED}${CROSS} 拉取失败 [$img]${NC}"
fi
done
cd "$dir" || continue
docker compose up -d --force-recreate
cd - >/dev/null
done
echo -e "\n${GREEN}${CHECKMARK} 全部项目恢复完成${NC}"
read -p "按任意键返回主菜单..."
}
# ======================== 菜单分页浏览 ========================
menu_list_and_restore_single() {
get_auto_page_size
local total_projects=${#found_projects[@]}
local total_pages=$(( (total_projects + PAGE_SIZE - 1) / PAGE_SIZE ))
local current_page=1
local selected_idx=""
while [ $current_page -le $total_pages ]; do
local start_idx=$(( (current_page-1) * PAGE_SIZE ))
local end_idx=$(( start_idx + PAGE_SIZE - 1 ))
[ $end_idx -ge $total_projects ] && end_idx=$(( total_projects-1 ))
echo -e "\n${CYAN}========== 第 $current_page/$total_pages 页 ==========${NC}"
for ((i=start_idx; i<=end_idx; i++)); do
echo -e " ${GREEN}$((i+1)))${NC} $(basename "${found_projects[$i]}")"
done
echo -e "${BLUE}-----------------------------------------------------${NC}"
if [ $current_page -eq $total_pages ]; then
echo -ne "${YELLOW}输入项目编号恢复(0 返回主菜单),按回车翻页:${NC}"
else
echo -ne "${YELLOW}输入项目编号恢复,按回车翻下一页:${NC}"
fi
read user_input
if [ -z "$user_input" ]; then
if [ $current_page -eq $total_pages ]; then break; else current_page=$((current_page+1)); clear; fi
else
if ! [[ "$user_input" =~ ^[0-9]+$ ]]; then
echo -e "${RED}${CROSS} 输入无效!${NC}"; read -p "按任意键继续..."; clear; continue
fi
if [ "$user_input" -eq 0 ] && [ $current_page -eq $total_pages ]; then
selected_idx="0"; break
elif [ "$user_input" -ge 1 ] && [ "$user_input" -le "$total_projects" ]; then
selected_idx="$user_input"; break
else
echo -e "${RED}${CROSS} 编号无效!${NC}"; read -p "按任意键继续..."; clear; continue
fi
fi
done
if [ "$selected_idx" = "0" ]; then return
elif [ -n "$selected_idx" ]; then
restore_single_project "${found_projects[$((selected_idx-1))]}"
read -p "按任意键返回主菜单..."
else
echo -e "${YELLOW}未选择任何项目,返回主菜单...${NC}"; read -p "按任意键继续..."
fi
}
# ======================== 主菜单 ========================
main_menu() {
while true; do
print_title
echo -e "${YELLOW}🔧 主菜单${NC}"
echo -e "${BLUE}-----------------------------------------------------${NC}"
echo -e " 1) 备份所有本地镜像"
echo -e
echo -e " 2) 备份单个本地镜像"
echo -e
echo -e " 3) 恢复单个项目(会搜索项目文件夹中的项目,输入数值恢复)"
echo -e
echo -e " 4) 恢复全部项目(会恢复项目文件夹下每个项目,慎用!!)"
echo -e
echo -e " 0) 退出"
echo -e "${BLUE}-----------------------------------------------------${NC}"
echo -ne "${YELLOW}请选择操作(0-4):${NC}"
read choice
echo ""
case $choice in
1) backup_all_images ;;
2) backup_single_image ;;
3) menu_list_and_restore_single ;;
4) restore_all_projects ;;
0) echo -e "${GREEN}👋 感谢使用!${NC}"; exit 0 ;;
*) echo -e "${RED}${CROSS} 无效选择!请输入 0-4 之间的数字${NC}"; read -p "按任意键返回主菜单..." ;;
esac
done
}
# 主入口
main() {
print_title
echo -ne "${YELLOW}请输入 Docker 项目目录路径(例如:/vol1/1000/000.Docker):\n> ${NC}"
read BASE_PATH
[ -z "$BASE_PATH" ] || [ ! -d "$BASE_PATH" ] && { echo -e "${RED}${CROSS} 路径无效!${NC}"; exit 1; }
ensure_dir_exists "$IMAGE_BAK_DIR"
scan_projects
main_menu
}
main
