topfans/docker/deploy.sh
2026-04-24 18:04:55 +08:00

698 lines
21 KiB
Bash
Executable File
Raw 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
# ===================================================================
# 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"
"taskservice"
"starbookservice"
)
# ==================== 服务器配置 ====================
# ⚠️ 修改为你的服务器信息
SERVER_HOST="101.132.250.62" # 服务器 IP 或域名
SERVER_PORT="22" # SSH 端口
SERVER_USER="root" # SSH 用户名
SERVER_PASSWORD="" # 服务器密码(仅在未配置 SSH 密钥时使用)
SERVER_PATH="/opt/topfans/docker" # 服务器上 docker 目录路径
SSH_KEY_PATH="$HOME/.ssh/id_rsa" # SSH 密钥路径,默认使用 ~/.ssh/id_rsa
# ==================== SSH 别名 ====================
# 优先使用 SSH 密钥,如果失败则使用密码
ssh_cmd() {
if [ -n "$SERVER_PASSWORD" ]; then
sshpass -p "$SERVER_PASSWORD" ssh -o StrictHostKeyChecking=no -p "$SERVER_PORT" "$SERVER_USER@$SERVER_HOST" "$@"
else
ssh -o StrictHostKeyChecking=no -p "$SERVER_PORT" "$SERVER_USER@$SERVER_HOST" "$@"
fi
}
ssh_cmd_batch() {
if [ -n "$SERVER_PASSWORD" ]; then
sshpass -p "$SERVER_PASSWORD" ssh -o StrictHostKeyChecking=no -p "$SERVER_PORT" "$SERVER_USER@$SERVER_HOST" "$@"
else
ssh -o StrictHostKeyChecking=no -p "$SERVER_PORT" "$SERVER_USER@$SERVER_HOST" "$@"
fi
}
scp_cmd() {
if [ -n "$SERVER_PASSWORD" ]; then
sshpass -p "$SERVER_PASSWORD" scp -o StrictHostKeyChecking=no -P "$SERVER_PORT" "$@"
else
scp -o StrictHostKeyChecking=no -P "$SERVER_PORT" "$@"
fi
}
# ==================== 打印函数 ====================
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 <IP> 指定服务器(覆盖配置文件)
--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 密钥登录(推荐):
- 生成密钥ssh-keygen -t rsa
- 上传公钥ssh-copy-id -i ~/.ssh/id_rsa.pub root@你的服务器IP
- 设置 SSH_KEY_PATH="~/.ssh/id_rsa"
3. 或使用密码登录:设置 SERVER_PASSWORD
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 <IP>"
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, init-db.sql..."
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}/"
scp_cmd -r "${SCRIPT_DIR}/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 "$@"