#!/bin/bash # Docker镜像和配置文件推送脚本 # 将生产环境所需的文件和镜像推送到远程服务器 set -e # =========================================== # 脚本配置 # =========================================== SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" # 默认配置 DEFAULT_REMOTE_HOST="" DEFAULT_REMOTE_USER="root" DEFAULT_REMOTE_PATH="/root/product/anxin" DEFAULT_SSH_PORT="22" # 镜像配置 FRONTEND_IMAGE="anxin-frontend" BACKEND_IMAGE="anxin-backend" DEFAULT_TAG="latest" # 临时目录 TEMP_DIR="/tmp/anxin-deploy-$(date +%s)" # =========================================== # 颜色定义 # =========================================== RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' PURPLE='\033[0;35m' CYAN='\033[0;36m' NC='\033[0m' # No Color # =========================================== # 日志函数 # =========================================== log_info() { echo -e "${BLUE}[INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" } log_success() { echo -e "${GREEN}[SUCCESS]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" } log_warn() { echo -e "${YELLOW}[WARN]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" } log_error() { echo -e "${RED}[ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" } log_step() { echo -e "${CYAN}[STEP]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" } # =========================================== # 帮助信息 # =========================================== show_help() { cat << EOF 若依框架Docker部署 - 推送脚本 用法: $0 [选项] 选项: -h, --host HOST 远程服务器地址 (必需) -u, --user USER 远程服务器用户名 [默认: root] -p, --port PORT SSH端口 [默认: 22] -d, --dest PATH 远程服务器目标路径 [默认: /opt/anxin] -e, --env ENV 环境 (development|staging|production) [默认: production] -t, --tag TAG 镜像标签 [默认: latest,会自动使用环境标签] --skip-images 跳过镜像推送,仅推送配置文件 --skip-files 跳过配置文件推送,仅推送镜像 --frontend-only 仅推送前端镜像 --backend-only 仅推送后端镜像 --help 显示此帮助信息 示例: $0 -h 192.168.1.100 # 推送生产环境到指定服务器 $0 -h 192.168.1.100 -e staging # 推送测试环境 $0 -h 192.168.1.100 -u deploy -p 2222 # 指定用户和端口 $0 -h 192.168.1.100 -d /home/deploy/anxin # 指定目标路径 $0 -h 192.168.1.100 -t v1.0.0 # 指定镜像标签 $0 -h 192.168.1.100 --skip-images # 仅推送配置文件 $0 -h 192.168.1.100 --frontend-only # 仅推送前端镜像 $0 -h 192.168.1.100 --backend-only # 仅推送后端镜像 推送内容: 配置文件: - docker-compose.{ENVIRONMENT}.yml - deploy.sh - environments/ 目录 - configs/ 目录 (如果存在) - database/ 目录 (如果存在) Docker镜像: - anxin-frontend:{ENV_TAG} - anxin-backend:{ENV_TAG} 注意: - ENV_TAG 根据环境自动确定: dev/staging/prod - 如果指定 -t 参数,将使用自定义标签 - 推送前请确保已运行 ./build.sh -e {ENVIRONMENT} 构建镜像 EOF } # =========================================== # 参数解析 # =========================================== parse_args() { SKIP_IMAGES=false SKIP_FILES=false FRONTEND_ONLY=false BACKEND_ONLY=false while [[ $# -gt 0 ]]; do case $1 in -h|--host) REMOTE_HOST="$2" shift 2 ;; -u|--user) REMOTE_USER="$2" shift 2 ;; -p|--port) SSH_PORT="$2" shift 2 ;; -d|--dest) REMOTE_PATH="$2" shift 2 ;; -e|--env) ENVIRONMENT="$2" shift 2 ;; -t|--tag) TAG="$2" shift 2 ;; --skip-images) SKIP_IMAGES=true shift ;; --skip-files) SKIP_FILES=true shift ;; --frontend-only) FRONTEND_ONLY=true shift ;; --backend-only) BACKEND_ONLY=true shift ;; --help) show_help exit 0 ;; *) log_error "未知参数: $1" show_help exit 1 ;; esac done # 设置默认值 REMOTE_HOST=${REMOTE_HOST:-$DEFAULT_REMOTE_HOST} REMOTE_USER=${REMOTE_USER:-$DEFAULT_REMOTE_USER} SSH_PORT=${SSH_PORT:-$DEFAULT_SSH_PORT} REMOTE_PATH=${REMOTE_PATH:-$DEFAULT_REMOTE_PATH} ENVIRONMENT=${ENVIRONMENT:-"production"} # 推送脚本默认为生产环境 TAG=${TAG:-$DEFAULT_TAG} # 验证必需参数 if [[ -z "$REMOTE_HOST" ]]; then log_error "必须指定远程服务器地址 (-h|--host)" show_help exit 1 fi # 验证互斥参数 if [[ "$FRONTEND_ONLY" == "true" && "$BACKEND_ONLY" == "true" ]]; then log_error "--frontend-only 和 --backend-only 不能同时使用" exit 1 fi if [[ "$SKIP_IMAGES" == "true" && ("$FRONTEND_ONLY" == "true" || "$BACKEND_ONLY" == "true") ]]; then log_error "--skip-images 不能与 --frontend-only 或 --backend-only 同时使用" exit 1 fi } # =========================================== # 系统检查 # =========================================== check_prerequisites() { log_step "检查系统依赖..." # 检查SSH if ! command -v ssh &> /dev/null; then log_error "SSH未安装或不在PATH中" exit 1 fi # 检查SCP if ! command -v scp &> /dev/null; then log_error "SCP未安装或不在PATH中" exit 1 fi # 检查Docker (如果需要推送镜像) if [[ "$SKIP_IMAGES" != "true" ]] && ! command -v docker &> /dev/null; then log_error "Docker未安装或不在PATH中" exit 1 fi log_success "系统依赖检查通过" } # =========================================== # SSH执行函数 # =========================================== # =========================================== # SSH连接管理 # =========================================== SSH_CONTROL_PATH="" SSH_MASTER_STARTED=false # 启动SSH主连接 start_ssh_master() { if [[ "$SSH_MASTER_STARTED" == "true" ]]; then return 0 fi SSH_CONTROL_PATH="/tmp/ssh-anxin-deploy-$$" log_info "启动SSH主连接 (连接复用)..." # 启动SSH主连接 ssh -M -S "$SSH_CONTROL_PATH" -f -N -p "$SSH_PORT" \ -o ControlPersist=600 \ -o ConnectTimeout=30 \ -o ServerAliveInterval=60 \ -o ServerAliveCountMax=3 \ "$REMOTE_USER@$REMOTE_HOST" if [[ $? -eq 0 ]]; then SSH_MASTER_STARTED=true log_success "SSH主连接启动成功" else log_error "SSH主连接启动失败" return 1 fi } # 停止SSH主连接 stop_ssh_master() { if [[ "$SSH_MASTER_STARTED" == "true" && -n "$SSH_CONTROL_PATH" ]]; then log_info "关闭SSH主连接..." ssh -S "$SSH_CONTROL_PATH" -O exit "$REMOTE_USER@$REMOTE_HOST" 2>/dev/null || true SSH_MASTER_STARTED=false fi } # 通用SCP传输函数,支持大文件传输和连接复用 scp_transfer() { local source="$1" local destination="$2" local is_recursive=${3:-false} local scp_options="-P $SSH_PORT -o ConnectTimeout=30 -o ServerAliveInterval=60 -o ServerAliveCountMax=3" # 如果SSH主连接已启动,使用连接复用 if [[ "$SSH_MASTER_STARTED" == "true" && -n "$SSH_CONTROL_PATH" ]]; then scp_options="$scp_options -o ControlPath=$SSH_CONTROL_PATH" fi if [[ "$is_recursive" == "true" ]]; then scp_options="$scp_options -r" fi # 对于大文件,显示传输进度 if [[ -f "$source" ]]; then local file_size=$(stat -f%z "$source" 2>/dev/null || stat -c%s "$source" 2>/dev/null || echo "0") if [[ $file_size -gt 10485760 ]]; then # 大于10MB的文件显示进度 scp_options="$scp_options -v" fi fi scp $scp_options "$source" "$destination" } # 通用SSH执行函数,支持密钥和密码认证 ssh_execute() { local command="$1" local show_output=${2:-true} local timeout=${3:-300} # 默认5分钟超时 local ssh_options="-p $SSH_PORT -o ConnectTimeout=30 -o ServerAliveInterval=60 -o ServerAliveCountMax=3" # 如果SSH主连接已启动,使用连接复用 if [[ "$SSH_MASTER_STARTED" == "true" && -n "$SSH_CONTROL_PATH" ]]; then ssh_options="$ssh_options -o ControlPath=$SSH_CONTROL_PATH" if [[ "$show_output" == "true" ]]; then ssh $ssh_options "$REMOTE_USER@$REMOTE_HOST" "$command" else ssh $ssh_options "$REMOTE_USER@$REMOTE_HOST" "$command" 2>/dev/null fi return $? fi # 首先尝试使用SSH密钥连接(非交互式) if ssh $ssh_options -o BatchMode=yes "$REMOTE_USER@$REMOTE_HOST" "$command" 2>/dev/null; then return 0 fi # 如果SSH密钥失败,使用交互式连接(允许密码输入) if [[ "$show_output" == "true" ]]; then ssh $ssh_options "$REMOTE_USER@$REMOTE_HOST" "$command" else ssh $ssh_options "$REMOTE_USER@$REMOTE_HOST" "$command" 2>/dev/null fi } # =========================================== # 连接测试 # =========================================== test_connection() { log_step "测试远程服务器连接..." # 首先尝试使用SSH密钥连接(非交互式) if ssh -p "$SSH_PORT" -o ConnectTimeout=10 -o BatchMode=yes "$REMOTE_USER@$REMOTE_HOST" "echo 'Connection test successful'" &>/dev/null; then log_success "远程服务器连接测试成功 (使用SSH密钥)" return 0 fi # 如果SSH密钥失败,尝试交互式连接(允许密码输入) log_info "SSH密钥认证失败,尝试密码认证..." if ssh -p "$SSH_PORT" -o ConnectTimeout=10 -o PasswordAuthentication=yes "$REMOTE_USER@$REMOTE_HOST" "echo 'Connection test successful'"; then log_success "远程服务器连接测试成功 (使用密码认证)" return 0 else log_error "无法连接到远程服务器: $REMOTE_USER@$REMOTE_HOST:$SSH_PORT" log_info "请确保:" log_info "1. 服务器地址和端口正确" log_info "2. SSH密钥已配置或密码正确" log_info "3. 用户有相应权限" log_info "4. 服务器SSH服务正在运行" exit 1 fi } # =========================================== # 准备推送 # =========================================== prepare_push() { log_step "准备推送环境..." # 创建临时目录 mkdir -p "$TEMP_DIR" log_info "创建临时目录: $TEMP_DIR" # 在远程服务器创建目标目录 log_info "在远程服务器创建目标目录..." ssh_execute "mkdir -p $REMOTE_PATH" log_success "推送环境准备完成" } # =========================================== # 推送配置文件 # =========================================== push_config_files() { if [[ "$SKIP_FILES" == "true" ]]; then log_info "跳过配置文件推送" return 0 fi log_step "推送配置文件..." cd "$SCRIPT_DIR" # 推送对应环境的docker-compose文件 local compose_file="docker-compose.${ENVIRONMENT}.yml" log_info "推送 $compose_file..." if [[ -f "$compose_file" ]]; then scp_transfer "$compose_file" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/" else log_error "Docker Compose文件不存在: $compose_file" return 1 fi # 推送deploy.sh log_info "推送 deploy.sh..." scp_transfer "deploy.sh" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/" # 推送environments目录 log_info "推送 environments/ 目录..." scp_transfer "environments" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/" true # 推送configs目录(如果存在) if [[ -d "configs" ]]; then log_info "推送 configs/ 目录..." scp_transfer "configs" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/" true fi # 推送database目录(如果存在) if [[ -d "database" ]]; then log_info "推送 database/ 目录..." scp_transfer "database" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/" true fi # 设置deploy.sh执行权限 log_info "设置deploy.sh执行权限..." ssh_execute "chmod +x $REMOTE_PATH/deploy.sh" log_success "配置文件推送完成" } # =========================================== # 推送Docker镜像 # =========================================== push_docker_images() { if [[ "$SKIP_IMAGES" == "true" ]]; then log_info "跳过Docker镜像推送" return 0 fi log_step "推送Docker镜像..." # 根据环境确定镜像标签 local env_tag case $ENVIRONMENT in production) env_tag="prod" ;; staging) env_tag="staging" ;; development) env_tag="dev" ;; esac # 如果指定了自定义标签,使用自定义标签,否则使用环境标签 local image_tag if [[ "$TAG" != "latest" ]]; then image_tag="$TAG" else image_tag="$env_tag" fi # 根据参数确定要推送的镜像 local images=() if [[ "$FRONTEND_ONLY" == "true" ]]; then images=("${FRONTEND_IMAGE}:${image_tag}") log_info "仅推送前端镜像" elif [[ "$BACKEND_ONLY" == "true" ]]; then images=("${BACKEND_IMAGE}:${image_tag}") log_info "仅推送后端镜像" else images=("${FRONTEND_IMAGE}:${image_tag}" "${BACKEND_IMAGE}:${image_tag}") log_info "推送前端和后端镜像" fi for image in "${images[@]}"; do log_info "处理镜像: $image" # 检查本地镜像是否存在 if ! docker images --format "{{.Repository}}:{{.Tag}}" | grep -q "^$image$"; then log_error "本地镜像不存在: $image" log_info "请先运行构建脚本: ./build.sh -e $ENVIRONMENT" if [[ "$TAG" != "latest" ]]; then log_info "或者: ./build.sh -e $ENVIRONMENT -t $TAG" fi exit 1 fi # 导出镜像到tar文件 local image_file="$TEMP_DIR/$(echo $image | tr ':/' '_').tar" log_info "导出镜像到: $image_file" docker save -o "$image_file" "$image" # 推送镜像文件到远程服务器 log_info "推送镜像文件到远程服务器..." scp_transfer "$image_file" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/" # 在远程服务器加载镜像 local remote_image_file="$REMOTE_PATH/$(basename $image_file)" log_info "在远程服务器加载镜像..." ssh_execute "docker load -i $remote_image_file && rm $remote_image_file" log_success "镜像推送完成: $image" done log_success "Docker镜像推送完成" } # =========================================== # 验证推送结果 # =========================================== verify_push() { log_step "验证推送结果..." # 验证配置文件 if [[ "$SKIP_FILES" != "true" ]]; then log_info "验证配置文件..." local files=("docker-compose.${ENVIRONMENT}.yml" "deploy.sh" "environments") for file in "${files[@]}"; do if ssh_execute "test -e $REMOTE_PATH/$file" false; then log_success "✓ $file" else log_error "✗ $file 不存在" fi done fi # 验证Docker镜像 if [[ "$SKIP_IMAGES" != "true" ]]; then log_info "验证Docker镜像..." # 根据环境确定镜像标签 local env_tag case $ENVIRONMENT in production) env_tag="prod" ;; staging) env_tag="staging" ;; development) env_tag="dev" ;; esac # 如果指定了自定义标签,使用自定义标签,否则使用环境标签 local image_tag if [[ "$TAG" != "latest" ]]; then image_tag="$TAG" else image_tag="$env_tag" fi # 根据参数确定要验证的镜像 local images=() if [[ "$FRONTEND_ONLY" == "true" ]]; then images=("${FRONTEND_IMAGE}:${image_tag}") elif [[ "$BACKEND_ONLY" == "true" ]]; then images=("${BACKEND_IMAGE}:${image_tag}") else images=("${FRONTEND_IMAGE}:${image_tag}" "${BACKEND_IMAGE}:${image_tag}") fi for image in "${images[@]}"; do if ssh_execute "docker images --format '{{.Repository}}:{{.Tag}}' | grep -q '^$image$'" false; then log_success "✓ $image" else log_error "✗ $image 不存在" fi done fi log_success "推送结果验证完成" } # =========================================== # 显示部署信息 # =========================================== show_deploy_info() { log_info "推送完成! 部署信息:" echo "----------------------------------------" echo "远程服务器: $REMOTE_USER@$REMOTE_HOST:$SSH_PORT" echo "部署路径: $REMOTE_PATH" echo "环境: $ENVIRONMENT" echo "镜像标签: $TAG" # 显示推送的镜像类型 if [[ "$SKIP_IMAGES" != "true" ]]; then if [[ "$FRONTEND_ONLY" == "true" ]]; then echo "推送镜像: 仅前端镜像" elif [[ "$BACKEND_ONLY" == "true" ]]; then echo "推送镜像: 仅后端镜像" else echo "推送镜像: 前端和后端镜像" fi else echo "推送镜像: 跳过" fi echo "----------------------------------------" echo "下一步操作:" echo "1. 登录远程服务器:" echo " ssh -p $SSH_PORT $REMOTE_USER@$REMOTE_HOST" echo "" echo "2. 进入部署目录:" echo " cd $REMOTE_PATH" echo "" echo "3. 启动服务:" echo " ./deploy.sh start -e $ENVIRONMENT" echo "" echo "4. 查看服务状态:" echo " ./deploy.sh status -e $ENVIRONMENT" echo "" echo "5. 查看服务日志:" echo " ./deploy.sh logs -e $ENVIRONMENT" echo "----------------------------------------" } # =========================================== # 主函数 # =========================================== main() { # 显示脚本信息 log_info "若依框架Docker部署 - 推送脚本" log_info "执行时间: $(date '+%Y-%m-%d %H:%M:%S')" # 解析参数 parse_args "$@" # 检查系统依赖 check_prerequisites # 测试连接 test_connection # 如果连接测试成功但不是使用SSH密钥,启动SSH主连接进行连接复用 if ! ssh -p "$SSH_PORT" -o ConnectTimeout=10 -o BatchMode=yes "$REMOTE_USER@$REMOTE_HOST" "echo 'test'" &>/dev/null; then log_info "检测到密码认证,启用SSH连接复用以减少密码输入次数" start_ssh_master fi # 准备推送 prepare_push # 推送配置文件 push_config_files # 推送Docker镜像 push_docker_images # 验证推送结果 verify_push # 显示部署信息 show_deploy_info log_success "推送脚本执行完成!" } # =========================================== # 错误处理和清理 # =========================================== cleanup() { # 停止SSH主连接 stop_ssh_master # 清理临时文件 if [[ -n "$TEMP_DIR" && -d "$TEMP_DIR" ]]; then log_info "清理临时文件..." rm -rf "$TEMP_DIR" log_success "临时文件清理完成" fi } trap 'log_error "脚本执行被中断"; cleanup; exit 1' INT TERM trap 'cleanup' EXIT # 执行主函数 main "$@"