一键脚本
bash <(curl -sL gitee.com/meimolihan/cmdbox/raw/master/sh/docker_images_all_bak.sh) /备份目录 <备份数量>
bash <(curl -sL ../sh/docker_images_all_bak.sh) /备份目录 <备份数量>
传参指定备份根目录:
bash <(curl -sL gitee.com/meimolihan/cmdbox/raw/master/sh/docker_images_all_bak.sh) /vol2/1000/file/myfile/docker/backup 5
bash <(curl -sL ../sh/docker_images_all_bak.sh) /vol2/1000/file/myfile/docker/backup 5
效果预览
补充说明
该脚本用于全量备份 Docker 所有镜像,自动创建时间戳备份目录、逐镜像 docker save 打包并压缩、生成 manifest.json 清单,同时自动生成 restore_images.sh 恢复脚本,适合迁移服务器前完整打包镜像环境。
功能特点
- 全量备份:自动获取并备份所有 Docker 镜像
- 自动命名:按时间戳生成 images_backup_YYYYMMDD_HHMMSS 备份目录
- 智能压缩:优先使用 pigz 多线程压缩,备用 gzip
- 清单生成:manifest.json 记录每个镜像名称与备份文件
- 自动生成恢复脚本:备份时自动创建 restore_images.sh 方便快速恢复
- 容错处理:单个镜像备份失败自动跳过,不影响其他镜像
- 依赖自动安装:自动安装 tar、jq、gzip、pigz(支持多包管理器)
语法
docker_images_all_bak.sh [备份根目录]
参数
[备份根目录] 备份存放根目录(默认: /mnt/backup_images)
输出说明
脚本输出包含以下字段:
| 字段 | 说明 |
|---|---|
| 备份目录 | 本次备份生成的时间戳目录 |
| 备份进度 | 每个镜像的备份状态和进度 |
| 成功镜像 | 备份成功数量/总数量 |
| 备份大小 | 备份目录总占用空间 |
| 恢复脚本 | 自动生成的 restore_images.sh 路径 |
注意事项
- 备份默认存储在 /mnt/backup_images 目录,可传参指定其他根目录
- 需要已安装并运行 Docker(脚本会先检测 docker 可用性)
- 备份时会生成 restore_images.sh 恢复脚本,可在目标服务器直接执行
- 单个镜像备份失败会自动跳过,结束后统计失败数量
- 脚本需要 root 权限或 sudo 权限来安装依赖
- 建议使用 pigz 提高压缩速度(脚本会自动安装)
脚本源码
传参:脚本.sh /备份根目录
不传参:脚本.sh 默认备份到 /mnt/backup_images
#!/bin/bash
set -uo pipefail
list_color_init() {
export gl_hui=$'\033[38;5;59m'
export gl_hong=$'\033[38;5;9m'
export gl_lv=$'\033[38;5;10m'
export gl_huang=$'\033[38;5;11m'
export gl_lan=$'\033[38;5;32m'
export gl_bai=$'\033[38;5;15m'
export gl_zi=$'\033[38;5;13m'
export gl_bufan=$'\033[38;5;14m'
export reset=$'\033[0m'
}
list_color_init
log_info() { echo -e "${gl_lan}[信息]${gl_bai} $*"; }
log_ok() { echo -e "${gl_lv}[成功]${gl_bai} $*"; }
log_warn() { echo -e "${gl_huang}[警告]${gl_bai} $*"; }
log_error() { echo -e "${gl_hong}[错误]${gl_bai} $*" >&2; }
sleep_fractional() {
local seconds=$1
if sleep "$seconds" 2>/dev/null; then return 0; fi
if command -v perl >/dev/null 2>&1; then perl -e "select(undef, undef, undef, $seconds)"; return 0; fi
if command -v python3 >/dev/null 2>&1; then python3 -c "import time; time.sleep($seconds)"; return 0; fi
if command -v python >/dev/null 2>&1; then python -c "import time; time.sleep($seconds)"; return 0; fi
local int_seconds=$(awk -v s="$seconds" 'BEGIN{print int(s+0.999)}')
sleep "$int_seconds"
}
install() {
[[ $# -eq 0 ]] && { log_error "未提供软件包参数!"; return 1; }
local pkg mgr installed=false ver=""
for pkg in "$@"; do
installed=false; ver=""
if command -v "$pkg" &>/dev/null; then
cmd_ver=$("$pkg" --version 2>/dev/null | head -n1 | tr -cd '[:print:]' | grep -oE '[0-9]+(\.[0-9]+)+' | head -n1 || echo "")
[[ -n "$cmd_ver" ]] && ver="$cmd_ver" && installed=true
fi
if [[ "$pkg" == "7zip" || "$pkg" == "7z" ]]; then
command -v 7z &>/dev/null && { ver=$(7z 2>&1 | grep -oE '[0-9]+(\.[0-9]+)+' | head -n1 || echo ""); installed=true; }
fi
if [[ "$installed" == false ]]; then
if command -v opkg &>/dev/null; then
opkg list-installed | grep -q "^${pkg} " && { installed=true; ver=$(opkg list-installed | grep "^${pkg} " | awk '{print $3}' 2>/dev/null || echo ""); }
elif command -v dpkg-query &>/dev/null; then
dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null | grep -q "install ok installed" && { installed=true; ver=$(dpkg-query -W -f='${Version}' "$pkg" 2>/dev/null || echo ""); }
elif command -v rpm &>/dev/null; then
rpm -q "$pkg" &>/dev/null && { installed=true; ver=$(rpm -q --qf '%{VERSION}' "$pkg" 2>/dev/null || echo ""); }
elif command -v apk &>/dev/null; then
apk info "$pkg" 2>/dev/null | grep -q "^installed" && { installed=true; ver=$(apk info -a "$pkg" 2>/dev/null | grep -oE '[0-9]+(\.[0-9]+)+' | head -n1 || echo ""); }
elif command -v pacman &>/dev/null; then
pacman -Qi "$pkg" &>/dev/null && { installed=true; ver=$(pacman -Qi "$pkg" 2>/dev/null | grep -i "version" | grep -oE '[0-9]+(\.[0-9]+)+' | head -n1 || echo ""); }
fi
fi
if [[ "$installed" == true ]]; then
echo -e "${gl_huang}${pkg}${gl_bai} ${gl_lv}已安装${gl_bai}$([[ -n "$ver" ]] && echo " 版本 ${gl_lv}${ver}${gl_bai}")"
continue
fi
echo -e "\n${gl_huang}开始安装:${gl_bai}${pkg}"
local install_success=false
for mgr in opkg dnf yum apt apk pacman zypper pkg; do
command -v "$mgr" &>/dev/null || continue
case $mgr in
opkg)
[[ "$pkg" == "7zip" || "$pkg" == "7z" ]] && { opkg update && opkg install p7zip && install_success=true; } || { opkg update && opkg install "$pkg" && install_success=true; }
;;
dnf) dnf -y update && dnf install -y "$pkg" && install_success=true ;;
yum) yum -y update && yum install -y "$pkg" && install_success=true ;;
apt) apt update -y && apt install -y "$pkg" && install_success=true ;;
apk) apk update && apk add "$pkg" && install_success=true ;;
pacman) pacman -Syu --noconfirm && pacman -S --noconfirm "$pkg" && install_success=true ;;
zypper) zypper refresh && zypper install -y "$pkg" && install_success=true ;;
pkg) pkg update && pkg install -y "$pkg" && install_success=true ;;
esac
[[ "$install_success" == true ]] && break
done
[[ "$install_success" == true ]] && echo -e "${gl_lv}✓ ${pkg} 安装成功${gl_bai}" || echo -e "${gl_hong}✗ ${pkg} 安装失败${gl_bai}"
done
}
backup_all_docker_images() {
local BACKUP_ROOT="$1"
local RETAIN_COUNT="$2"
log_info "安装依赖 tar jq gzip pigz"
install tar jq gzip pigz
if ! command -v docker &>/dev/null || ! docker info &>/dev/null; then
log_error "Docker未安装或未运行"
return 1
fi
clear
echo -e "${gl_zi}>>> 备份所有 Docker 镜像${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
if [[ -n "${RETAIN_COUNT}" && "${RETAIN_COUNT}" -gt 0 ]]; then
log_info "轮转策略:总共保留 ${RETAIN_COUNT} 套备份(含本次新建)"
mapfile -t EXIST_BACKUPS < <(find "${BACKUP_ROOT}" -maxdepth 1 -type d -name "images_backup_*" | sort)
local exist_cnt=${#EXIST_BACKUPS[@]}
log_info "当前已存在历史备份: ${exist_cnt} 套"
# 本次会新增1套,所以历史最多允许保留 RETAIN_COUNT -1 套
local max_keep_history=$(( RETAIN_COUNT - 1 ))
if [[ ${exist_cnt} -gt ${max_keep_history} ]]; then
local del_cnt=$(( exist_cnt - max_keep_history ))
log_warn "历史备份超出配额,将删除最旧 ${del_cnt} 套备份"
for ((i=0; i<del_cnt; i++)); do
local rm_dir="${EXIST_BACKUPS[$i]}"
log_info "删除旧备份: ${rm_dir}"
rm -rf "${rm_dir}"
done
else
log_info "历史备份数量(${exist_cnt}) ≤ 允许历史保留(${max_keep_history}),无需删除"
fi
fi
local DATE_STR=$(date +%Y%m%d_%H%M%S)
local BACKUP_DIR="${BACKUP_ROOT}/images_backup_${DATE_STR}"
mkdir -p "$BACKUP_DIR"
log_info "本次备份目录: ${BACKUP_DIR}"
mapfile -t IMAGES < <(docker images --format "{{.Repository}}:{{.Tag}}" | grep -v "<none>" | grep -v "REPOSITORY:TAG")
if [[ ${#IMAGES[@]} -eq 0 ]]; then
log_warn "未找到任何Docker镜像,退出"
return 0
fi
log_info "共计 ${#IMAGES[@]} 个镜像,全部开始备份"
local RESTORE_SCRIPT="${BACKUP_DIR}/restore_images.sh"
cat >"$RESTORE_SCRIPT" <<'EOF'
#!/bin/bash
set -e
gl_bai='\033[0m';gl_bufan='\033[96m';gl_lv='\033[32m';gl_huang='\033[33m';gl_hong='\033[31m';gl_zi='\033[35m'
BACKUP_DIR="$(cd "$(dirname "$0")"; pwd)"
MANIFEST="${BACKUP_DIR}/manifest.json"
[[ ! -f "$MANIFEST" ]] && { echo -e "${gl_hong}错误:未找到manifest.json${gl_bai}";exit 1; }
IMAGE_COUNT=$(jq '.images | length' "$MANIFEST")
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_zi}>>>恢复Docker镜像${gl_bai}"
for i in $(seq 0 $((IMAGE_COUNT-1))); do
IMAGE_NAME=$(jq -r ".images[$i].name" "$MANIFEST")
IMAGE_FILE=$(jq -r ".images[$i].file" "$MANIFEST")
IMAGE_PATH="${BACKUP_DIR}/${IMAGE_FILE}"
echo -e "${gl_bai}[$((i+1))/${IMAGE_COUNT}]加载镜像:${gl_huang}${IMAGE_NAME}${gl_bai}"
[[ -f "$IMAGE_PATH" ]] && docker load -i "$IMAGE_PATH" && echo -e "${gl_lv}✓完成${gl_bai}" || echo -e "${gl_hong}✗文件不存在${IMAGE_FILE}${gl_bai}"
done
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
echo -e "${gl_lv}全部镜像加载完成!执行docker images查看${gl_bai}"
EOF
chmod +x "$RESTORE_SCRIPT"
local MANIFEST="${BACKUP_DIR}/manifest.json"
cat >"$MANIFEST" <<EOF
{
"backup_date": "$(date -Iseconds)",
"images": []
}
EOF
local success_count=0 fail_count=0
for i in "${!IMAGES[@]}"; do
local image="${IMAGES[i]}"
local safe_name=$(echo "$image" | sed 's/[\/:]/-/g' | sed 's/^-*//')
local backup_file="${safe_name}.tar"
echo -e "${gl_bai}[$((i + 1))/${#IMAGES[@]}]备份镜像: ${gl_huang}${image}${gl_bai}"
if docker save -o "${BACKUP_DIR}/${backup_file}" "$image" 2>/dev/null; then
if command -v pigz &>/dev/null; then pigz "${BACKUP_DIR}/${backup_file}"; backup_file="${backup_file}.gz";
else gzip "${BACKUP_DIR}/${backup_file}"; backup_file="${backup_file}.gz"; fi
jq --arg name "$image" --arg file "$backup_file" '.images += [{"name":$name,"file":$file}]' "$MANIFEST" >"${MANIFEST}.tmp" && mv "${MANIFEST}.tmp" "$MANIFEST"
echo -e "${gl_lv}✓成功${gl_bai}"
((success_count++))
else
echo -e "${gl_hong}✗失败${gl_bai}"
((fail_count++))
fi
done
local total_size=$(du -sh "$BACKUP_DIR" 2>/dev/null | cut -f1 || echo "未知")
echo -e "${gl_bufan}================================================${gl_bai}"
log_ok "镜像备份任务结束"
echo -e "备份目录 : ${gl_huang}${BACKUP_DIR}${gl_bai}"
echo -e "成功镜像 : ${gl_huang}${success_count}/${#IMAGES[@]}${gl_bai}"
echo -e "备份总大小: ${gl_huang}${total_size}${gl_bai}"
echo -e "恢复脚本 : ${gl_huang}${RESTORE_SCRIPT}${gl_bai}"
[[ $fail_count -gt 0 ]] && log_warn "有 ${fail_count} 个镜像备份失败"
echo -e "${gl_bufan}================================================${gl_bai}"
}
ARG_BACKUP_ROOT="/mnt/backup_images"
ARG_RETAIN=""
for arg in "$@"; do
if [[ "$arg" =~ ^[0-9]+$ ]]; then
ARG_RETAIN="$arg"
else
ARG_BACKUP_ROOT="$arg"
fi
done
backup_all_docker_images "${ARG_BACKUP_ROOT}" "${ARG_RETAIN}"
创建本地脚本
new_script="docker_images_all_bak.sh"
cat > "$new_script" << 'EOF'
#!/bin/bash
# 粘贴脚本源码
EOF
chmod +x "$new_script" && ./"$new_script" && rm -f "$new_script"
相关命令
- docker-hub_images_check
- docker-hub_images_delete
- docker-hub_images_delete_menu
- docker_build_push_img
- docker_check_env
- docker_clean_all
- docker_container_export
- docker_container_import
- docker_daily_scripts
- docker_disk_list
- docker_images_all_bak 👈 当前所在位置
- docker_images_all_rst 👈 配合使用的还原工具
- docker_images_list
- docker_images_manager
- docker_install_uninstall
- docker_login
- docker_login_check
- docker_network_list
- docker_occupy_list
- docker_port_list
- docker_ps_list
- docker_update_all
- docker_user
- docker_volume_list