#!/bin/bash # =================================================================== # TopFans 打包上传部署脚本 # 功能: # - 本地:构建镜像 → 推送到镜像仓库 # - 远程:拉取镜像 → 启动服务 # - 回滚:回滚到指定版本 # - 配置:快速上传配置文件、重启服务 # =================================================================== # # 使用前提: # 1. 已安装 Docker # 2. 已创建阿里云容器镜像仓库 # 3. 服务器已配置 SSH 免密登录 # # 使用方式: # # ========== 完整部署流程 ========== # # 本地构建镜像 # ./deploy.sh build v1.0.0 # # # 上传镜像到服务器(会自动跳过未变化的镜像) # ./deploy.sh push v1.0.0 # # # 远程部署 # ./deploy.sh deploy v1.0.0 # # # 一键构建 + 推送 + 部署 # ./deploy.sh all v1.0.0 # # # ========== 配置更新(不重新构建镜像)========== # # 修改 .env.prod 或 docker-compose.prod.yml 后: # ./deploy.sh upload-config --server 101.132.250.62 # # # 上传配置并重启服务 # ./deploy.sh upload-config --server 101.132.250.62 && ./deploy.sh restart --server 101.132.250.62 # # # ========== 其他命令 ========== # # 回滚到指定版本 # ./deploy.sh rollback v0.9.0 # # # 查看部署历史 # ./deploy.sh history # # # 清理本地镜像 # ./deploy.sh clean # # # ========== 服务器信息 ========== # SERVER_HOST="101.132.250.62" # SERVER_USER="root" # SERVER_PASSWORD=">n73qBnCja-,#VF+Wq" # SERVER_PATH="/opt/topfans/docker" # # =================================================================== set -e # ==================== 颜色定义 ==================== RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' MAGENTA='\033[0;35m' NC='\033[0m' # ==================== 路径配置 ==================== SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" # ==================== 镜像配置 ==================== # 本地构建后打包传输到服务器,不需要镜像仓库 SERVICES=( "gateway" "userservice" "socialservice" "assetservice" "galleryservice" "activityservice" ) # ==================== 服务器配置 ==================== # ⚠️ 修改为你的服务器信息 SERVER_HOST="101.132.250.62" # 服务器 IP 或域名 SERVER_PORT="22" # SSH 端口 SERVER_USER="root" # SSH 用户名 SERVER_PASSWORD=">n73qBnCja-,#VF+Wq" # 服务器密码 SERVER_PATH="/opt/topfans/docker" # 服务器上 docker 目录路径 # ==================== SSH 别名 ==================== # 使用 sshpass 执行 SSH 命令 ssh_cmd() { sshpass -p "$SERVER_PASSWORD" ssh -o StrictHostKeyChecking=no -p "$SERVER_PORT" "$SERVER_USER@$SERVER_HOST" "$@" } ssh_cmd_batch() { sshpass -p "$SERVER_PASSWORD" ssh -o StrictHostKeyChecking=no -p "$SERVER_PORT" "$SERVER_USER@$SERVER_HOST" "$@" } scp_cmd() { sshpass -p "$SERVER_PASSWORD" scp -P "$SERVER_PORT" "$@" } # ==================== 打印函数 ==================== print_step() { echo "" echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${BLUE} $1${NC}" echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" } print_msg() { local color=$1 local msg=$2 echo -e "${color}${msg}${NC}" } # ==================== 帮助信息 ==================== show_help() { cat << EOF ${MAGENTA}TopFans 部署脚本${NC} ${YELLOW}用法:${NC} $0 <命令> [版本号] [选项] ${YELLOW}命令:${NC} ${GREEN}build${NC} <版本号> 本地构建镜像并打包传输到服务器 ${GREEN}deploy${NC} <版本号> 远程部署(加载镜像 + 启动服务) ${GREEN}rollback${NC} <版本号> 回滚到指定版本 ${GREEN}history${NC} 查看部署历史 ${GREEN}all${NC} <版本号> 一键构建 + 传输 + 部署 ${GREEN}clean${NC} 清理本地镜像(谨慎使用) ${GREEN}upload-config${NC} 仅上传配置文件(不重新构建镜像) ${GREEN}restart${NC} 重启服务(不重新构建/上传) ${YELLOW}选项:${NC} --server 指定服务器(覆盖配置文件) --skip-build 跳过构建(用于已构建过的情况) --skip-push 跳过传输(用于已传输过的情况) --force 强制执行(不确认) --help, -h 显示此帮助 ${YELLOW}示例:${NC} $0 build v1.0.0 # 构建并打包传输到服务器 $0 deploy v1.0.0 --server 192.168.1.100 # 部署到服务器 $0 rollback v0.9.0 # 回滚到 v0.9.0 $0 history # 查看部署历史 $0 all v1.0.0 --server 192.168.1.100 # 一键完成所有操作 $0 upload-config --server 192.168.1.100 # 仅上传配置 $0 restart --server 192.168.1.100 # 重启服务 ${YELLOW}前提准备:${NC} 1. 修改 SERVER_HOST 为你的服务器 IP 2. 配置服务器 SSH 免密登录(建议) EOF } # ==================== 配置检查 ==================== check_config() { local errors=0 if [ -z "$SERVER_HOST" ]; then print_msg "$YELLOW" "警告: SERVER_HOST 未设置,远程部署功能将不可用" fi return $errors } # ==================== 1. 构建镜像 ==================== do_build() { print_step "🔨 构建 Docker 镜像" # 调用构建脚本 # ./build.sh --no-cache ./build.sh if [ $? -ne 0 ]; then print_msg "$RED" "❌ 构建失败" exit 1 fi print_msg "$GREEN" "✅ 镜像构建完成" } # ==================== 2. 打包并传输镜像到服务器 ==================== do_push() { local version=$1 if [ -z "$SERVER_HOST" ]; then print_msg "$RED" "错误: 请设置 SERVER_HOST" exit 1 fi print_step "📦 打包镜像为 tar 文件" local tmp_dir="/tmp/topfans-images-${version}" mkdir -p "${tmp_dir}" local failed=() local packed=() for SERVICE in "${SERVICES[@]}"; do local local_image="topfans/${SERVICE}:latest" local tar_file="${tmp_dir}/${SERVICE}.tar" echo "" print_msg "$YELLOW" "处理 ${SERVICE}..." if docker save "${local_image}" -o "${tar_file}"; then echo -e " ${GREEN}✅ 已打包${NC}" packed+=("${SERVICE}") else echo -e " ${RED}❌ 打包失败${NC}" failed+=("${SERVICE}") fi done if [ ${#failed[@]} -ne 0 ]; then print_msg "$RED" "❌ 打包失败: ${failed[*]}" rm -rf "${tmp_dir}" exit 1 fi print_msg "$GREEN" "✅ 全部打包完成" # 计算本地镜像的 MD5 哈希 print_step "🔍 检查镜像差异" local local_hash="" for SERVICE in "${packed[@]}"; do local tar_file="${tmp_dir}/${SERVICE}.tar" local file_hash=$(md5 -q "${tar_file}") local image_hash=$(docker images -q "topfans/${SERVICE}:latest") local combo="${file_hash}:${image_hash}" if [ -z "$local_hash" ]; then local_hash="$combo" else local_hash="${local_hash}-${combo}" fi done local local_md5=$(echo "$local_hash" | md5) print_msg "$YELLOW" "本地镜像 MD5: ${local_md5:0:16}..." # 获取服务器上的哈希 local server_hash=$(ssh_cmd "cat ${SERVER_PATH}/images/.images_hash 2>/dev/null || echo ''") print_step "📤 传输镜像到服务器" # 检查是否需要上传 if [ "$local_md5" = "$server_hash" ]; then print_msg "$GREEN" "✅ 镜像未变化,跳过上传" rm -rf "${tmp_dir}" return 0 fi print_msg "$YELLOW" "正在传输到 ${SERVER_USER}@${SERVER_HOST}:${SERVER_PATH}..." # 创建服务器目录 ssh_cmd "mkdir -p ${SERVER_PATH}/images && rm -f ${SERVER_PATH}/images/*.tar 2>/dev/null || true" # 传输 tar 文件 for SERVICE in "${packed[@]}"; do local tar_file="${tmp_dir}/${SERVICE}.tar" print_msg "$YELLOW" "传输 ${SERVICE}.tar..." scp_cmd "${tar_file}" "${SERVER_USER}@${SERVER_HOST}:${SERVER_PATH}/images/" print_msg "$GREEN" "✅ ${SERVICE}.tar 传输完成" done # 保存哈希到服务器 ssh_cmd "echo '$local_md5' > ${SERVER_PATH}/images/.images_hash" # 清理本地临时文件 rm -rf "${tmp_dir}" print_msg "$GREEN" "✅ 镜像传输完成" } # ==================== 3. 远程部署 ==================== do_deploy() { local version=$1 if [ -z "$SERVER_HOST" ]; then print_msg "$RED" "错误: 请设置 SERVER_HOST(服务器 IP)" print_msg "$YELLOW" "使用方法: $0 deploy ${version} --server " exit 1 fi print_step "🚀 远程部署到 ${SERVER_HOST}" print_msg "$YELLOW" "检查 Docker 环境..." sshpass -p "$SERVER_PASSWORD" ssh -T -p "${SERVER_PORT}" "${SERVER_USER}@${SERVER_HOST}" << 'ENDSSH' set -e echo '=== 1. 检查/安装 Docker 环境 ===' if ! command -v docker &> /dev/null; then echo '📦 Docker 未安装,开始安装...' # 使用阿里云镜像源安装 Docker yum remove -y docker docker-common docker-selinux docker-engine-selinux docker-engine docker-ce 2>/dev/null || true yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo yum install -y docker-ce docker-ce-cli containerd.io systemctl start docker systemctl enable docker echo '✅ Docker 安装成功' fi if ! command -v docker-compose &> /dev/null; then if docker compose version &> /dev/null; then echo '📦 创建 docker-compose 软链接...' ln -sf /usr/libexec/docker/cli-plugins/docker-compose /usr/local/bin/docker-compose else echo '⚠️ docker-compose 未安装' fi fi # 配置 Docker 镜像加速器 echo '📦 配置 Docker 镜像加速器...' mkdir -p /etc/docker cat > /etc/docker/daemon.json << 'DOCKER_EOF' { "registry-mirrors": [ "https://docker.1ms.run", "https://docker.xuanyuan.me" ] } DOCKER_EOF systemctl restart docker echo '✅ 镜像加速器配置完成' echo '✅ Docker 环境就绪' ENDSSH print_step "🌐 创建 Docker 网络" print_msg "$YELLOW" "创建 topfans-net..." sshpass -p "$SERVER_PASSWORD" ssh -T -p "${SERVER_PORT}" "${SERVER_USER}@${SERVER_HOST}" "docker network create topfans-net 2>/dev/null || true" print_msg "$GREEN" "✅ 网络就绪" # 确保服务器目录存在 ssh_cmd "mkdir -p ${SERVER_PATH}/images" print_msg "$GREEN" "✅ 服务器目录就绪" # 上传配置文件 print_step "📤 上传配置文件" print_msg "$YELLOW" "上传 docker-compose.prod.yml 和 .env.prod..." scp_cmd "${SCRIPT_DIR}/docker-compose.prod.yml" "${SERVER_USER}@${SERVER_HOST}:${SERVER_PATH}/" scp_cmd "${SCRIPT_DIR}/.env.prod" "${SERVER_USER}@${SERVER_HOST}:${SERVER_PATH}/" scp_cmd "${SCRIPT_DIR}/init-db.sql" "${SERVER_USER}@${SERVER_HOST}:${SERVER_PATH}/" print_msg "$GREEN" "✅ 配置文件上传完成" # 从 tar 文件加载镜像 print_step "📥 从 tar 文件加载镜像" for SERVICE in "${SERVICES[@]}"; do print_msg "$YELLOW" "加载 ${SERVICE}..." ssh_cmd "docker load -i ${SERVER_PATH}/images/${SERVICE}.tar" print_msg "$GREEN" "✅ ${SERVICE} 加载完成" done # 打 latest 标签 print_msg "$YELLOW" "打 latest 标签..." ssh_cmd " for service in ${SERVICES[*]}; do docker tag topfans/\${service}:latest topfans/\${service}:v${version} docker tag topfans/\${service}:latest topfans/\${service}:latest done echo '标签完成' " # 停止旧服务并清理 print_msg "$YELLOW" "停止旧服务..." ssh_cmd " cd ${SERVER_PATH} && \ docker-compose -f docker-compose.prod.yml down 2>/dev/null || true # 停止并删除所有 topfans 容器 docker ps -a --filter 'name=topfans-' -q | xargs -r docker stop 2>/dev/null || true docker ps -a --filter 'name=topfans-' -q | xargs -r docker rm 2>/dev/null || true # 清理可能的端口占用 (8080, 5432) fuser -k 8080/tcp 2>/dev/null || true fuser -k 5432/tcp 2>/dev/null || true " # 启动新服务 print_msg "$YELLOW" "启动服务..." ssh_cmd " cd ${SERVER_PATH} && \ docker-compose -f docker-compose.prod.yml --profile prod up -d " # 等待并检查 print_msg "$YELLOW" "等待服务启动 (20s)..." sleep 20 print_step "📊 部署结果" ssh_cmd " echo '' docker-compose -f docker-compose.prod.yml ps echo '' echo -n 'Gateway 健康检查: ' curl -s http://localhost:8080/health > /dev/null && echo '✅ OK' || echo '⚠️ 检查失败' " print_msg "$GREEN" "✅ 远程部署完成" } # ==================== 4. 回滚 ==================== do_rollback() { local version=$1 if [ -z "$SERVER_HOST" ]; then print_msg "$RED" "错误: 请设置 SERVER_HOST" exit 1 fi print_step "🔄 回滚到版本 v${version}" print_msg "$YELLOW" "正在回滚 ${SERVER_HOST} 上的服务..." # 停止服务 print_msg "$YELLOW" "停止服务..." ssh_cmd " cd ${SERVER_PATH} && \ docker-compose -f docker-compose.prod.yml down " # 从已有的 tar 文件加载镜像并打标签 for SERVICE in "${SERVICES[@]}"; do print_msg "$YELLOW" "加载 ${SERVICE}:v${version}..." ssh_cmd " docker load -i ${SERVER_PATH}/images/${SERVICE}.tar docker tag topfans/${SERVICE}:latest topfans/${SERVICE}:v${version} docker tag topfans/${SERVICE}:latest topfans/${SERVICE}:latest " print_msg "$GREEN" "✅ ${SERVICE} 回滚完成" done # 启动服务 print_msg "$YELLOW" "启动服务..." ssh_cmd " cd ${SERVER_PATH} && \ docker-compose -f docker-compose.prod.yml --profile prod up -d " sleep 10 print_step "📊 回滚结果" ssh_cmd " echo '' docker-compose -f docker-compose.prod.yml ps echo '' echo -n 'Gateway 健康检查: ' curl -s http://localhost:8080/health > /dev/null && echo '✅ OK' || echo '⚠️ 检查失败' " print_msg "$GREEN" "✅ 回滚完成!当前版本: v${version}" } # ==================== 5. 查看历史 ==================== do_history() { if [ -z "$SERVER_HOST" ]; then print_msg "$RED" "错误: 请设置 SERVER_HOST" exit 1 fi print_step "📜 部署历史" ssh_cmd " if [ -f ${SERVER_PATH}/deploy_history.json ]; then cat ${SERVER_PATH}/deploy_history.json else echo '暂无部署历史' fi " echo "" print_msg "$YELLOW" "查看实时日志: ssh ${SERVER_USER}@${SERVER_HOST} 'docker-compose -f ${SERVER_PATH}/docker-compose.prod.yml logs -f'" } # ==================== 6. 清理本地镜像 ==================== do_clean() { print_step "🧹 清理本地镜像" print_msg "$RED" "警告: 将删除所有 topfans 镜像" print_msg "$YELLOW" "列出当前镜像:" docker images | grep topfans || echo "无 topfans 镜像" echo "" read -p "确认删除? (y/N): " confirm if [ "$confirm" != "y" ]; then print_msg "$YELLOW" "已取消" exit 0 fi docker images | grep topfans | awk '{print $3}' | xargs -r docker rmi -f print_msg "$GREEN" "✅ 清理完成" } # ==================== 主函数 ==================== main() { if [ $# -eq 0 ]; then show_help exit 0 fi local command=$1 shift local version="" local skip_build=false local skip_push=false local force=false # 解析剩余参数 while [[ $# -gt 0 ]]; do case $1 in --server) SERVER_HOST="$2" shift 2 ;; --skip-build) skip_build=true shift ;; --skip-push) skip_push=true shift ;; --force) force=true shift ;; -h|--help) show_help exit 0 ;; v*) version="${1#v}" shift ;; *) echo -e "${RED}错误: 未知参数 '$1'${NC}" exit 1 ;; esac done # 检查配置 check_config || true case $command in build) # 构建 + 推送 if [ -z "$version" ]; then echo -e "${RED}错误: 请指定版本号${NC}" echo "用法: $0 build v1.0.0" exit 1 fi echo -e "${CYAN}版本: v${version}${NC}" echo -e "${CYAN}目标: ${SERVER_USER}@${SERVER_HOST}${NC}" if [ "$skip_build" = false ]; then do_build fi if [ "$skip_push" = false ]; then do_push "$version" fi ;; deploy) # 远程部署 if [ -z "$version" ]; then echo -e "${RED}错误: 请指定版本号${NC}" echo "用法: $0 deploy v1.0.0" exit 1 fi echo -e "${CYAN}版本: v${version}${NC}" echo -e "${CYAN}目标: ${SERVER_USER}@${SERVER_HOST}${NC}" do_deploy "$version" ;; rollback) # 回滚 if [ -z "$version" ]; then echo -e "${RED}错误: 请指定要回滚的版本${NC}" echo "用法: $0 rollback v1.0.0" exit 1 fi echo -e "${RED}⚠️ 确认回滚到 v${version}?${NC}" [ "$force" = false ] && read -p "确认? (y/N): " confirm && [ "$confirm" != "y" ] && exit 0 do_rollback "$version" ;; history) do_history ;; clean) do_clean ;; upload-config) # 上传配置文件 if [ -z "$SERVER_HOST" ]; then print_msg "$RED" "错误: 请设置 SERVER_HOST 或使用 --server 参数" exit 1 fi print_step "📤 上传配置文件到 ${SERVER_HOST}" scp_cmd "${SCRIPT_DIR}/docker-compose.prod.yml" "${SERVER_USER}@${SERVER_HOST}:${SERVER_PATH}/" print_msg "$GREEN" "✅ docker-compose.prod.yml 上传完成" scp_cmd "${SCRIPT_DIR}/.env.prod" "${SERVER_USER}@${SERVER_HOST}:${SERVER_PATH}/" print_msg "$GREEN" "✅ .env.prod 上传完成" print_msg "$YELLOW" "请运行以下命令重启服务:" print_msg "$CYAN" "ssh ${SERVER_USER}@${SERVER_HOST} 'cd ${SERVER_PATH} && docker-compose -f docker-compose.prod.yml down && docker-compose -f docker-compose.prod.yml up -d'" ;; restart) # 重启服务 if [ -z "$SERVER_HOST" ]; then print_msg "$RED" "错误: 请设置 SERVER_HOST 或使用 --server 参数" exit 1 fi print_step "🔄 重启服务" ssh_cmd "cd ${SERVER_PATH} && docker-compose -f docker-compose.prod.yml down && docker-compose -f docker-compose.prod.yml up -d" sleep 10 print_msg "$GREEN" "✅ 服务重启完成" ;; all) # 构建 + 推送 + 部署 if [ -z "$version" ]; then echo -e "${RED}错误: 请指定版本号${NC}" exit 1 fi if [ -z "$SERVER_HOST" ]; then echo -e "${RED}错误: 请设置 SERVER_HOST 或使用 --server 参数${NC}" exit 1 fi echo -e "${MAGENTA}一键部署流程:${NC}" echo " 1. 构建镜像" echo " 2. 打包传输到服务器" echo " 3. 部署到 ${SERVER_HOST}" echo "" [ "$force" = false ] && read -p "继续? (y/N): " confirm && [ "$confirm" != "y" ] && exit 0 do_build do_push "$version" do_deploy "$version" print_step "🎉 全部完成!" ;; *) echo -e "${RED}错误: 未知命令 '$command'${NC}" show_help exit 1 ;; esac } main "$@"