#!/bin/bash # Docker镜像导出脚本 # 用于将构建好的Docker镜像导出为tar文件,便于离线部署 set -e # 脚本配置 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" EXPORT_DIR="${PROJECT_ROOT}/docker-images" LOG_FILE="${EXPORT_DIR}/export.log" # 颜色输出 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # 日志函数 log() { echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" | tee -a "$LOG_FILE" } log_success() { echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] ✓${NC} $1" | tee -a "$LOG_FILE" } log_warning() { echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] ⚠${NC} $1" | tee -a "$LOG_FILE" } log_error() { echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ✗${NC} $1" | tee -a "$LOG_FILE" } # 显示帮助信息 show_help() { cat << EOF Docker镜像导出脚本 用法: $0 [选项] 选项: -h, --help 显示此帮助信息 -o, --output DIR 指定导出目录 (默认: ./docker-images) -c, --compress 启用压缩 (使用gzip) -v, --verify 导出后验证镜像完整性 --custom-images 导出自定义镜像列表 (用逗号分隔) --skip-build 跳过镜像构建,直接导出现有镜像 示例: $0 # 导出所有镜像 $0 -c -v # 导出并压缩,验证完整性 $0 -o /tmp/images --compress # 导出到指定目录并压缩 $0 --custom-images "mysql:8.0,redis:7-alpine" # 导出指定镜像 EOF } # 默认配置 COMPRESS=false VERIFY=false SKIP_BUILD=false CUSTOM_IMAGES="" # 解析命令行参数 while [[ $# -gt 0 ]]; do case $1 in -h|--help) show_help exit 0 ;; -o|--output) EXPORT_DIR="$2" shift 2 ;; -c|--compress) COMPRESS=true shift ;; -v|--verify) VERIFY=true shift ;; --custom-images) CUSTOM_IMAGES="$2" shift 2 ;; --skip-build) SKIP_BUILD=true shift ;; *) log_error "未知参数: $1" show_help exit 1 ;; esac done # 创建导出目录 mkdir -p "$EXPORT_DIR" mkdir -p "$(dirname "$LOG_FILE")" log "开始Docker镜像导出过程..." log "导出目录: $EXPORT_DIR" # 检查Docker是否运行 if ! docker info >/dev/null 2>&1; then log_error "Docker未运行或无法访问" exit 1 fi # 定义需要导出的镜像列表 if [[ -n "$CUSTOM_IMAGES" ]]; then IFS=',' read -ra IMAGES <<< "$CUSTOM_IMAGES" else IMAGES=( "knowledge-base-app:latest" "mysql:8.0" "redis:7-alpine" "getmeili/meilisearch:v1.5" ) fi # 构建自定义镜像(如果需要) if [[ "$SKIP_BUILD" == false ]]; then log "检查是否需要构建自定义镜像..." # 检查knowledge-base-app镜像是否存在 if ! docker image inspect knowledge-base-app:latest >/dev/null 2>&1; then log "构建knowledge-base-app镜像..." cd "$PROJECT_ROOT" if docker build --platform linux/amd64 -t knowledge-base-app:latest -f Dockerfile --target production .; then log_success "knowledge-base-app镜像构建成功" else log_error "knowledge-base-app镜像构建失败" exit 1 fi else log_success "knowledge-base-app镜像已存在" fi fi # 拉取外部镜像 log "拉取外部镜像..." for image in "mysql:8.0" "redis:7-alpine" "getmeili/meilisearch:v1.5"; do if [[ " ${IMAGES[@]} " =~ " ${image} " ]]; then log "拉取镜像: $image" if docker pull --platform linux/amd64 "$image"; then log_success "镜像 $image 拉取成功" else log_warning "镜像 $image 拉取失败,将尝试使用本地镜像" fi fi done # 导出镜像 log "开始导出镜像..." EXPORTED_FILES=() for image in "${IMAGES[@]}"; do log "导出镜像: $image" # 检查镜像是否存在 if ! docker image inspect "$image" >/dev/null 2>&1; then log_error "镜像 $image 不存在,跳过导出" continue fi # 生成文件名(替换特殊字符) filename=$(echo "$image" | sed 's/[\/:]/_/g') output_file="${EXPORT_DIR}/${filename}.tar" # 导出镜像 if docker save -o "$output_file" "$image"; then log_success "镜像 $image 导出成功: $output_file" EXPORTED_FILES+=("$output_file") # 显示文件大小 size=$(du -h "$output_file" | cut -f1) log "文件大小: $size" else log_error "镜像 $image 导出失败" continue fi # 压缩文件(如果启用) if [[ "$COMPRESS" == true ]]; then log "压缩文件: $output_file" if gzip "$output_file"; then compressed_file="${output_file}.gz" log_success "文件压缩成功: $compressed_file" # 更新文件列表 EXPORTED_FILES=("${EXPORTED_FILES[@]/$output_file}") EXPORTED_FILES+=("$compressed_file") # 显示压缩后大小 compressed_size=$(du -h "$compressed_file" | cut -f1) original_size=$(du -h "$output_file" 2>/dev/null | cut -f1 || echo "N/A") log "压缩后大小: $compressed_size (原始: $original_size)" else log_error "文件压缩失败" fi fi done # 验证导出的镜像(如果启用) if [[ "$VERIFY" == true ]]; then log "验证导出的镜像..." for file in "${EXPORTED_FILES[@]}"; do if [[ -f "$file" ]]; then log "验证文件: $file" # 检查文件完整性 if [[ "$file" == *.gz ]]; then # 验证gzip文件 if gzip -t "$file"; then log_success "压缩文件完整性验证通过" else log_error "压缩文件完整性验证失败" fi else # 验证tar文件 if tar -tf "$file" >/dev/null 2>&1; then log_success "tar文件完整性验证通过" else log_error "tar文件完整性验证失败" fi fi fi done fi # 生成镜像清单 manifest_file="${EXPORT_DIR}/images-manifest.txt" log "生成镜像清单: $manifest_file" cat > "$manifest_file" << EOF # Docker镜像导出清单 # 生成时间: $(date) # 导出目录: $EXPORT_DIR # 压缩: $COMPRESS # 验证: $VERIFY EOF for file in "${EXPORTED_FILES[@]}"; do if [[ -f "$file" ]]; then filename=$(basename "$file") size=$(du -h "$file" | cut -f1) checksum=$(sha256sum "$file" | cut -d' ' -f1) echo "文件: $filename" >> "$manifest_file" echo "大小: $size" >> "$manifest_file" echo "SHA256: $checksum" >> "$manifest_file" echo "" >> "$manifest_file" fi done # 生成导入脚本 import_script="${EXPORT_DIR}/import-images.sh" log "生成导入脚本: $import_script" cat > "$import_script" << 'EOF' #!/bin/bash # Docker镜像导入脚本 # 自动生成,用于导入导出的Docker镜像 set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" echo "开始导入Docker镜像..." # 检查Docker是否运行 if ! docker info >/dev/null 2>&1; then echo "错误: Docker未运行或无法访问" exit 1 fi # 导入所有tar文件 for file in "$SCRIPT_DIR"/*.tar*; do if [[ -f "$file" ]]; then echo "导入镜像: $(basename "$file")" if [[ "$file" == *.gz ]]; then # 解压并导入 if gunzip -c "$file" | docker load; then echo "✓ 镜像导入成功" else echo "✗ 镜像导入失败" fi else # 直接导入 if docker load -i "$file"; then echo "✓ 镜像导入成功" else echo "✗ 镜像导入失败" fi fi fi done echo "镜像导入完成" echo "可用镜像列表:" docker images EOF chmod +x "$import_script" # 显示总结 log_success "镜像导出完成!" log "导出的文件:" for file in "${EXPORTED_FILES[@]}"; do if [[ -f "$file" ]]; then size=$(du -h "$file" | cut -f1) log " - $(basename "$file") ($size)" fi done log "生成的文件:" log " - images-manifest.txt (镜像清单)" log " - import-images.sh (导入脚本)" total_size=$(du -sh "$EXPORT_DIR" | cut -f1) log "总大小: $total_size" log_success "所有文件已保存到: $EXPORT_DIR"