From f7bc7182c9afa955f0df289d8c3974783855721e Mon Sep 17 00:00:00 2001 From: MerCry Date: Wed, 25 Feb 2026 22:17:56 +0800 Subject: [PATCH] feat: add Nginx dynamic routing support for Docker deployment Features: - Add nginxManager service for dynamic Nginx config generation - Add Nginx config templates (main.conf.tpl, location.conf.tpl) - Support single port mode (8080->80) with internal Nginx routing - Add project restoration on container restart - Add HTML path auto-fix for uploaded projects (absolute to relative) - Add manual Nginx reload/regenerate API endpoints - Add detailed logging for Nginx config operations Docker: - Add Dockerfile.single for single port deployment - Add docker-compose.single.yml - Add start.sh startup script - Add build-dist.ps1 and build-dist.sh scripts Docs: - Add TROUBLESHOOTING.md for deployment troubleshooting - Update README.md with Docker deployment instructions Fixes: - Fix absolute path issue in container environment - Fix project data directory creation - Fix HTML static resource path for sub-path deployment --- .env.example | 12 + .gitignore | 6 +- .../specs/nginx-dynamic-routing/checklist.md | 52 ++ .trae/specs/nginx-dynamic-routing/spec.md | 255 ++++++++ .trae/specs/nginx-dynamic-routing/tasks.md | 55 ++ Dockerfile | 22 + README.md | 589 ++++++++++++++++-- TROUBLESHOOTING.md | 398 ++++++++++++ build-dist.ps1 | 140 +++++ build-dist.sh | 195 ++++++ deploy/Dockerfile | 25 + deploy/Dockerfile.nginx | 35 ++ deploy/Dockerfile.single | 40 ++ deploy/docker-compose.single.yml | 22 + deploy/docker-compose.yml | 42 ++ deploy/start-nginx.sh | 19 + deploy/start.sh | 39 ++ docker-compose.yml | 32 + server/config/index.js | 75 ++- server/index.js | 48 +- server/routes/deploy.js | 56 ++ server/services/nginxManager.js | 290 +++++++++ server/services/processManager.js | 28 +- server/services/projectService.js | 53 +- server/templates/nginx/location.conf.tpl | 13 + server/templates/nginx/main.conf.tpl | 28 + 26 files changed, 2511 insertions(+), 58 deletions(-) create mode 100644 .trae/specs/nginx-dynamic-routing/checklist.md create mode 100644 .trae/specs/nginx-dynamic-routing/spec.md create mode 100644 .trae/specs/nginx-dynamic-routing/tasks.md create mode 100644 Dockerfile create mode 100644 TROUBLESHOOTING.md create mode 100644 build-dist.ps1 create mode 100644 build-dist.sh create mode 100644 deploy/Dockerfile create mode 100644 deploy/Dockerfile.nginx create mode 100644 deploy/Dockerfile.single create mode 100644 deploy/docker-compose.single.yml create mode 100644 deploy/docker-compose.yml create mode 100644 deploy/start-nginx.sh create mode 100644 deploy/start.sh create mode 100644 docker-compose.yml create mode 100644 server/services/nginxManager.js create mode 100644 server/templates/nginx/location.conf.tpl create mode 100644 server/templates/nginx/main.conf.tpl diff --git a/.env.example b/.env.example index c723f61..a7891cc 100644 --- a/.env.example +++ b/.env.example @@ -8,3 +8,15 @@ BASE_DOMAIN= # 项目端口范围 PROJECT_PORT_START=9000 PROJECT_PORT_END=9100 + +# 项目服务绑定地址 +# 127.0.0.1 - 仅本地访问(Nginx代理模式) +# 0.0.0.0 - 允许外部访问(Docker模式) +PROJECT_BIND_ADDRESS=127.0.0.1 + +# Nginx配置(动态路由模式) +USE_NGINX=auto +NGINX_CONFIG_DIR=./nginx/sites-enabled +NGINX_TEMPLATE_PATH=./server/templates/nginx +NGINX_RELOAD_CMD=nginx -s reload +NGINX_TEST_CMD=nginx -t diff --git a/.gitignore b/.gitignore index e00e7fd..e706c02 100644 --- a/.gitignore +++ b/.gitignore @@ -8,11 +8,15 @@ client/dist/ .env # Data files -server/data/ +data/ # Project uploads projects/ +# Deployment +deploy-dist/ +auto-deploy-dist*.zip + # IDE .idea/ .vscode/ diff --git a/.trae/specs/nginx-dynamic-routing/checklist.md b/.trae/specs/nginx-dynamic-routing/checklist.md new file mode 100644 index 0000000..1fe0c39 --- /dev/null +++ b/.trae/specs/nginx-dynamic-routing/checklist.md @@ -0,0 +1,52 @@ +# Checklist + +## Nginx配置模板系统 +- [x] 主配置模板 `main.conf.tpl` 创建完成,支持变量替换 +- [x] Location配置模板 `location.conf.tpl` 创建完成,支持SPA路由回退 +- [x] 模板变量命名规范,易于理解和维护 + +## Nginx配置管理服务 +- [x] `nginxManager.js` 服务模块创建完成 +- [x] 配置模板渲染功能正常工作 +- [x] Nginx配置文件读写功能正常工作 +- [x] 配置验证功能(nginx -t)正确执行 +- [x] Nginx重载功能(nginx -s reload)正确执行 +- [x] 配置回滚机制在失败时正确触发 + +## 环境配置扩展 +- [x] `config/index.js` 包含所有新增Nginx配置项 +- [x] `.env.example` 包含新配置项说明文档 +- [x] Nginx可用性检测功能正确判断环境 + +## 进程管理服务修改 +- [x] 项目服务正确绑定到127.0.0.1 +- [x] 端口分配逻辑支持内部端口模式 +- [x] Nginx配置更新集成点正确触发 + +## 项目服务修改 +- [x] URL生成逻辑输出正确格式:`{BASE_URL}/project/{id}/` +- [x] 项目创建时Nginx location配置正确生成 +- [x] 项目删除时Nginx配置正确清理 +- [x] 项目启动/停止时Nginx upstream状态正确更新 + +## 部署路由修改 +- [x] `/api/deploy/:id/start` 正确集成Nginx配置更新 +- [x] `/api/deploy/:id/stop` 正确集成Nginx配置更新 +- [x] Nginx配置状态查询接口正常工作 + +## 本地开发模式兼容 +- [x] Nginx环境检测逻辑正确判断安装状态 +- [x] 无Nginx时正确回退到多端口模式 +- [x] 有Nginx时正确使用动态路由模式 +- [x] 模式状态在日志中正确输出 + +## 前端适配 +- [x] 项目列表页正确显示新格式URL +- [x] 项目详情页正确显示新格式URL +- [x] 访问项目链接正确跳转 + +## 集成测试 +- [ ] 完整流程:创建项目 → 启动项目 → 访问项目 → 停止项目 → 删除项目 +- [ ] Nginx配置变更后项目仍可正常访问 +- [ ] 多项目同时运行时路由正确隔离 +- [ ] SPA项目路由刷新正常工作 diff --git a/.trae/specs/nginx-dynamic-routing/spec.md b/.trae/specs/nginx-dynamic-routing/spec.md new file mode 100644 index 0000000..3fabe41 --- /dev/null +++ b/.trae/specs/nginx-dynamic-routing/spec.md @@ -0,0 +1,255 @@ +# 动态路由与Nginx自动化配置 Spec + +## Why +当前系统为每个项目分配独立端口(9000-9100),存在以下问题: +1. 服务器防火墙需要开放大量端口 +2. URL不够友好(需要带端口号) +3. 手动配置Nginx反向代理繁琐 + +通过动态路由+Nginx自动化配置,可以实现: +- 所有项目共用单一端口(80/443) +- 友好的URL路径(如 `/project/{id}/`) +- 自动化Nginx配置更新,无需手动干预 + +## What Changes +- **新增** Nginx配置管理服务 +- **新增** Nginx配置模板系统 +- **修改** 项目访问方式从多端口改为动态路由 +- **修改** processManager 启动逻辑,使用固定内部端口 +- **新增** Nginx配置自动重载机制 +- **新增** 本地开发模式支持(可选安装Nginx) + +## Impact +- Affected specs: 项目部署流程、URL生成规则、端口管理 +- Affected code: + - `server/services/processManager.js` + - `server/services/projectService.js` + - `server/config/index.js` + - 新增 `server/services/nginxManager.js` + - 新增 `server/templates/nginx/` 配置模板 + +## ADDED Requirements + +### Requirement: Nginx配置管理服务 +系统 SHALL 提供Nginx配置管理服务,支持自动生成和更新Nginx配置。 + +#### Scenario: 创建项目时生成Nginx配置 +- **WHEN** 用户创建新项目 +- **THEN** 系统自动为该项目生成Nginx location配置块 +- **AND** 配置文件保存到指定目录 +- **AND** 自动重载Nginx使配置生效 + +#### Scenario: 删除项目时清理Nginx配置 +- **WHEN** 用户删除项目 +- **THEN** 系统自动移除该项目的Nginx配置 +- **AND** 自动重载Nginx使配置生效 + +#### Scenario: 启动项目时更新upstream +- **WHEN** 项目启动成功 +- **THEN** 系统更新Nginx upstream配置指向正确的内部端口 +- **AND** 自动重载Nginx使配置生效 + +### Requirement: 动态路由访问 +系统 SHALL 通过动态路由路径访问不同项目,而非独立端口。 + +#### Scenario: 访问项目 +- **WHEN** 用户访问 `/project/{projectId}/` +- **THEN** Nginx将请求代理到对应项目的内部服务 +- **AND** 项目内部服务运行在固定内部端口范围(如127.0.0.1:9000-9100) + +#### Scenario: 生成项目URL +- **WHEN** 项目启动成功 +- **THEN** 系统生成格式为 `{BASE_URL}/project/{projectId}/` 的访问地址 +- **AND** 不再包含端口号 + +### Requirement: 本地开发模式支持 +系统 SHALL 支持本地开发环境,可选择是否使用Nginx。 + +#### Scenario: 本地无Nginx环境 +- **WHEN** 系统检测到本地未安装或未配置Nginx +- **THEN** 系统回退到原有的多端口模式 +- **AND** 在日志中提示可安装Nginx获得更好体验 + +#### Scenario: 本地有Nginx环境 +- **WHEN** 系统检测到本地已安装Nginx +- **THEN** 系统使用动态路由模式 +- **AND** 自动配置Nginx + +### Requirement: Nginx配置模板 +系统 SHALL 使用模板化方式管理Nginx配置。 + +#### Scenario: 主配置模板 +- **GIVEN** 系统需要生成Nginx主配置 +- **WHEN** 初始化或配置变更 +- **THEN** 使用预定义模板生成 `auto-deploy.conf` +- **AND** 模板支持变量替换(如端口范围、项目列表) + +#### Scenario: 项目location模板 +- **GIVEN** 需要为项目生成location配置 +- **WHEN** 创建或更新项目配置 +- **THEN** 使用location模板生成配置片段 +- **AND** 支持SPA路由回退处理 + +### Requirement: 配置热重载 +系统 SHALL 在配置变更后自动重载Nginx。 + +#### Scenario: 配置变更后重载 +- **WHEN** Nginx配置文件被修改 +- **THEN** 系统执行 `nginx -s reload` 命令 +- **AND** 检查重载结果 +- **AND** 失败时回滚配置并记录错误日志 + +### Requirement: 环境配置扩展 +系统 SHALL 扩展环境配置以支持Nginx相关设置。 + +#### Scenario: 新增环境变量 +- **GIVEN** 用户配置 `.env` 文件 +- **WHEN** 系统读取配置 +- **THEN** 支持以下新增配置项: + - `USE_NGINX` - 是否使用Nginx(true/false/auto) + - `NGINX_CONFIG_DIR` - Nginx配置目录路径 + - `NGINX_TEMPLATE_PATH` - 配置模板路径 + - `NGINX_RELOAD_CMD` - Nginx重载命令 + +## MODIFIED Requirements + +### Requirement: 项目URL生成 +**原需求**: 项目URL格式为 `http://{domain}:{port}/` +**修改为**: 项目URL格式为 `{BASE_URL}/project/{projectId}/` + +### Requirement: 端口管理 +**原需求**: 项目使用外部可访问端口(9000-9100) +**修改为**: 项目使用内部端口(127.0.0.1:9000-9100),外部通过Nginx代理访问 + +## Technical Design + +### 架构设计 + +``` + ┌─────────────────────────────────────┐ + │ Nginx (80/443) │ + │ │ + │ location /project/xxx/ { │ + │ proxy_pass http://127.0.0.1:9000│ + │ } │ + └──────────────┬──────────────────────┘ + │ + ┌────────────────────┼────────────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌──────────┐ ┌──────────┐ ┌──────────┐ + │ 项目1 │ │ 项目2 │ │ 项目N │ + │ :9000 │ │ :9001 │ │ :90xx │ + │(内部端口) │ │(内部端口) │ │(内部端口) │ + └──────────┘ └──────────┘ └──────────┘ +``` + +### 文件结构 + +``` +server/ +├── services/ +│ ├── nginxManager.js # 新增:Nginx配置管理 +│ ├── processManager.js # 修改:端口绑定127.0.0.1 +│ └── projectService.js # 修改:URL生成逻辑 +├── templates/ +│ └── nginx/ +│ ├── main.conf.tpl # 主配置模板 +│ └── location.conf.tpl # location配置模板 +└── config/ + └── index.js # 修改:新增Nginx配置项 + +nginx/ +└── sites-enabled/ + └── auto-deploy.conf # 生成的Nginx配置 +``` + +### Nginx配置模板示例 + +**主配置模板 (main.conf.tpl)**: +```nginx +# Auto-generated by auto-deploy-demo +# Do not edit manually + +upstream projects { + # 项目upstream配置将由系统自动生成 +} + +server { + listen {{PORT}}; + server_name {{SERVER_NAME}}; + + # 管理后台 + location / { + proxy_pass http://127.0.0.1:{{MANAGER_PORT}}; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + + # API接口 + location /api/ { + proxy_pass http://127.0.0.1:{{MANAGER_PORT}}/api/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + + # 项目路由 - 由系统动态生成 + {{PROJECT_LOCATIONS}} +} +``` + +**Location配置模板 (location.conf.tpl)**: +```nginx +# Project: {{PROJECT_NAME}} ({{PROJECT_ID}}) +location /project/{{PROJECT_ID}}/ { + proxy_pass http://127.0.0.1:{{PROJECT_PORT}}/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # SPA路由支持 + try_files $uri $uri/ /project/{{PROJECT_ID}}/index.html; +} +``` + +### 核心流程 + +**项目启动流程**: +``` +1. 查找可用内部端口 +2. 启动项目服务(绑定127.0.0.1) +3. 生成/更新Nginx location配置 +4. 重载Nginx +5. 返回项目URL: {BASE_URL}/project/{id}/ +``` + +**项目停止流程**: +``` +1. 停止项目进程 +2. 更新Nginx配置(移除upstream或标记为down) +3. 重载Nginx +``` + +**项目删除流程**: +``` +1. 停止项目(如运行中) +2. 删除项目文件 +3. 移除Nginx location配置 +4. 重载Nginx +``` + +### 本地开发模式 + +当 `USE_NGINX=auto` 时: +1. 检测系统是否安装Nginx +2. 检测Nginx配置目录是否可写 +3. 若检测失败,回退到多端口模式 +4. 在控制台输出当前模式信息 + +### 安全考虑 + +1. Nginx配置文件权限控制 +2. 配置变更前备份 +3. 配置验证(`nginx -t`)后再重载 +4. 重载失败时回滚 diff --git a/.trae/specs/nginx-dynamic-routing/tasks.md b/.trae/specs/nginx-dynamic-routing/tasks.md new file mode 100644 index 0000000..eb9ba4e --- /dev/null +++ b/.trae/specs/nginx-dynamic-routing/tasks.md @@ -0,0 +1,55 @@ +# Tasks + +- [x] Task 1: 创建Nginx配置模板系统 + - [x] SubTask 1.1: 创建 `server/templates/nginx/main.conf.tpl` 主配置模板 + - [x] SubTask 1.2: 创建 `server/templates/nginx/location.conf.tpl` location配置模板 + - [x] SubTask 1.3: 确保模板支持变量替换和SPA路由 + +- [x] Task 2: 实现Nginx配置管理服务 + - [x] SubTask 2.1: 创建 `server/services/nginxManager.js` 服务模块 + - [x] SubTask 2.2: 实现配置模板渲染功能 + - [x] SubTask 2.3: 实现Nginx配置文件读写功能 + - [x] SubTask 2.4: 实现Nginx配置验证功能(nginx -t) + - [x] SubTask 2.5: 实现Nginx重载功能(nginx -s reload) + - [x] SubTask 2.6: 实现配置回滚机制 + +- [x] Task 3: 扩展环境配置 + - [x] SubTask 3.1: 在 `server/config/index.js` 添加Nginx相关配置项 + - [x] SubTask 3.2: 更新 `.env.example` 添加新配置项说明 + - [ ] SubTask 3.3: 实现Nginx可用性检测功能 + +- [x] Task 4: 修改进程管理服务 + - [x] SubTask 4.1: 修改 `processManager.js` 使项目绑定到127.0.0.1 + - [x] SubTask 4.2: 修改端口分配逻辑,支持内部端口模式 + - [x] SubTask 4.3: 添加Nginx配置更新集成点 + +- [x] Task 5: 修改项目服务 + - [x] SubTask 5.1: 修改 `projectService.js` 的URL生成逻辑 + - [x] SubTask 5.2: 在项目创建时生成Nginx location配置 + - [x] SubTask 5.3: 在项目删除时清理Nginx配置 + - [x] SubTask 5.4: 在项目启动/停止时更新Nginx upstream状态 + +- [x] Task 6: 修改部署路由 + - [x] SubTask 6.1: 修改 `routes/deploy.js` 集成Nginx配置更新 + - [x] SubTask 6.2: 添加Nginx配置状态查询接口 + +- [x] Task 7: 实现本地开发模式兼容 + - [x] SubTask 7.1: 实现Nginx环境检测逻辑 + - [x] SubTask 7.2: 实现模式自动切换(Nginx模式/多端口模式) + - [x] SubTask 7.3: 添加模式状态日志输出 + +- [x] Task 8: 更新前端适配 + - [x] SubTask 8.1: 确认前端正确显示新的URL格式 + - [x] SubTask 8.2: 添加Nginx模式状态显示(可选) + +# Task Dependencies +- [Task 2] depends on [Task 1] - Nginx管理服务依赖模板系统 +- [Task 4] depends on [Task 2] - 进程管理修改依赖Nginx管理服务 +- [Task 5] depends on [Task 2] - 项目服务修改依赖Nginx管理服务 +- [Task 6] depends on [Task 4, Task 5] - 路由修改依赖服务和进程管理 +- [Task 7] depends on [Task 3] - 本地模式依赖配置扩展 +- [Task 8] depends on [Task 5, Task 6] - 前端适配依赖后端完成 + +# Parallelizable Work +- Task 1 和 Task 3 可以并行执行 +- Task 4 和 Task 5 可以在Task 2完成后并行执行 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..031d52f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM node:18-alpine + +WORKDIR /app + +# 复制package文件 +COPY package*.json ./ +RUN npm install --production + +# 复制后端代码 +COPY server ./server + +# 复制前端构建产物 +COPY client/dist ./client/dist + +# 创建必要的目录 +RUN mkdir -p /app/data /app/projects /app/nginx/sites-enabled + +# 暴露端口(管理后台) +EXPOSE 8888 + +# 启动服务 +CMD ["node", "server/index.js"] diff --git a/README.md b/README.md index 2fb8da9..0ef67ff 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ - **安全登录** - 支持登录失败锁定机制 - **密码修改** - 用户可自行修改密码 - **域名配置** - 支持自定义域名生成项目链接 +- **Nginx动态路由** - 支持Nginx反向代理,统一端口访问 ## 技术栈 @@ -18,12 +19,23 @@ - **前端**: Vue 3 + Vite + Tailwind CSS - **状态管理**: Pinia - **进程管理**: PM2 +- **反向代理**: Nginx(可选) ## 环境要求 - Node.js >= 16.0.0 - npm >= 8.0.0 - 操作系统: Windows / Linux / macOS +- Nginx(可选,用于动态路由模式) + +## 运行模式 + +系统支持两种运行模式: + +| 模式 | 说明 | URL格式 | 适用场景 | +|------|------|---------|----------| +| **多端口模式** | 每个项目独立端口 | `http://domain:9000/` | 本地开发、无Nginx环境 | +| **Nginx动态路由模式** | 所有项目共用80端口 | `http://domain/project/{id}/` | 服务器部署、生产环境 | ## 快速开始 @@ -60,6 +72,13 @@ BASE_DOMAIN=your-domain.com # 项目端口范围 PROJECT_PORT_START=9000 PROJECT_PORT_END=9100 + +# Nginx配置(动态路由模式) +USE_NGINX=auto +NGINX_CONFIG_DIR=./nginx/sites-enabled +NGINX_TEMPLATE_PATH=./server/templates/nginx +NGINX_RELOAD_CMD=nginx -s reload +NGINX_TEST_CMD=nginx -t ``` ### 启动服务 @@ -75,6 +94,497 @@ npm start - 用户名: `admin` - 密码: `admin123` +--- + +## Windows 本地开发环境 + +### 方式一:多端口模式(无需Nginx) + +默认配置 `USE_NGINX=auto`,系统会自动检测Nginx。本地未安装Nginx时,自动使用多端口模式。 + +```env +# .env +USE_NGINX=false +``` + +启动后访问: +- 管理后台: http://localhost:8888 +- 项目地址: http://localhost:9000、http://localhost:9001 等 + +### 方式二:Nginx动态路由模式 + +#### 1. 安装Nginx(Windows) + +**方法A - 使用Chocolatey(推荐):** +```powershell +# 安装Chocolatey(如未安装) +Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) + +# 安装Nginx +choco install nginx +``` + +**方法B - 手动安装:** +1. 下载Nginx: http://nginx.org/en/download.html (选择 Stable version → nginx/Windows) +2. 解压到 `C:\nginx` +3. 将 `C:\nginx` 添加到系统环境变量 PATH + +#### 2. 配置Nginx + +创建Nginx配置目录: +```powershell +# 在项目目录下创建 +mkdir nginx\sites-enabled +``` + +编辑 `C:\nginx\conf\nginx.conf`,在 http 块末尾添加: +```nginx +http { + # ... 其他配置 ... + + # 引入自动生成的配置 + include Q:/agentProject/auto-deploy-demo/nginx/sites-enabled/*.conf; +} +``` + +#### 3. 配置环境变量 + +```env +# .env +USE_NGINX=true +NGINX_CONFIG_DIR=./nginx/sites-enabled +NGINX_RELOAD_CMD=nginx -s reload +NGINX_TEST_CMD=nginx -t +``` + +#### 4. 启动服务 + +```powershell +# 启动Nginx +start nginx + +# 启动应用 +npm start +``` + +启动后访问: +- 管理后台: http://localhost:8888 +- 项目地址: http://localhost/project/{id}/ + +--- + +## Linux 服务器部署 + +### 方式一:多端口模式 + +适用于无需Nginx或已有其他反向代理的场景。 + +```env +# .env +USE_NGINX=false +PORT=8888 +BASE_DOMAIN=your-domain.com +``` + +防火墙配置: +```bash +sudo firewall-cmd --permanent --add-port=8888/tcp +sudo firewall-cmd --permanent --add-port=9000-9100/tcp +sudo firewall-cmd --reload +``` + +### 方式二:Nginx动态路由模式(推荐) + +#### 1. 安装Nginx + +```bash +# CentOS/RHEL +sudo yum install -y nginx +sudo systemctl enable nginx +sudo systemctl start nginx + +# Ubuntu/Debian +sudo apt update +sudo apt install -y nginx +sudo systemctl enable nginx +sudo systemctl start nginx +``` + +#### 2. 创建配置目录 + +```bash +# 在项目目录下创建 +mkdir -p /path/to/app/nginx/sites-enabled +``` + +#### 3. 配置Nginx主配置 + +编辑 `/etc/nginx/nginx.conf`,在 http 块内添加: +```nginx +http { + # ... 其他配置 ... + + # 引入自动生成的配置 + include /path/to/app/nginx/sites-enabled/*.conf; +} +``` + +#### 4. 配置环境变量 + +```env +# .env +PORT=8888 +BASE_DOMAIN=demo.your-domain.com +USE_NGINX=true +NGINX_CONFIG_DIR=/path/to/app/nginx/sites-enabled +NGINX_TEMPLATE_PATH=/path/to/app/server/templates/nginx +NGINX_RELOAD_CMD=sudo nginx -s reload +NGINX_TEST_CMD=sudo nginx -t +``` + +#### 5. 配置sudo权限(重要) + +为了让Node.js应用能够执行nginx命令,需要配置sudo免密: + +```bash +sudo visudo +``` + +添加以下内容(替换 `www-user` 为实际运行用户): +``` +www-user ALL=(ALL) NOPASSWD: /usr/sbin/nginx -s reload, /usr/sbin/nginx -t +``` + +#### 6. 防火墙配置 + +```bash +# 只需开放80和443端口 +sudo firewall-cmd --permanent --add-port=80/tcp +sudo firewall-cmd --permanent --add-port=443/tcp +sudo firewall-cmd --reload +``` + +#### 7. 启动服务 + +```bash +npm install --production +``` + +**方式A:使用systemd服务(推荐)** + +创建服务文件: +```bash +sudo nano /etc/systemd/system/auto-deploy.service +``` + +内容如下(修改路径和用户): +```ini +[Unit] +Description=Auto Deploy Demo +After=network.target + +[Service] +Type=simple +User=www-data +WorkingDirectory=/path/to/app +ExecStart=/usr/bin/node server/index.js +Restart=on-failure +RestartSec=10 + +[Install] +WantedBy=multi-user.target +``` + +启动服务: +```bash +sudo systemctl daemon-reload +sudo systemctl enable auto-deploy +sudo systemctl start auto-deploy +``` + +**方式B:使用PM2(可选)** + +```bash +sudo npm install -g pm2 +pm2 start server/index.js --name auto-deploy-demo +pm2 save +pm2 startup +``` + +启动后访问: +- 管理后台: http://demo.your-domain.com/ +- 项目地址: http://demo.your-domain.com/project/{id}/ + +--- + +## 一键打包部署(推荐) + +本地构建,服务器直接运行,无需在服务器上安装Node.js。 + +### 部署模式 + +| 模式 | 命令 | 端口 | 适用场景 | +|------|------|------|----------| +| **单端口模式** | `.\build-dist.ps1 -SinglePort` | 8080→80 | 宿主机有Nginx,只需一个端口 | +| **多端口模式** | `.\build-dist.ps1` | 8888+9000-9100 | 宿主机无Nginx,直接暴露多端口 | + +### 单端口模式架构(推荐) + +``` +用户请求 + │ + ▼ +┌─────────────────────────────────────┐ +│ 宿主机 Nginx (80) │ +│ demo.example.com → localhost:8080 │ +└──────────────┬──────────────────────┘ + │ + ▼ +┌─────────────────────────────────────┐ +│ Docker 容器 (8080→80) │ +│ ┌───────────────────────────────┐ │ +│ │ 容器内 Nginx (80) │ │ +│ │ / → 管理后台 (127.0.0.1:8888)│ │ +│ │ /project/123/ → :9000 │ │ +│ │ /project/456/ → :9001 │ │ +│ └───────────────────────────────┘ │ +│ ┌───────────────────────────────┐ │ +│ │ Node.js 服务 (127.0.0.1:8888)│ │ +│ │ 项目服务 (127.0.0.1:9000+) │ │ +│ └───────────────────────────────┘ │ +└─────────────────────────────────────┘ +``` + +### 本地打包 + +**Windows PowerShell:** +```powershell +# 单端口模式(推荐) +.\build-dist.ps1 -SinglePort + +# 多端口模式 +.\build-dist.ps1 +``` + +**Linux/Mac:** +```bash +chmod +x build-dist.sh + +# 单端口模式(推荐) +./build-dist.sh --single-port + +# 多端口模式 +./build-dist.sh +``` + +打包完成后会生成: +- `deploy-dist/` - 部署目录 +- `auto-deploy-dist-single-YYYYMMDD_HHMMSS.zip` - 压缩包 + +### 服务器部署 + +1. **上传部署包** +```bash +scp auto-deploy-dist-*.zip user@server:/opt/auto-deploy/ +``` + +2. **解压并启动** +```bash +cd /opt/auto-deploy +unzip auto-deploy-dist-*.zip +docker-compose up -d +``` + +3. **配置宿主机Nginx** + +创建 `/etc/nginx/conf.d/demo.conf`: +```nginx +server { + listen 80; + server_name demo.example.com; + + location / { + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +重载Nginx: +```bash +sudo nginx -t && sudo nginx -s reload +``` + +4. **访问管理后台** +``` +http://demo.example.com/ +``` + +### 部署包结构 + +``` +deploy-dist/ +├── Dockerfile # Docker构建文件 +├── docker-compose.yml # Docker编排文件 +├── package.json # 依赖配置 +├── server/ # 后端代码 +├── client/dist/ # 前端构建产物 +├── data/ # 数据目录(持久化) +├── projects/ # 项目文件(持久化) +├── .env # 环境变量 +└── README.md # 部署说明 +``` + +--- + +## Docker 部署(推荐) + +适用于宿主机已有Nginx的场景。 + +### 架构说明 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 宿主机 │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ Nginx (80/443) │ │ +│ │ location / { proxy_pass http://127.0.0.1:8888; } │ │ +│ │ location /project/ { proxy_pass http://127.0.0.1:9000-9100; } │ +│ └──────────────────────┬──────────────────────────────┘ │ +│ │ │ +│ ┌──────────────────────▼──────────────────────────────┐ │ +│ │ Docker Container │ │ +│ │ ┌─────────────────────────────────────────────┐ │ │ +│ │ │ Node.js App (8888) - 管理后台/API │ │ │ +│ │ └─────────────────────────────────────────────┘ │ │ +│ │ ┌─────────────────────────────────────────────┐ │ │ +│ │ │ 项目服务 (9000-9100) - 演示项目 │ │ │ +│ │ └─────────────────────────────────────────────┘ │ │ +│ └─────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 1. 构建前端 + +```bash +cd client +npm install +npm run build +cd .. +``` + +### 2. 构建Docker镜像 + +```bash +docker build -t auto-deploy-demo . +``` + +### 3. 启动容器 + +**方式A:使用docker-compose(推荐)** + +```bash +docker-compose up -d +``` + +**方式B:使用docker run** + +```bash +docker run -d \ + --name auto-deploy-demo \ + -p 8888:8888 \ + -p 9000:9000 \ + -p 9001:9001 \ + -p 9002:9002 \ + -p 9003:9003 \ + -p 9004:9004 \ + -p 9005:9005 \ + -v $(pwd)/data:/app/data \ + -v $(pwd)/projects:/app/projects \ + -e BASE_DOMAIN=your-domain.com \ + -e USE_NGINX=false \ + -e PROJECT_BIND_ADDRESS=0.0.0.0 \ + auto-deploy-demo +``` + +> **注意**: Docker模式下必须设置 `PROJECT_BIND_ADDRESS=0.0.0.0`,否则宿主机无法访问项目服务。 + +### 4. 配置宿主机Nginx + +在宿主机的Nginx配置中添加: + +```nginx +# /etc/nginx/conf.d/auto-deploy.conf + +server { + listen 80; + server_name demo.your-domain.com; + + # 管理后台和API + location / { + proxy_pass http://127.0.0.1:8888; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + +# 每个项目使用独立子域名(推荐方案) +# 项目1: http://project1.demo.your-domain.com +server { + listen 80; + server_name project1.demo.your-domain.com; + location / { + proxy_pass http://127.0.0.1:9000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} + +# 项目2: http://project2.demo.your-domain.com +server { + listen 80; + server_name project2.demo.your-domain.com; + location / { + proxy_pass http://127.0.0.1:9001; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} +``` + +### 5. 简化方案:直接使用端口访问 + +如果不需要域名,可以直接通过端口访问: + +- 管理后台: http://your-server-ip:8888 +- 项目1: http://your-server-ip:9000 +- 项目2: http://your-server-ip:9001 + +### Docker常用命令 + +```bash +# 查看日志 +docker logs -f auto-deploy-demo + +# 重启容器 +docker restart auto-deploy-demo + +# 停止容器 +docker-compose down + +# 更新部署 +docker-compose down +docker-compose build +docker-compose up -d +``` + +--- + ## 使用指南 ### 创建项目 @@ -102,6 +612,8 @@ npm start - 锁定期间显示实时倒计时 - 提供「忘记密码」入口 +--- + ## 项目结构 ``` @@ -115,7 +627,12 @@ auto-deploy-demo/ │ │ └── deploy.js # 部署相关 │ ├── services/ # 业务服务 │ │ ├── projectService.js -│ │ └── processManager.js +│ │ ├── processManager.js +│ │ └── nginxManager.js # Nginx配置管理 +│ ├── templates/ # 配置模板 +│ │ └── nginx/ +│ │ ├── main.conf.tpl +│ │ └── location.conf.tpl │ ├── middleware/ # 中间件 │ ├── utils/ # 工具函数 │ │ └── loginLimiter.js @@ -126,11 +643,15 @@ auto-deploy-demo/ │ │ ├── components/ # 通用组件 │ │ └── stores/ # 状态管理 │ └── dist/ # 构建产物 +├── nginx/ # Nginx配置目录 +│ └── sites-enabled/ # 自动生成的配置 ├── projects/ # 上传的项目存储 ├── deploy/ # 部署脚本 └── .env # 环境配置 ``` +--- + ## API 接口 ### 认证相关 @@ -159,47 +680,9 @@ auto-deploy-demo/ | POST | /api/deploy/:id/stop | 停止项目 | | GET | /api/deploy/:id/status | 获取运行状态 | | GET | /api/deploy/:id/logs | 获取运行日志 | +| GET | /api/deploy/nginx/status | 获取Nginx配置状态 | -## 服务器部署 - -### 使用部署脚本 - -```powershell -# Windows PowerShell -cd deploy -.\deploy.ps1 -``` - -### 手动部署 - -1. **安装Node.js** -```bash -curl -fsSL https://rpm.nodesource.com/setup_18.x | sudo bash - -sudo yum install -y nodejs -sudo npm install -g pm2 -``` - -2. **上传文件** -```bash -scp -r server projects client/dist package.json .env user@server:/path/to/app/ -``` - -3. **启动服务** -```bash -npm install --production -pm2 start server/index.js --name auto-deploy-demo -pm2 save -pm2 startup -``` - -4. **配置防火墙** -```bash -sudo firewall-cmd --permanent --add-port=8888/tcp -sudo firewall-cmd --permanent --add-port=9000-9100/tcp -sudo firewall-cmd --reload -``` - -详细部署指南请参考 [deploy/DEPLOY_GUIDE.md](deploy/DEPLOY_GUIDE.md) +--- ## 支持的项目类型 @@ -209,14 +692,19 @@ sudo firewall-cmd --reload | Vue/React构建产物 | 上传dist目录内容 | | Vite项目 | 自动识别并启动 | +--- + ## 注意事项 1. **文件大小**: 单个文件最大100MB 2. **端口范围**: - 管理系统: 8888 - - 演示项目: 9000-9100 + - 演示项目: 9000-9100(内部端口,Nginx模式下外部不可见) 3. **并发限制**: 最多同时运行100个项目 4. **数据备份**: 请定期备份 `server/data` 和 `projects` 目录 +5. **Nginx模式**: 需要确保Node.js进程有权限执行nginx命令 + +--- ## 常见问题 @@ -240,6 +728,23 @@ sudo firewall-cmd --reload 连续3次密码错误将锁定10分钟,请等待倒计时结束后重试。 +### Q: Nginx模式下项目无法访问? + +1. 检查Nginx是否正常运行: `nginx -t` +2. 检查配置文件是否生成: `cat nginx/sites-enabled/auto-deploy.conf` +3. 检查Nginx错误日志: `tail -f /var/log/nginx/error.log` +4. 确认Node.js有权限执行nginx命令 + +### Q: Windows下Nginx命令执行失败? + +确保Nginx安装目录已添加到系统PATH环境变量,或使用完整路径: +```env +NGINX_RELOAD_CMD=C:\nginx\nginx.exe -s reload +NGINX_TEST_CMD=C:\nginx\nginx.exe -t +``` + +--- + ## 许可证 MIT License diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md new file mode 100644 index 0000000..a55b8bb --- /dev/null +++ b/TROUBLESHOOTING.md @@ -0,0 +1,398 @@ +# 部署问题排查指南 + +## 问题描述 + +访问项目时出现 404 或 500 错误,Nginx 配置未正确生成或生效。 + +--- + +## 排查步骤 + +### 1. 检查容器是否正常运行 + +```bash +# 查看容器状态 +docker ps | grep auto-deploy-demo + +# 查看容器日志 +docker logs auto-deploy-demo + +# 查看最近日志 +docker logs --tail 50 auto-deploy-demo +``` + +**预期输出**: +- 容器状态为 `Up` +- 日志显示 `Server running on port 8888` +- 日志显示 `Running mode: Nginx dynamic routing` + +--- + +### 2. 检查项目状态 + +```bash +# 查看项目数据 +docker exec auto-deploy-demo cat /app/data/projects.json + +# 查看项目目录 +docker exec auto-deploy-demo ls -la /app/projects/ + +# 查看具体项目文件 +docker exec auto-deploy-demo ls -la /app/projects/{项目ID}/ +``` + +**检查要点**: +- 项目状态是否为 `running` +- 项目是否有 `port` 字段 +- 项目目录是否存在且包含文件 + +--- + +### 3. 检查项目服务是否运行 + +```bash +# 查看所有进程 +docker exec auto-deploy-demo ps aux + +# 查看 Node.js 进程 +docker exec auto-deploy-demo ps aux | grep node + +# 测试项目端口(假设项目端口是 9000) +docker exec auto-deploy-demo wget -qO- http://127.0.0.1:9000/ +``` + +**预期输出**: +- 应该有多个 node 进程(主服务 + 项目服务) +- 项目端口能返回 HTML 内容 + +--- + +### 4. 检查 Nginx 配置 + +```bash +# 查看 Nginx 配置文件 +docker exec auto-deploy-demo cat /app/nginx/sites-enabled/auto-deploy.conf + +# 检查 Nginx 配置语法 +docker exec auto-deploy-demo nginx -t + +# 查看 Nginx 错误日志 +docker exec auto-deploy-demo cat /var/log/nginx/error.log + +# 查看 Nginx 访问日志 +docker exec auto-deploy-demo tail -20 /var/log/nginx/access.log +``` + +**检查要点**: +- 配置文件中是否包含项目路由 +- 配置语法是否正确 +- 错误日志是否有循环重定向等问题 + +--- + +### 5. 测试内部访问 + +```bash +# 测试主页面 +docker exec auto-deploy-demo wget -qO- http://127.0.0.1:80/ | head -20 + +# 测试 API +docker exec auto-deploy-demo wget -qO- http://127.0.0.1:8888/api/projects + +# 测试项目访问(假设项目ID是 1772016995850,端口是 9000) +docker exec auto-deploy-demo wget -qO- http://127.0.0.1:80/project/1772016995850/ + +# 直接测试项目服务 +docker exec auto-deploy-demo wget -qO- http://127.0.0.1:9000/ +``` + +--- + +### 6. 常见问题及修复 + +#### 问题 1:Nginx 配置中没有项目路由 + +**症状**:配置文件中 `# 项目路由 - 由系统动态生成` 下面为空 + +**原因**: +- 项目启动时未正确更新 Nginx 配置 +- 项目对象没有传递 port 字段 + +**修复**: +```bash +# 进入容器 +docker exec -it auto-deploy-demo sh + +# 手动重新生成配置 +cd /app +node -e " +const nginxManager = require('./server/services/nginxManager'); +const projectService = require('./server/services/projectService'); +const fs = require('fs'); + +const projects = projectService.getAllProjects(); +const running = projects.filter(p => p.status === 'running'); + +running.forEach(p => { + const projectWithPort = { ...p, port: p.port }; + nginxManager.addProjectLocation(projectWithPort); +}); + +console.log(nginxManager.safeReload()); +" + +exit +``` + +--- + +#### 问题 2:循环重定向 (500 错误) + +**症状**:错误日志显示 `rewrite or internal redirection cycle` + +**原因**:Nginx 配置中的 `try_files` 导致循环 + +**修复**: +```bash +# 进入容器 +docker exec -it auto-deploy-demo sh + +# 删除 try_files 行 +sed -i '/try_files/d' /app/nginx/sites-enabled/auto-deploy.conf + +# 重载 Nginx +nginx -s reload + +exit +``` + +--- + +#### 问题 3:项目服务未启动 + +**症状**:项目端口连接被拒绝 `Connection refused` + +**原因**: +- 容器重启后项目进程丢失 +- 项目启动失败 + +**修复**: +```bash +# 在管理界面手动停止再启动项目 +# 或者重启容器 +docker restart auto-deploy-demo +``` + +--- + +#### 问题 4:404 Not Found + +**症状**:访问项目返回 404,Nginx 版本号显示在页面底部 + +**原因**: +- Nginx 配置中没有对应项目的路由 +- 项目 ID 不匹配 + +**修复**: +1. 确认项目状态为 `running` +2. 确认 Nginx 配置中包含该项目路由 +3. 手动重新生成配置(见问题 1 的修复方法) + +--- + +#### 问题 5:静态资源加载失败 (MIME type 错误) + +**症状**:控制台报错 `Expected a JavaScript-or-Wasm module script but the server responded with a MIME type of "text/html"` + +**原因**:前端项目的静态资源路径是绝对路径 `/assets/...`,被 Nginx 的 `/` location 捕获,代理到了管理后台 + +**修复**: +```bash +# 进入容器 +docker exec -it auto-deploy-demo sh + +# 修改项目 HTML 文件,将绝对路径改为相对路径 +sed -i 's|src="/|src="./|g' /app/projects/{项目ID}/index.html +sed -i 's|href="/|href="./|g' /app/projects/{项目ID}/index.html + +# 查看修改结果 +cat /app/projects/{项目ID}/index.html + +exit +``` + +**注意**:新版本已自动修复此问题,上传新项目时会自动转换路径 + +--- + +## 手动配置 Nginx + +### 手动添加项目路由 + +如果自动生成失败,可以手动添加项目路由配置: + +```bash +# 进入容器编辑配置 +docker exec -it auto-deploy-demo vi /app/nginx/sites-enabled/auto-deploy.conf +``` + +在 `# 项目路由 - 由系统动态生成` 下面添加: + +```nginx + # Project: 项目名称 (项目ID) + location /project/项目ID/ { + proxy_pass http://127.0.0.1:项目端口/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket支持 + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } +``` + +**示例**(项目ID: 1772019512922,端口: 9000): + +```nginx + # Project: lot (1772019512922) + location /project/1772019512922/ { + proxy_pass http://127.0.0.1:9000/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket支持 + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } +``` + +### 在容器外操作 Nginx + +```bash +# 测试 Nginx 配置语法 +docker exec auto-deploy-demo nginx -t + +# 重新加载 Nginx 配置(在容器外执行) +docker exec auto-deploy-demo nginx -s reload + +# 查看 Nginx 配置 +docker exec auto-deploy-demo cat /app/nginx/sites-enabled/auto-deploy.conf + +# 查看 Nginx 错误日志 +docker exec auto-deploy-demo cat /var/log/nginx/error.log + +# 查看 Nginx 访问日志 +docker exec auto-deploy-demo tail -50 /var/log/nginx/access.log +``` + +--- + +### 7. 一键诊断脚本 + +```bash +docker exec auto-deploy-demo sh -c ' +echo "=== 容器进程 ===" +ps aux | grep -E "node|nginx" +echo "" +echo "=== 项目数据 ===" +cat /app/data/projects.json 2>/dev/null | head -50 +echo "" +echo "=== Nginx 配置 ===" +cat /app/nginx/sites-enabled/auto-deploy.conf 2>/dev/null +echo "" +echo "=== Nginx 错误日志 ===" +tail -20 /var/log/nginx/error.log 2>/dev/null +echo "" +echo "=== 测试主页面 ===" +wget -qO- http://127.0.0.1:80/ 2>/dev/null | head -5 +echo "" +echo "=== 测试 API ===" +wget -qO- http://127.0.0.1:8888/api/projects 2>/dev/null | head -50 +' +``` + +--- + +### 8. 重新部署步骤 + +如果以上方法都无法解决问题,请重新部署: + +```bash +# 1. 停止并删除旧容器 +docker rm -f auto-deploy-demo + +# 2. 清理数据(谨慎操作!) +# rm -rf /opt/auto-deploy/data/* +# rm -rf /opt/auto-deploy/projects/* + +# 3. 上传新的部署包并解压 +cd /opt/auto-deploy +unzip -o auto-deploy-dist.zip + +# 4. 创建 Nginx 配置目录 +mkdir -p nginx/sites-enabled + +# 5. 构建镜像 +docker build -t auto-deploy-demo:latest . + +# 6. 启动容器(带 Nginx 配置映射) +docker run -d \ + --name auto-deploy-demo \ + --restart unless-stopped \ + -p 8181:80 \ + -v $(pwd)/data:/app/data \ + -v $(pwd)/projects:/app/projects \ + -v $(pwd)/nginx/sites-enabled:/app/nginx/sites-enabled \ + -e PORT=8888 \ + -e BASE_DOMAIN=ashai.com.cn:8181 \ + -e USE_NGINX=true \ + -e PROJECT_BIND_ADDRESS=127.0.0.1 \ + auto-deploy-demo:latest + +# 7. 查看日志 +docker logs -f auto-deploy-demo +``` + +--- + +## 架构说明 + +``` +用户请求 + ↓ +宿主机:8181 + ↓ +容器 Nginx:80 + ↓ + ├─ /api/* → Node.js:8888 + ├─ /project/{id}/* → 项目服务:900x + └─ / → 管理后台静态文件 +``` + +--- + +## 相关文件位置 + +| 文件 | 容器内路径 | 宿主机路径(如果映射) | +|------|-----------|----------------------| +| Nginx 配置 | `/app/nginx/sites-enabled/auto-deploy.conf` | `./nginx/sites-enabled/auto-deploy.conf` | +| 项目数据 | `/app/data/projects.json` | `./data/projects.json` | +| 项目文件 | `/app/projects/{项目ID}/` | `./projects/{项目ID}/` | +| Nginx 错误日志 | `/var/log/nginx/error.log` | - | +| Nginx 访问日志 | `/var/log/nginx/access.log` | - | + +--- + +## 联系方式 + +如有问题,请提供以下信息: +1. `docker logs auto-deploy-demo` 的输出 +2. `docker exec auto-deploy-demo cat /app/nginx/sites-enabled/auto-deploy.conf` +3. `docker exec auto-deploy-demo cat /app/data/projects.json` +4. `docker exec auto-deploy-demo cat /var/log/nginx/error.log` diff --git a/build-dist.ps1 b/build-dist.ps1 new file mode 100644 index 0000000..99edfc7 --- /dev/null +++ b/build-dist.ps1 @@ -0,0 +1,140 @@ +# Build deployment package +param( + [switch]$SinglePort +) + +$ErrorActionPreference = "Stop" + +$DistDir = "deploy-dist" + +Write-Host "=== Building deployment package ===" -ForegroundColor Green +if ($SinglePort) { + Write-Host "Mode: Single Port (8080->80)" -ForegroundColor Cyan +} else { + Write-Host "Mode: Multi Port (8888+9000-9100)" -ForegroundColor Cyan +} + +# 1. Clean old directory +if (Test-Path $DistDir) { + Write-Host "Cleaning old directory..." -ForegroundColor Yellow + Remove-Item -Recurse -Force $DistDir +} + +# 2. Create directory structure +Write-Host "Creating directory structure..." -ForegroundColor Yellow +New-Item -ItemType Directory -Path $DistDir -Force | Out-Null +New-Item -ItemType Directory -Path "$DistDir/server" -Force | Out-Null +New-Item -ItemType Directory -Path "$DistDir/client/dist" -Force | Out-Null +New-Item -ItemType Directory -Path "$DistDir/data" -Force | Out-Null +New-Item -ItemType Directory -Path "$DistDir/projects" -Force | Out-Null +New-Item -ItemType Directory -Path "$DistDir/nginx/sites-enabled" -Force | Out-Null +New-Item -ItemType Directory -Path "$DistDir/deploy" -Force | Out-Null + +# 3. Install backend dependencies +Write-Host "Installing backend dependencies..." -ForegroundColor Yellow +npm install --production + +# 4. Build frontend +Write-Host "Building frontend..." -ForegroundColor Yellow +Set-Location client +npm install +npm run build +Set-Location .. + +# 5. Copy files +Write-Host "Copying files..." -ForegroundColor Yellow + +# Backend code +Copy-Item -Recurse -Force "server/*" "$DistDir/server/" + +# Frontend build +Copy-Item -Recurse -Force "client/dist/*" "$DistDir/client/dist/" + +# Config files +Copy-Item -Force "package.json" "$DistDir/" +Copy-Item -Force "package-lock.json" "$DistDir/" +Copy-Item -Force ".env.example" "$DistDir/" + +# Docker files +Copy-Item -Force "deploy/start.sh" "$DistDir/deploy/" + +if ($SinglePort) { + Copy-Item -Force "deploy/Dockerfile.single" "$DistDir/Dockerfile" + Copy-Item -Force "deploy/docker-compose.single.yml" "$DistDir/docker-compose.yml" +} else { + Copy-Item -Force "deploy/Dockerfile" "$DistDir/Dockerfile" + Copy-Item -Force "deploy/docker-compose.yml" "$DistDir/docker-compose.yml" +} + +# 6. Create .env file +Write-Host "Creating .env file..." -ForegroundColor Yellow +if ($SinglePort) { + $envContent = @" +PORT=8888 +BASE_DOMAIN= +PROJECT_PORT_START=9000 +PROJECT_PORT_END=9100 +PROJECT_BIND_ADDRESS=127.0.0.1 +USE_NGINX=true +"@ +} else { + $envContent = @" +PORT=8888 +BASE_DOMAIN= +PROJECT_PORT_START=9000 +PROJECT_PORT_END=9100 +PROJECT_BIND_ADDRESS=0.0.0.0 +USE_NGINX=false +"@ +} +Set-Content -Path "$DistDir/.env" -Value $envContent -Encoding UTF8 + +# 7. Create README +$readmeContent = @" +# Deployment Guide + +## Mode: $(if ($SinglePort) { "Single Port" } else { "Multi Port" }) + +## Quick Start + +1. Edit .env file, set BASE_DOMAIN to your domain + +2. Start container: + docker-compose up -d + +3. Access admin panel: + http://your-server-ip$(if ($SinglePort) { "" } else { ":8888" }) + +## Default Account + +- Username: admin +- Password: admin123 + +## Ports + +$(if ($SinglePort) { "- 8080: Admin and all projects" } else { "- 8888: Admin panel`n- 9000-9009: Project ports" }) + +## Commands + +docker-compose logs -f +docker-compose restart +docker-compose down +"@ +Set-Content -Path "$DistDir/README.md" -Value $readmeContent -Encoding UTF8 + +# 8. Create zip +Write-Host "Creating zip file..." -ForegroundColor Yellow +$timestamp = Get-Date -Format "yyyyMMdd_HHmmss" +$modeSuffix = if ($SinglePort) { "-single" } else { "-multi" } +$zipFile = "auto-deploy-dist$modeSuffix-$timestamp.zip" +Compress-Archive -Path "$DistDir/*" -DestinationPath $zipFile + +Write-Host "" +Write-Host "=== Build Complete ===" -ForegroundColor Green +Write-Host "Mode: $(if ($SinglePort) { 'Single Port (8080->80)' } else { 'Multi Port (8888+9000-9100)' })" -ForegroundColor Cyan +Write-Host "Package: $zipFile" -ForegroundColor Cyan +Write-Host "Directory: $DistDir" -ForegroundColor Cyan +Write-Host "" +Write-Host "Upload to server and run:" -ForegroundColor Yellow +Write-Host " unzip $zipFile" -ForegroundColor White +Write-Host " docker-compose up -d" -ForegroundColor White diff --git a/build-dist.sh b/build-dist.sh new file mode 100644 index 0000000..dfc6c77 --- /dev/null +++ b/build-dist.sh @@ -0,0 +1,195 @@ +#!/bin/bash +# 部署打包脚本 (Linux/Mac) +# 使用方法: +# ./build-dist.sh # 多端口模式(默认) +# ./build-dist.sh --single-port # 单端口模式 + +set -e + +DIST_DIR="deploy-dist" +SINGLE_PORT=false + +# 解析参数 +while [[ "$#" -gt 0 ]]; do + case $1 in + --single-port) SINGLE_PORT=true ;; + *) echo "未知参数: $1"; exit 1 ;; + esac + shift +done + +echo "=== 构建部署包 ===" +if [ "$SINGLE_PORT" = true ]; then + echo "模式: 单端口模式(8080→80)" +else + echo "模式: 多端口模式(8888+9000-9100)" +fi + +# 1. 清理旧的部署目录 +if [ -d "$DIST_DIR" ]; then + echo "清理旧的部署目录..." + rm -rf "$DIST_DIR" +fi + +# 2. 创建目录结构 +echo "创建目录结构..." +mkdir -p "$DIST_DIR/server" +mkdir -p "$DIST_DIR/client/dist" +mkdir -p "$DIST_DIR/data" +mkdir -p "$DIST_DIR/projects" +mkdir -p "$DIST_DIR/nginx/sites-enabled" +mkdir -p "$DIST_DIR/deploy" + +# 3. 安装后端依赖 +echo "安装后端依赖..." +npm install --production + +# 4. 构建前端 +echo "构建前端..." +cd client +npm install +npm run build +cd .. + +# 5. 复制文件 +echo "复制文件..." + +# 后端代码 +cp -r server/* "$DIST_DIR/server/" + +# 前端构建产物 +cp -r client/dist/* "$DIST_DIR/client/dist/" + +# 配置文件 +cp package.json "$DIST_DIR/" +cp package-lock.json "$DIST_DIR/" +cp .env.example "$DIST_DIR/" + +# Docker文件 +cp deploy/start.sh "$DIST_DIR/deploy/" + +if [ "$SINGLE_PORT" = true ]; then + # 单端口模式 + cp deploy/Dockerfile.single "$DIST_DIR/Dockerfile" + cp deploy/docker-compose.single.yml "$DIST_DIR/docker-compose.yml" +else + # 多端口模式 + cp deploy/Dockerfile "$DIST_DIR/Dockerfile" + cp deploy/docker-compose.yml "$DIST_DIR/docker-compose.yml" +fi + +# 6. 创建 .env 文件 +echo "创建 .env 文件..." +if [ "$SINGLE_PORT" = true ]; then + cat > "$DIST_DIR/.env" << 'EOF' +# 服务端口(容器内部) +PORT=8888 + +# 基础域名(修改为您的域名) +BASE_DOMAIN= + +# 项目端口范围 +PROJECT_PORT_START=9000 +PROJECT_PORT_END=9100 + +# 单端口模式:项目绑定到127.0.0.1(容器内Nginx代理) +PROJECT_BIND_ADDRESS=127.0.0.1 + +# 启用内置Nginx +USE_NGINX=true +EOF +else + cat > "$DIST_DIR/.env" << 'EOF' +# 服务端口 +PORT=8888 + +# 基础域名(修改为您的域名) +BASE_DOMAIN= + +# 项目端口范围 +PROJECT_PORT_START=9000 +PROJECT_PORT_END=9100 + +# 多端口模式:绑定到0.0.0.0(宿主机可访问) +PROJECT_BIND_ADDRESS=0.0.0.0 + +# 禁用内置Nginx(使用宿主机Nginx) +USE_NGINX=false +EOF +fi + +# 7. 创建启动说明 +if [ "$SINGLE_PORT" = true ]; then + PORT_INFO="- 8080: 管理后台和所有项目(通过路径区分)" + ACCESS_URL="http://demo.example.com/" +else + PORT_INFO="- 8888: 管理后台 +- 9000-9009: 项目端口" + ACCESS_URL="http://your-server-ip:8888" +fi + +cat > "$DIST_DIR/README.md" << EOF +# 部署说明 + +## 模式: $(if [ "$SINGLE_PORT" = true ]; then echo "单端口模式"; else echo "多端口模式"; fi) + +## 快速启动 + +1. 修改 .env 文件中的 BASE_DOMAIN 为您的域名 + +2. 启动容器: + docker-compose up -d + +3. 访问管理后台: + $ACCESS_URL + +## 默认账号 + +- 用户名: admin +- 密码: admin123 + +## 端口说明 + +$PORT_INFO + +## 常用命令 + +# 查看日志 +docker-compose logs -f + +# 重启服务 +docker-compose restart + +# 停止服务 +docker-compose down + +# 更新部署 +docker-compose down +docker-compose build +docker-compose up -d +EOF + +# 8. 打包 +echo "打包..." +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +if [ "$SINGLE_PORT" = true ]; then + MODE_SUFFIX="-single" +else + MODE_SUFFIX="-multi" +fi +ZIP_FILE="auto-deploy-dist$MODE_SUFFIX-$TIMESTAMP.tar.gz" +tar -czvf "$ZIP_FILE" -C "$DIST_DIR" . + +echo "" +echo "=== 构建完成 ===" +if [ "$SINGLE_PORT" = true ]; then + echo "模式: 单端口(8080→80)" +else + echo "模式: 多端口(8888+9000-9100)" +fi +echo "部署包: $ZIP_FILE" +echo "部署目录: $DIST_DIR" +echo "" +echo "上传到服务器后执行:" +echo " tar -xzvf $ZIP_FILE" +echo " docker-compose up -d" diff --git a/deploy/Dockerfile b/deploy/Dockerfile new file mode 100644 index 0000000..21df399 --- /dev/null +++ b/deploy/Dockerfile @@ -0,0 +1,25 @@ +FROM node:18-alpine + +WORKDIR /app + +# 复制package文件并安装依赖 +COPY package*.json ./ +RUN npm install --production + +# 复制后端代码 +COPY server ./server + +# 复制前端构建产物 +COPY client/dist ./client/dist + +# 复制配置模板 +COPY server/templates ./server/templates + +# 创建必要的目录 +RUN mkdir -p /app/data /app/projects /app/nginx/sites-enabled + +# 暴露端口 +EXPOSE 8888 + +# 启动服务 +CMD ["node", "server/index.js"] diff --git a/deploy/Dockerfile.nginx b/deploy/Dockerfile.nginx new file mode 100644 index 0000000..0bea2ff --- /dev/null +++ b/deploy/Dockerfile.nginx @@ -0,0 +1,35 @@ +# Docker部署 - Nginx动态路由模式 +# 所有项目通过同一个端口访问,通过路径区分 + +FROM node:18-alpine + +# 安装Nginx +RUN apk add --no-cache nginx + +WORKDIR /app + +# 复制package文件并安装依赖 +COPY package*.json ./ +RUN npm install --production + +# 复制后端代码 +COPY server ./server + +# 复制前端构建产物 +COPY client/dist ./client/dist + +# 创建必要的目录 +RUN mkdir -p /app/data /app/projects /app/nginx/sites-enabled /run/nginx + +# 复制Nginx配置模板 +COPY server/templates ./server/templates + +# 复制启动脚本 +COPY deploy/start-nginx.sh /start-nginx.sh +RUN chmod +x /start-nginx.sh + +# 只暴露一个端口 +EXPOSE 80 + +# 启动Nginx和Node.js服务 +CMD ["/start-nginx.sh"] diff --git a/deploy/Dockerfile.single b/deploy/Dockerfile.single new file mode 100644 index 0000000..d1660f9 --- /dev/null +++ b/deploy/Dockerfile.single @@ -0,0 +1,40 @@ +# 单端口模式 - 容器内置Nginx +# 宿主机Nginx转发到容器,容器内Nginx根据路径分发到不同项目 + +FROM node:18-alpine + +# 安装Nginx +RUN apk add --no-cache nginx + +WORKDIR /app + +# 复制package文件并安装依赖 +COPY package*.json ./ +RUN npm install --production + +# 复制后端代码 +COPY server ./server + +# 复制前端构建产物 +COPY client/dist ./client/dist + +# 创建必要的目录 +RUN mkdir -p /app/data /app/projects /app/nginx/sites-enabled /run/nginx + +# 复制Nginx配置模板 +COPY server/templates ./server/templates + +# 创建Nginx主配置 +RUN echo 'events { worker_connections 1024; } \ +http { \ + include /app/nginx/sites-enabled/*.conf; \ +}' > /etc/nginx/nginx.conf + +# 暴露端口 +EXPOSE 80 + +# 复制启动脚本 +COPY deploy/start.sh /start.sh +RUN chmod +x /start.sh + +CMD ["/start.sh"] diff --git a/deploy/docker-compose.single.yml b/deploy/docker-compose.single.yml new file mode 100644 index 0000000..1e4a024 --- /dev/null +++ b/deploy/docker-compose.single.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + auto-deploy: + build: + context: . + dockerfile: Dockerfile.single + container_name: auto-deploy-demo + restart: unless-stopped + # 宿主机8080端口映射到容器80端口 + # 宿主机Nginx转发到 localhost:8080 + ports: + - "8080:80" + volumes: + - ./data:/app/data + - ./projects:/app/projects + environment: + - PORT=8888 + - BASE_DOMAIN=your-domain.com + - USE_NGINX=true + - PROJECT_BIND_ADDRESS=127.0.0.1 + - NGINX_CONFIG_DIR=/app/nginx/sites-enabled diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml new file mode 100644 index 0000000..e9be17a --- /dev/null +++ b/deploy/docker-compose.yml @@ -0,0 +1,42 @@ +# 部署包目录结构 +# +# deploy-dist/ +# ├── Dockerfile +# ├── docker-compose.yml +# ├── package.json +# ├── package-lock.json +# ├── server/ # 后端代码 +# ├── client/dist/ # 前端构建产物 +# ├── data/ # 数据目录(空) +# ├── projects/ # 项目目录(空) +# └── .env.example # 环境变量示例 + +version: '3.8' + +services: + auto-deploy: + build: . + container_name: auto-deploy-demo + restart: unless-stopped + ports: + - "8888:8888" + - "9000:9000" + - "9001:9001" + - "9002:9002" + - "9003:9003" + - "9004:9004" + - "9005:9005" + - "9006:9006" + - "9007:9007" + - "9008:9008" + - "9009:9009" + volumes: + - ./data:/app/data + - ./projects:/app/projects + environment: + - PORT=8888 + - BASE_DOMAIN=your-domain.com + - PROJECT_PORT_START=9000 + - PROJECT_PORT_END=9100 + - USE_NGINX=false + - PROJECT_BIND_ADDRESS=0.0.0.0 diff --git a/deploy/start-nginx.sh b/deploy/start-nginx.sh new file mode 100644 index 0000000..3b08125 --- /dev/null +++ b/deploy/start-nginx.sh @@ -0,0 +1,19 @@ +#!/bin/sh +# 启动Nginx和Node.js服务 + +# 初始化Nginx配置 +node -e " +const nginxManager = require('./server/services/nginxManager'); +const config = require('./server/config'); + +if (config.useNginx) { + const result = nginxManager.initConfig(); + console.log('Nginx config initialized:', result.message); +} +" + +# 启动Nginx +nginx + +# 启动Node.js服务 +exec node server/index.js diff --git a/deploy/start.sh b/deploy/start.sh new file mode 100644 index 0000000..aee4f90 --- /dev/null +++ b/deploy/start.sh @@ -0,0 +1,39 @@ +#!/bin/sh +# Startup script - Start Nginx and Node.js + +# Set working directory +cd /app + +# Set environment variables +export USE_NGINX=true +export NGINX_CONFIG_DIR=/app/nginx/sites-enabled +export NGINX_RELOAD_CMD="nginx -s reload" +export NGINX_TEST_CMD="nginx -t" +export PROJECT_BIND_ADDRESS=127.0.0.1 + +# Initialize Nginx config +node -e " +const fs = require('fs'); +const path = require('path'); + +process.chdir('/app'); +const config = require('./server/config'); +const nginxManager = require('./server/services/nginxManager'); + +// Generate initial config +const mainConfig = nginxManager.generateMainConfig(); +const configDir = '/app/nginx/sites-enabled'; +if (!fs.existsSync(configDir)) { + fs.mkdirSync(configDir, { recursive: true }); +} +fs.writeFileSync(path.join(configDir, 'auto-deploy.conf'), mainConfig); +console.log('Nginx config initialized'); +" + +# Start Nginx +echo "Starting Nginx..." +nginx + +# Start Node.js server +echo "Starting Node.js server..." +exec node server/index.js diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..86d8c89 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,32 @@ +version: '3.8' + +services: + auto-deploy: + build: . + container_name: auto-deploy-demo + restart: unless-stopped + ports: + - "8888:8888" + # 项目端口范围 - 根据需要添加 + - "9000:9000" + - "9001:9001" + - "9002:9002" + - "9003:9003" + - "9004:9004" + - "9005:9005" + - "9006:9006" + - "9007:9007" + - "9008:9008" + - "9009:9009" + volumes: + - ./data:/app/data + - ./projects:/app/projects + - ./nginx/sites-enabled:/app/nginx/sites-enabled + environment: + - PORT=8888 + - BASE_DOMAIN=your-domain.com + - PROJECT_PORT_START=9000 + - PROJECT_PORT_END=9100 + - USE_NGINX=false + # Docker模式下绑定到0.0.0.0,让宿主机可以访问 + - PROJECT_BIND_ADDRESS=0.0.0.0 diff --git a/server/config/index.js b/server/config/index.js index 1aea63d..fbe6328 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -1,25 +1,84 @@ +const { execSync } = require('child_process'); + const config = { port: parseInt(process.env.PORT, 10) || 8888, - + baseDomain: process.env.BASE_DOMAIN || '', - + projectPortStart: parseInt(process.env.PROJECT_PORT_START, 10) || 9000, projectPortEnd: parseInt(process.env.PROJECT_PORT_END, 10) || 9100, - getProjectUrl(port) { - if (this.baseDomain) { - return `http://${this.baseDomain}:${port}`; + projectBindAddress: process.env.PROJECT_BIND_ADDRESS || '127.0.0.1', + + useNginx: process.env.USE_NGINX || 'auto', + nginxConfigDir: process.env.NGINX_CONFIG_DIR || './nginx/sites-enabled', + nginxTemplatePath: process.env.NGINX_TEMPLATE_PATH || './server/templates/nginx', + nginxReloadCmd: process.env.NGINX_RELOAD_CMD || 'nginx -s reload', + nginxTestCmd: process.env.NGINX_TEST_CMD || 'nginx -t', + + nginxAvailable: null, + actualMode: null, + + checkNginxMode() { + if (this.actualMode !== null) { + return this.actualMode; } - return `http://localhost:${port}`; + + if (this.useNginx === true || this.useNginx === 'true') { + this.nginxAvailable = true; + this.actualMode = 'nginx'; + return 'nginx'; + } + + if (this.useNginx === false || this.useNginx === 'false') { + this.nginxAvailable = false; + this.actualMode = 'multiport'; + return 'multiport'; + } + + if (this.useNginx === 'auto') { + this.nginxAvailable = this._testNginxAvailable(); + this.actualMode = this.nginxAvailable ? 'nginx' : 'multiport'; + return this.actualMode; + } + + this.nginxAvailable = this._testNginxAvailable(); + this.actualMode = this.nginxAvailable ? 'nginx' : 'multiport'; + return this.actualMode; }, - + + _testNginxAvailable() { + try { + execSync('nginx -v 2>&1', { stdio: 'pipe' }); + return true; + } catch (error) { + return false; + } + }, + + isNginxMode() { + if (this.actualMode === null) { + this.checkNginxMode(); + } + return this.actualMode === 'nginx'; + }, + + getProjectUrl(projectId, port) { + if (this.isNginxMode()) { + const baseUrl = this.getBaseUrl(); + return `${baseUrl}/project/${projectId}/`; + } + const domain = this.baseDomain || 'localhost'; + return `http://${domain}:${port}`; + }, + getBaseUrl() { if (this.baseDomain) { return `http://${this.baseDomain}`; } return `http://localhost:${this.port}`; }, - + isProduction() { return !!this.baseDomain; } diff --git a/server/index.js b/server/index.js index 2726202..ef97c2d 100644 --- a/server/index.js +++ b/server/index.js @@ -5,6 +5,8 @@ const cors = require('cors'); const path = require('path'); const fs = require('fs'); const config = require('./config'); +const processManager = require('./services/processManager'); +const projectService = require('./services/projectService'); const app = express(); const PORT = config.port; @@ -28,9 +30,53 @@ if (fs.existsSync(path.join(__dirname, '../client/dist'))) { }); } -app.listen(PORT, () => { +const restoreProjects = async () => { + const projects = projectService.getAllProjects(); + const runningProjects = projects.filter(p => p.status === 'running'); + + console.log(`Found ${runningProjects.length} project(s) to restore...`); + + for (const project of runningProjects) { + try { + // Use absolute path for container environment + const projectDir = path.join('/app/projects', project.id); + console.log(`Checking project directory: ${projectDir}`); + + if (!fs.existsSync(projectDir)) { + console.log(`Project ${project.name} directory not found, marking as stopped`); + projectService.updateProjectStatus(project.id, 'stopped'); + continue; + } + + console.log(`Restoring project ${project.name}...`); + const result = await processManager.startProject(project); + projectService.updateProjectStatus(project.id, 'running', result.port, result.url); + console.log(`Restored project ${project.name} on port ${result.port}`); + } catch (error) { + console.error(`Failed to restore project ${project.name}: ${error.message}`); + projectService.updateProjectStatus(project.id, 'stopped'); + } + } + + console.log('Project restoration completed'); +}; + +app.listen(PORT, async () => { console.log(`Server running on port ${PORT}`); + + const mode = config.checkNginxMode(); + + if (mode === 'nginx') { + console.log('Running mode: Nginx dynamic routing'); + console.log(`Nginx config: ${config.nginxConfigDir}/auto-deploy.conf`); + } else { + console.log('Running mode: Multi-port (Nginx not available)'); + console.log(`Project ports: ${config.projectPortStart}-${config.projectPortEnd}`); + } + if (config.baseDomain) { console.log(`Base domain: ${config.baseDomain}`); } + + await restoreProjects(); }); diff --git a/server/routes/deploy.js b/server/routes/deploy.js index 1c4a4c8..8d62267 100644 --- a/server/routes/deploy.js +++ b/server/routes/deploy.js @@ -1,10 +1,65 @@ const express = require('express'); +const path = require('path'); const { authMiddleware } = require('../middleware/auth'); const projectService = require('../services/projectService'); const processManager = require('../services/processManager'); const router = express.Router(); +router.get('/nginx/status', authMiddleware, (req, res) => { + const nginxManager = require('../services/nginxManager'); + const config = require('../config'); + + res.json({ + useNginx: config.useNginx, + nginxAvailable: nginxManager.checkNginxAvailable(), + configPath: path.resolve(config.nginxConfigDir, 'auto-deploy.conf') + }); +}); + +router.post('/nginx/reload', authMiddleware, (req, res) => { + const nginxManager = require('../services/nginxManager'); + const config = require('../config'); + + if (!config.isNginxMode()) { + return res.status(400).json({ error: 'Not in Nginx mode' }); + } + + try { + const result = nginxManager.safeReload(); + res.json(result); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +router.post('/nginx/regenerate', authMiddleware, (req, res) => { + const nginxManager = require('../services/nginxManager'); + const projectService = require('../services/projectService'); + const config = require('../config'); + + if (!config.isNginxMode()) { + return res.status(400).json({ error: 'Not in Nginx mode' }); + } + + try { + const projects = projectService.getAllProjects(); + const runningProjects = projects.filter(p => p.status === 'running'); + + for (const project of runningProjects) { + nginxManager.addProjectLocation(project); + } + + const result = nginxManager.safeReload(); + res.json({ + ...result, + regeneratedCount: runningProjects.length + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + router.post('/:id/start', authMiddleware, async (req, res) => { const project = projectService.getProjectById(req.params.id); @@ -17,6 +72,7 @@ router.post('/:id/start', authMiddleware, async (req, res) => { } try { + projectService.updateProjectStatus(project.id, 'running', null, null); const result = await processManager.startProject(project); projectService.updateProjectStatus(project.id, 'running', result.port, result.url); diff --git a/server/services/nginxManager.js b/server/services/nginxManager.js new file mode 100644 index 0000000..ff98364 --- /dev/null +++ b/server/services/nginxManager.js @@ -0,0 +1,290 @@ +const fs = require('fs'); +const path = require('path'); +const { execSync, exec } = require('child_process'); +const config = require('../config'); + +class NginxManager { + constructor() { + this.configDir = path.resolve(config.nginxConfigDir); + this.templatePath = path.resolve(config.nginxTemplatePath); + this.backupFileName = 'auto-deploy.conf.backup'; + this.mainConfigFile = path.join(this.configDir, 'auto-deploy.conf'); + + console.log('[NginxManager] Initialized with:'); + console.log(` - configDir: ${this.configDir}`); + console.log(` - templatePath: ${this.templatePath}`); + console.log(` - mainConfigFile: ${this.mainConfigFile}`); + } + + renderTemplate(templateName, variables) { + const templateFile = path.join(this.templatePath, templateName); + + console.log(`[NginxManager] Rendering template: ${templateFile}`); + + if (!fs.existsSync(templateFile)) { + console.error(`[NginxManager] Template file not found: ${templateFile}`); + throw new Error(`Template file not found: ${templateName}`); + } + + let content = fs.readFileSync(templateFile, 'utf8'); + + Object.keys(variables).forEach(key => { + const regex = new RegExp(`{{${key}}}`, 'g'); + content = content.replace(regex, variables[key] || ''); + }); + + console.log(`[NginxManager] Template rendered successfully`); + return content; + } + + generateMainConfig() { + try { + console.log('[NginxManager] Generating main config...'); + + const projects = this._getRunningProjects(); + console.log(`[NginxManager] Found ${projects.length} running projects:`, + projects.map(p => ({ id: p.id, name: p.name, port: p.port }))); + + const projectLocations = projects.map(project => { + console.log(`[NginxManager] Generating location for project ${project.name} (ID: ${project.id}, Port: ${project.port})`); + return this.renderTemplate('location.conf.tpl', { + PROJECT_NAME: project.name, + PROJECT_ID: project.id, + PROJECT_PORT: project.port + }); + }).join('\n'); + + console.log(`[NginxManager] Project locations generated: ${projectLocations.length} chars`); + + const serverName = config.baseDomain || 'localhost'; + const port = config.isProduction() ? 80 : config.port; + + console.log(`[NginxManager] Server config: serverName=${serverName}, port=${port}`); + + const mainConfig = this.renderTemplate('main.conf.tpl', { + PORT: port, + SERVER_NAME: serverName, + MANAGER_PORT: config.port, + PROJECT_LOCATIONS: projectLocations + }); + + console.log(`[NginxManager] Main config generated: ${mainConfig.length} chars`); + return mainConfig; + } catch (error) { + console.error(`[NginxManager] Failed to generate main config: ${error.message}`); + throw new Error(`Failed to generate main config: ${error.message}`); + } + } + + addProjectLocation(project) { + console.log(`[NginxManager] addProjectLocation called for project:`, project); + + try { + this._backupConfig(); + + console.log(`[NginxManager] Checking config directory: ${this.configDir}`); + if (!fs.existsSync(this.configDir)) { + console.log(`[NginxManager] Creating config directory: ${this.configDir}`); + fs.mkdirSync(this.configDir, { recursive: true }); + } + + const serverName = config.baseDomain || 'localhost'; + const port = config.isProduction() ? 80 : config.port; + + const projectLocation = this.renderTemplate('location.conf.tpl', { + PROJECT_NAME: project.name, + PROJECT_ID: project.id, + PROJECT_PORT: project.port + }); + + const mainConfig = this.renderTemplate('main.conf.tpl', { + PORT: port, + SERVER_NAME: serverName, + MANAGER_PORT: config.port, + PROJECT_LOCATIONS: projectLocation + }); + + console.log(`[NginxManager] Writing config to: ${this.mainConfigFile}`); + fs.writeFileSync(this.mainConfigFile, mainConfig); + + console.log(`[NginxManager] Config written successfully`); + console.log(`[NginxManager] Config content preview:\n${mainConfig.substring(0, 500)}...`); + + return { success: true, message: `Added location for project ${project.name}` }; + } catch (error) { + console.error(`[NginxManager] addProjectLocation failed: ${error.message}`); + console.error(error.stack); + return { success: false, message: error.message }; + } + } + + removeProjectLocation(projectId) { + console.log(`[NginxManager] removeProjectLocation called for project: ${projectId}`); + + try { + this._backupConfig(); + + const mainConfig = this.generateMainConfig(); + + if (!fs.existsSync(this.configDir)) { + fs.mkdirSync(this.configDir, { recursive: true }); + } + + fs.writeFileSync(this.mainConfigFile, mainConfig); + + return { success: true, message: `Removed location for project ${projectId}` }; + } catch (error) { + console.error(`[NginxManager] removeProjectLocation failed: ${error.message}`); + return { success: false, message: error.message }; + } + } + + testConfig() { + try { + const testCmd = config.nginxTestCmd || 'nginx -t'; + console.log(`[NginxManager] Testing config: ${testCmd}`); + execSync(testCmd, { stdio: 'pipe' }); + console.log(`[NginxManager] Config test passed`); + return { success: true, message: 'Nginx configuration is valid' }; + } catch (error) { + const errorMessage = error.stderr ? error.stderr.toString() : error.message; + console.error(`[NginxManager] Config test failed: ${errorMessage}`); + return { success: false, message: `Configuration test failed: ${errorMessage}` }; + } + } + + reload() { + try { + const reloadCmd = config.nginxReloadCmd || 'nginx -s reload'; + console.log(`[NginxManager] Reloading Nginx: ${reloadCmd}`); + execSync(reloadCmd, { stdio: 'pipe' }); + console.log(`[NginxManager] Nginx reloaded successfully`); + return { success: true, message: 'Nginx reloaded successfully' }; + } catch (error) { + const errorMessage = error.stderr ? error.stderr.toString() : error.message; + console.error(`[NginxManager] Nginx reload failed: ${errorMessage}`); + return { success: false, message: `Nginx reload failed: ${errorMessage}` }; + } + } + + rollback() { + try { + const backupFile = path.join(this.configDir, this.backupFileName); + + if (!fs.existsSync(backupFile)) { + return { success: false, message: 'No backup file found to rollback' }; + } + + fs.copyFileSync(backupFile, this.mainConfigFile); + + return { success: true, message: 'Configuration rolled back successfully' }; + } catch (error) { + return { success: false, message: `Rollback failed: ${error.message}` }; + } + } + + checkNginxAvailable() { + try { + execSync('nginx -v', { stdio: 'pipe' }); + } catch (error) { + return { available: false, reason: 'Nginx command is not available. Please install nginx first.' }; + } + + if (!fs.existsSync(this.configDir)) { + try { + fs.mkdirSync(this.configDir, { recursive: true }); + } catch (error) { + return { available: false, reason: `Cannot create config directory: ${error.message}` }; + } + } + + try { + const testFile = path.join(this.configDir, '.write_test'); + fs.writeFileSync(testFile, 'test'); + fs.unlinkSync(testFile); + } catch (error) { + return { available: false, reason: `Config directory is not writable: ${error.message}` }; + } + + return { available: true, reason: 'Nginx is available and ready to use' }; + } + + initConfig() { + console.log('[NginxManager] initConfig called'); + + try { + if (!fs.existsSync(this.configDir)) { + console.log(`[NginxManager] Creating config directory: ${this.configDir}`); + fs.mkdirSync(this.configDir, { recursive: true }); + } + + const mainConfig = this.generateMainConfig(); + console.log(`[NginxManager] Writing initial config to: ${this.mainConfigFile}`); + fs.writeFileSync(this.mainConfigFile, mainConfig); + + return { success: true, message: 'Nginx configuration initialized successfully' }; + } catch (error) { + console.error(`[NginxManager] initConfig failed: ${error.message}`); + return { success: false, message: `Failed to initialize config: ${error.message}` }; + } + } + + _backupConfig() { + if (fs.existsSync(this.mainConfigFile)) { + const backupFile = path.join(this.configDir, this.backupFileName); + fs.copyFileSync(this.mainConfigFile, backupFile); + console.log(`[NginxManager] Config backed up to: ${backupFile}`); + } + } + + _getRunningProjects() { + // Use absolute path for container environment + const projectsFile = '/app/data/projects.json'; + + console.log(`[NginxManager] Reading projects from: ${projectsFile}`); + + if (!fs.existsSync(projectsFile)) { + console.log(`[NginxManager] Projects file not found: ${projectsFile}`); + return []; + } + + try { + const content = fs.readFileSync(projectsFile, 'utf8'); + const projects = JSON.parse(content); + console.log(`[NginxManager] Total projects: ${projects.length}`); + + const running = projects.filter(p => p.status === 'running' && p.port); + console.log(`[NginxManager] Running projects with port: ${running.length}`); + + return running; + } catch (error) { + console.error('[NginxManager] Failed to read projects.json:', error.message); + return []; + } + } + + async safeReload() { + console.log('[NginxManager] safeReload called'); + + const testResult = this.testConfig(); + if (!testResult.success) { + console.error('[NginxManager] Config test failed, aborting reload'); + return testResult; + } + + const reloadResult = this.reload(); + if (!reloadResult.success) { + console.error('[NginxManager] Reload failed, rolling back'); + this.rollback(); + return { + success: false, + message: `Reload failed, configuration rolled back. Error: ${reloadResult.message}` + }; + } + + console.log('[NginxManager] safeReload completed successfully'); + return reloadResult; + } +} + +module.exports = new NginxManager(); diff --git a/server/services/processManager.js b/server/services/processManager.js index 8cd9d5a..38a2cb7 100644 --- a/server/services/processManager.js +++ b/server/services/processManager.js @@ -5,6 +5,7 @@ const http = require('http'); const express = require('express'); const net = require('net'); const config = require('../config'); +const nginxManager = require('./nginxManager'); const PORT_RANGE_START = config.projectPortStart; const PORT_RANGE_END = config.projectPortEnd; @@ -87,7 +88,8 @@ const startStaticServer = (projectDir, port) => { } }); - const server = app.listen(port, () => { + const bindAddress = config.projectBindAddress || '0.0.0.0'; + const server = app.listen(port, bindAddress, () => { resolve({ server, port }); }); @@ -102,7 +104,8 @@ const startStaticServer = (projectDir, port) => { }; const startProject = async (project) => { - const projectDir = path.join(__dirname, '../../projects', project.id); + // Use absolute path for container environment + const projectDir = path.join('/app/projects', project.id); if (!fs.existsSync(projectDir)) { throw new Error('Project directory not found'); @@ -162,7 +165,21 @@ const startProject = async (project) => { runningProcesses.set(project.id, processInfo); - const url = config.getProjectUrl(port); + const url = config.getProjectUrl(project.id, port); + + console.log(`[ProcessManager] Project ${project.name} started on port ${port}`); + console.log(`[ProcessManager] useNginx: ${config.useNginx}, isNginxMode: ${config.isNginxMode()}`); + + if (config.useNginx) { + const projectWithPort = { ...project, port }; + console.log(`[ProcessManager] Calling nginxManager.addProjectLocation with:`, projectWithPort); + + const addResult = nginxManager.addProjectLocation(projectWithPort); + console.log(`[ProcessManager] addProjectLocation result:`, addResult); + + const reloadResult = nginxManager.safeReload(); + console.log(`[ProcessManager] safeReload result:`, reloadResult); + } return { port, url }; }; @@ -184,6 +201,11 @@ const stopProject = (project) => { runningProcesses.delete(project.id); + if (config.useNginx) { + nginxManager.removeProjectLocation(project.id); + nginxManager.safeReload(); + } + return { stopped: true, port }; }; diff --git a/server/services/projectService.js b/server/services/projectService.js index e6dc6d8..a2129fd 100644 --- a/server/services/projectService.js +++ b/server/services/projectService.js @@ -3,11 +3,14 @@ const path = require('path'); const crypto = require('crypto'); const AdmZip = require('adm-zip'); const processManager = require('./processManager'); +const nginxManager = require('./nginxManager'); +const config = require('../config'); -const DATA_DIR = path.join(__dirname, '../../data'); +// Use absolute paths for container environment +const DATA_DIR = '/app/data'; const PROJECTS_FILE = path.join(DATA_DIR, 'projects.json'); const LOGS_DIR = path.join(DATA_DIR, 'logs'); -const DEPLOY_DIR = path.join(__dirname, '../../projects'); +const DEPLOY_DIR = '/app/projects'; if (!fs.existsSync(DATA_DIR)) { fs.mkdirSync(DATA_DIR, { recursive: true }); @@ -22,6 +25,11 @@ if (!fs.existsSync(DEPLOY_DIR)) { } const getProjects = () => { + // Ensure directory exists before writing + if (!fs.existsSync(DATA_DIR)) { + fs.mkdirSync(DATA_DIR, { recursive: true }); + } + if (!fs.existsSync(PROJECTS_FILE)) { fs.writeFileSync(PROJECTS_FILE, JSON.stringify([])); return []; @@ -53,6 +61,38 @@ const calculateFileHash = (filePath) => { return crypto.createHash('md5').update(content).digest('hex'); }; +const fixHtmlPaths = (projectDir) => { + const htmlFiles = getAllFiles(projectDir).filter(f => f.endsWith('.html')); + + for (const htmlFile of htmlFiles) { + let content = fs.readFileSync(htmlFile, 'utf8'); + let modified = false; + + // Fix absolute paths in HTML attributes + // src="/assets/..." -> src="./assets/..." + // href="/assets/..." -> href="./assets/..." + // Also fix other common static resource paths + const patterns = [ + { regex: /src="\/(?!\/|http)/g, replacement: 'src="./' }, + { regex: /href="\/(?!\/|http)/g, replacement: 'href="./' }, + { regex: /src='\/(?!\/|http)/g, replacement: "src='./" }, + { regex: /href='\/(?!\/|http)/g, replacement: "href='./" }, + ]; + + for (const { regex, replacement } of patterns) { + if (regex.test(content)) { + content = content.replace(regex, replacement); + modified = true; + } + } + + if (modified) { + fs.writeFileSync(htmlFile, content); + console.log(`Fixed paths in ${htmlFile}`); + } + } +}; + const getAllProjects = () => { return getProjects(); }; @@ -96,6 +136,9 @@ const createProject = ({ name, description, files }) => { const allFiles = getAllFiles(projectDir); + // Fix absolute paths in HTML files to relative paths + fixHtmlPaths(projectDir); + const project = { id, name, @@ -167,6 +210,10 @@ const deleteProject = (id) => { processManager.stopProject(project); } + if (config.useNginx === true || config.useNginx === 'true') { + nginxManager.removeProjectLocation(id); + } + const projectDir = path.join(DEPLOY_DIR, id); if (fs.existsSync(projectDir)) { fs.rmSync(projectDir, { recursive: true, force: true }); @@ -220,7 +267,7 @@ const updateProjectStatus = (id, status, port = null, url = null) => { project.status = status; project.port = port; - project.url = url; + project.url = url || config.getProjectUrl(id, port); if (status === 'running') { project.lastDeployed = new Date().toISOString(); diff --git a/server/templates/nginx/location.conf.tpl b/server/templates/nginx/location.conf.tpl new file mode 100644 index 0000000..50e91e2 --- /dev/null +++ b/server/templates/nginx/location.conf.tpl @@ -0,0 +1,13 @@ + # Project: {{PROJECT_NAME}} ({{PROJECT_ID}}) + location /project/{{PROJECT_ID}}/ { + proxy_pass http://127.0.0.1:{{PROJECT_PORT}}/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket支持 + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } diff --git a/server/templates/nginx/main.conf.tpl b/server/templates/nginx/main.conf.tpl new file mode 100644 index 0000000..48ea0f0 --- /dev/null +++ b/server/templates/nginx/main.conf.tpl @@ -0,0 +1,28 @@ +# Auto-generated by auto-deploy-demo +# Do not edit manually + +server { + listen {{PORT}}; + server_name {{SERVER_NAME}}; + + # 管理后台 + location / { + proxy_pass http://127.0.0.1:{{MANAGER_PORT}}; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # API接口 + location /api/ { + proxy_pass http://127.0.0.1:{{MANAGER_PORT}}/api/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # 项目路由 - 由系统动态生成 +{{PROJECT_LOCATIONS}} +}