refactor: 改用 Docker 容器构建,无需在主机安装 Node.js

- 移除对主机 node/pnpm 的依赖,只需要 Docker
- 新增 builder 镜像自动创建功能
- 所有构建操作在容器中执行
- 支持 docker-compose 和 docker compose (V2)
- 使用 pnpm store volume 加速依赖安装

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-09 06:35:05 -08:00
parent 3ac83e9305
commit ae9b33a491
1 changed files with 128 additions and 103 deletions

231
deploy.sh
View File

@ -53,6 +53,9 @@ cd "$PROJECT_ROOT"
COMPOSE_FILE="docker-compose.yml"
ENV_FILE=".env"
# Docker Compose 命令 (默认值,会在 check_environment 中更新)
DOCKER_COMPOSE="docker-compose"
# 域名配置
DOMAIN="${DOMAIN:-iconsulting.szaiai.com}"
ADMIN_EMAIL="${ADMIN_EMAIL:-admin@szaiai.com}"
@ -131,23 +134,61 @@ check_command() {
fi
}
# 检查环境
# 检查环境 (只需要 Docker)
check_environment() {
log_step "检查运行环境..."
check_command "node"
check_command "pnpm"
check_command "docker"
check_command "docker-compose"
# 检查 Node 版本
NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
if [ "$NODE_VERSION" -lt 18 ]; then
log_error "Node.js 版本需要 >= 18当前版本: $(node -v)"
# 检查 docker-compose 或 docker compose
if command -v docker-compose &> /dev/null; then
DOCKER_COMPOSE="docker-compose"
elif docker compose version &> /dev/null 2>&1; then
DOCKER_COMPOSE="docker compose"
else
log_error "docker-compose 或 docker compose 未安装"
exit 1
fi
log_success "环境检查通过"
log_success "环境检查通过 (使用 $DOCKER_COMPOSE)"
}
# Builder 镜像名称
BUILDER_IMAGE="iconsulting-builder:latest"
# 构建 Builder 镜像
build_builder_image() {
log_step "检查/构建 Builder 镜像..."
# 检查镜像是否存在
if docker images | grep -q "iconsulting-builder"; then
log_info "Builder 镜像已存在"
return 0
fi
log_info "创建 Builder 镜像..."
# 创建临时 Dockerfile
cat > "$PROJECT_ROOT/.builder.Dockerfile" << 'DOCKERFILE'
FROM node:20-alpine
# 安装 pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate
# 设置工作目录
WORKDIR /app
# 设置 pnpm store 目录
ENV PNPM_HOME=/root/.local/share/pnpm
ENV PATH=$PNPM_HOME:$PATH
CMD ["sh"]
DOCKERFILE
docker build -t "$BUILDER_IMAGE" -f "$PROJECT_ROOT/.builder.Dockerfile" "$PROJECT_ROOT"
rm -f "$PROJECT_ROOT/.builder.Dockerfile"
log_success "Builder 镜像创建完成"
}
# 加载环境变量
@ -183,26 +224,35 @@ wait_for_service() {
}
#===============================================================================
# 构建函数
# 构建函数 (使用 Docker 容器构建,无需在主机安装 Node.js)
#===============================================================================
# 安装依赖
# 在容器中执行命令
run_in_builder() {
docker run --rm \
-v "$PROJECT_ROOT:/app" \
-v "iconsulting-pnpm-store:/root/.local/share/pnpm/store" \
-w /app \
"$BUILDER_IMAGE" \
sh -c "$1"
}
# 安装依赖 (在容器中)
install_deps() {
log_step "安装项目依赖..."
pnpm install
log_step "安装项目依赖 (在 Docker 容器中)..."
build_builder_image
run_in_builder "pnpm install --frozen-lockfile || pnpm install"
log_success "依赖安装完成"
}
# 构建共享包
# 构建共享包 (在容器中)
build_shared() {
log_step "构建 shared 包..."
cd "$PROJECT_ROOT/${SERVICE_DIRS[shared]}"
pnpm run build
cd "$PROJECT_ROOT"
run_in_builder "cd packages/shared && pnpm run build"
log_success "shared 构建完成"
}
# 构建单个后端服务
# 构建单个后端服务 (在容器中)
build_backend_service() {
local service=$1
local dir="${SERVICE_DIRS[$service]}"
@ -213,19 +263,11 @@ build_backend_service() {
fi
log_step "构建 $service..."
cd "$PROJECT_ROOT/$dir"
# 清理旧构建
rm -rf dist
# TypeScript 编译
pnpm run build
cd "$PROJECT_ROOT"
run_in_builder "cd $dir && rm -rf dist && pnpm run build"
log_success "$service 构建完成"
}
# 构建单个前端
# 构建单个前端 (在容器中)
build_frontend() {
local service=$1
local dir="${SERVICE_DIRS[$service]}"
@ -236,15 +278,7 @@ build_frontend() {
fi
log_step "构建 $service..."
cd "$PROJECT_ROOT/$dir"
# 清理旧构建
rm -rf dist
# Vite 构建
pnpm run build
cd "$PROJECT_ROOT"
run_in_builder "cd $dir && rm -rf dist && pnpm run build"
log_success "$service 构建完成"
}
@ -266,7 +300,8 @@ build_all_frontend() {
# 构建所有
build_all() {
log_info "开始构建所有服务..."
log_info "开始构建所有服务 (使用 Docker 容器)..."
build_builder_image
install_deps
build_all_backend
build_all_frontend
@ -277,6 +312,14 @@ build_all() {
do_build() {
local target=${1:-all}
# 确保 builder 镜像存在
build_builder_image
# 如果不是构建全部,先安装依赖
if [ "$target" != "all" ]; then
install_deps
fi
case $target in
all)
build_all
@ -285,6 +328,7 @@ do_build() {
build_shared
;;
backend)
build_shared
build_all_backend
;;
frontend)
@ -294,6 +338,7 @@ do_build() {
build_frontend "$target"
;;
conversation|user|payment|knowledge|evolution)
build_shared
build_backend_service "$target"
;;
*)
@ -316,13 +361,13 @@ build_docker_images() {
if [ -n "$service" ] && [ "$service" != "all" ]; then
local docker_service="${DOCKER_SERVICES[$service]}"
if [ -n "$docker_service" ]; then
docker-compose build "$docker_service"
$DOCKER_COMPOSE build "$docker_service"
else
log_error "未知服务: $service"
return 1
fi
else
docker-compose build
$DOCKER_COMPOSE build
fi
log_success "Docker 镜像构建完成"
@ -332,7 +377,7 @@ build_docker_images() {
start_infrastructure() {
log_step "启动基础设施服务..."
docker-compose up -d postgres redis neo4j
$DOCKER_COMPOSE up -d postgres redis neo4j
# 等待数据库就绪
wait_for_service localhost 5432 "PostgreSQL"
@ -346,52 +391,23 @@ start_infrastructure() {
start_kong() {
log_step "启动 Kong API 网关..."
docker-compose up -d kong-database
$DOCKER_COMPOSE up -d kong-database
sleep 5
# Kong 数据库迁移
docker-compose run --rm kong kong migrations bootstrap || true
$DOCKER_COMPOSE run --rm kong kong migrations bootstrap || true
docker-compose up -d kong
$DOCKER_COMPOSE up -d kong
wait_for_service localhost 8000 "Kong"
log_success "Kong 启动完成"
}
# 启动后端服务 (非 Docker 模式)
# 启动后端服务 (非 Docker 模式 - 已弃用,统一使用 Docker)
# 注意: 本函数已不再使用,保留仅作参考
start_backend_service_local() {
local service=$1
local dir="${SERVICE_DIRS[$service]}"
local port="${SERVICE_PORTS[$service]}"
if [ -z "$dir" ]; then
log_error "未知服务: $service"
return 1
fi
log_step "启动 $service (端口: $port)..."
cd "$PROJECT_ROOT/$dir"
# 检查是否已构建
if [ ! -d "dist" ]; then
log_warning "$service 未构建,先进行构建..."
pnpm run build
fi
# 使用 PM2 或直接启动
if command -v pm2 &> /dev/null; then
pm2 start dist/main.js --name "iconsulting-$service" --cwd "$PROJECT_ROOT/$dir"
else
# 后台启动
nohup node dist/main.js > "$PROJECT_ROOT/logs/$service.log" 2>&1 &
echo $! > "$PROJECT_ROOT/pids/$service.pid"
fi
cd "$PROJECT_ROOT"
sleep 2
wait_for_service localhost "$port" "$service" 15
log_warning "本地模式已弃用,将使用 Docker 模式启动服务"
start_backend_service_docker "$1"
}
# 启动后端服务 (Docker 模式)
@ -400,7 +416,7 @@ start_backend_service_docker() {
local docker_service="${DOCKER_SERVICES[$service]}"
log_step "启动 $service (Docker)..."
docker-compose up -d "$docker_service"
$DOCKER_COMPOSE up -d "$docker_service"
local port="${SERVICE_PORTS[$service]}"
wait_for_service localhost "$port" "$service"
@ -422,7 +438,7 @@ start_all_backend() {
# 启动 Nginx (静态文件服务)
start_nginx() {
log_step "启动 Nginx..."
docker-compose up -d nginx
$DOCKER_COMPOSE up -d nginx
wait_for_service localhost 80 "Nginx"
log_success "Nginx 启动完成"
}
@ -467,7 +483,7 @@ do_start() {
start_nginx
;;
postgres|redis|neo4j)
docker-compose up -d "$target"
$DOCKER_COMPOSE up -d "$target"
;;
conversation|user|payment|knowledge|evolution)
if [ "$mode" = "docker" ]; then
@ -517,7 +533,7 @@ stop_service_docker() {
if [ -n "$docker_service" ]; then
log_step "停止 $service..."
docker-compose stop "$docker_service"
$DOCKER_COMPOSE stop "$docker_service"
log_success "$service 已停止"
fi
}
@ -529,12 +545,12 @@ stop_all() {
log_info "停止所有服务..."
if [ "$mode" = "docker" ]; then
docker-compose down
$DOCKER_COMPOSE down
else
for service in conversation user payment knowledge evolution; do
stop_service_local "$service"
done
docker-compose down
$DOCKER_COMPOSE down
fi
log_success "所有服务已停止"
@ -550,7 +566,7 @@ do_stop() {
stop_all "$mode"
;;
infra|infrastructure)
docker-compose stop postgres redis neo4j
$DOCKER_COMPOSE stop postgres redis neo4j
;;
conversation|user|payment|knowledge|evolution)
if [ "$mode" = "docker" ]; then
@ -560,7 +576,7 @@ do_stop() {
fi
;;
kong|postgres|redis|neo4j|nginx)
docker-compose stop "$target"
$DOCKER_COMPOSE stop "$target"
;;
*)
log_error "未知停止目标: $target"
@ -596,7 +612,7 @@ do_status() {
# Docker 服务状态
echo -e "${PURPLE}Docker 容器状态:${NC}"
docker-compose ps
$DOCKER_COMPOSE ps
echo ""
# 端口检查
@ -632,11 +648,11 @@ do_logs() {
local lines=${2:-100}
if [ "$service" = "all" ]; then
docker-compose logs -f --tail="$lines"
$DOCKER_COMPOSE logs -f --tail="$lines"
else
local docker_service="${DOCKER_SERVICES[$service]}"
if [ -n "$docker_service" ]; then
docker-compose logs -f --tail="$lines" "$docker_service"
$DOCKER_COMPOSE logs -f --tail="$lines" "$docker_service"
else
# 本地日志
local log_file="$PROJECT_ROOT/logs/$service.log"
@ -674,7 +690,7 @@ do_clean() {
;;
docker)
log_step "清理 Docker 资源..."
docker-compose down -v --rmi local
$DOCKER_COMPOSE down -v --rmi local
docker system prune -f
log_success "Docker 资源已清理"
;;
@ -799,7 +815,7 @@ obtain_ssl_cert() {
mkdir -p "$PROJECT_ROOT/nginx/ssl"
# 停止 Nginx (释放 80 端口)
docker-compose stop nginx 2>/dev/null || true
$DOCKER_COMPOSE stop nginx 2>/dev/null || true
# 使用 standalone 模式申请证书
certbot certonly \
@ -825,7 +841,7 @@ obtain_ssl_cert() {
fi
# 重启 Nginx
docker-compose up -d nginx
$DOCKER_COMPOSE up -d nginx
log_success "SSL 证书配置完成"
}
@ -842,7 +858,7 @@ renew_ssl_cert() {
cp "/etc/letsencrypt/live/$DOMAIN/privkey.pem" "$PROJECT_ROOT/nginx/ssl/"
# 重载 Nginx
docker-compose exec nginx nginx -s reload
$DOCKER_COMPOSE exec nginx nginx -s reload
log_success "SSL 证书续期完成"
fi
}
@ -1069,13 +1085,12 @@ do_db() {
case $action in
migrate)
log_step "执行数据库迁移..."
# 可以添加 TypeORM 迁移命令
log_step "执行数据库迁移 (在 Docker 容器中)..."
build_builder_image
# 在容器中执行迁移命令
for service in user payment knowledge conversation evolution; do
local dir="${SERVICE_DIRS[$service]}"
cd "$PROJECT_ROOT/$dir"
pnpm run migration:run 2>/dev/null || log_warning "$service 无迁移或迁移失败"
cd "$PROJECT_ROOT"
run_in_builder "cd $dir && pnpm run migration:run 2>/dev/null" || log_warning "$service 无迁移或迁移失败"
done
log_success "数据库迁移完成"
;;
@ -1089,7 +1104,7 @@ do_db() {
mkdir -p "$backup_dir"
log_step "备份数据库..."
docker-compose exec -T postgres pg_dump -U postgres iconsulting > "$backup_dir/postgres.sql"
$DOCKER_COMPOSE exec -T postgres pg_dump -U postgres iconsulting > "$backup_dir/postgres.sql"
log_success "数据库备份到: $backup_dir"
;;
restore)
@ -1100,15 +1115,15 @@ do_db() {
fi
log_step "恢复数据库..."
docker-compose exec -T postgres psql -U postgres iconsulting < "$backup_file"
$DOCKER_COMPOSE exec -T postgres psql -U postgres iconsulting < "$backup_file"
log_success "数据库恢复完成"
;;
reset)
log_warning "这将删除所有数据!"
read -p "确认继续? (y/N) " confirm
if [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then
docker-compose down -v
docker-compose up -d postgres redis neo4j
$DOCKER_COMPOSE down -v
$DOCKER_COMPOSE up -d postgres redis neo4j
wait_for_service localhost 5432 "PostgreSQL"
do_db migrate
log_success "数据库已重置"
@ -1116,8 +1131,8 @@ do_db() {
;;
status)
echo -e "${PURPLE}数据库状态:${NC}"
docker-compose exec postgres psql -U postgres -c "SELECT version();" 2>/dev/null || echo "PostgreSQL 未运行"
docker-compose exec redis redis-cli ping 2>/dev/null || echo "Redis 未运行"
$DOCKER_COMPOSE exec postgres psql -U postgres -c "SELECT version();" 2>/dev/null || echo "PostgreSQL 未运行"
$DOCKER_COMPOSE exec redis redis-cli ping 2>/dev/null || echo "Redis 未运行"
curl -s http://localhost:7474 > /dev/null && echo "Neo4j 运行中" || echo "Neo4j 未运行"
;;
*)
@ -1209,6 +1224,11 @@ show_help() {
DOMAIN 域名 (默认: iconsulting.szaiai.com)
ADMIN_EMAIL 管理员邮箱 (用于 SSL 证书)
系统要求:
- Docker (必须)
- Docker Compose 或 docker compose (必须)
- 无需安装 Node.js / pnpm (构建在 Docker 容器中进行)
EOF
}
@ -1220,6 +1240,11 @@ main() {
local command=${1:-help}
shift || true
# 对于 help 命令不需要检查环境
if [ "$command" != "help" ] && [ "$command" != "--help" ] && [ "$command" != "-h" ]; then
check_environment
fi
case $command in
build)
do_build "$@"