anxin-ruoyi/docker/push.sh
2026-01-08 23:05:44 +08:00

668 lines
20 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 "$@"