Compare commits
1 Commits
main
...
release1.1
| Author | SHA1 | Date |
|---|---|---|
|
|
f7bc7182c9 |
12
.env.example
12
.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
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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项目路由刷新正常工作
|
||||
|
|
@ -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. 重载失败时回滚
|
||||
|
|
@ -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完成后并行执行
|
||||
|
|
@ -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"]
|
||||
589
README.md
589
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
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -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 };
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -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}}
|
||||
}
|
||||
Loading…
Reference in New Issue