Compare commits

..

1 Commits

Author SHA1 Message Date
MerCry f7bc7182c9 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
2026-02-25 22:17:56 +08:00
26 changed files with 2511 additions and 58 deletions

View File

@ -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

6
.gitignore vendored
View File

@ -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/

View File

@ -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项目路由刷新正常工作

View File

@ -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` - 是否使用Nginxtrue/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. 重载失败时回滚

View File

@ -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完成后并行执行

22
Dockerfile Normal file
View File

@ -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
View File

@ -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. 安装NginxWindows
**方法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

398
TROUBLESHOOTING.md Normal file
View File

@ -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. 常见问题及修复
#### 问题 1Nginx 配置中没有项目路由
**症状**:配置文件中 `# 项目路由 - 由系统动态生成` 下面为空
**原因**
- 项目启动时未正确更新 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
```
---
#### 问题 4404 Not Found
**症状**:访问项目返回 404Nginx 版本号显示在页面底部
**原因**
- 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`

140
build-dist.ps1 Normal file
View File

@ -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

195
build-dist.sh Normal file
View File

@ -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"

25
deploy/Dockerfile Normal file
View File

@ -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"]

35
deploy/Dockerfile.nginx Normal file
View File

@ -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"]

40
deploy/Dockerfile.single Normal file
View File

@ -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"]

View File

@ -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

42
deploy/docker-compose.yml Normal file
View File

@ -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

19
deploy/start-nginx.sh Normal file
View File

@ -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

39
deploy/start.sh Normal file
View File

@ -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

32
docker-compose.yml Normal file
View File

@ -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

View File

@ -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;
}

View File

@ -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();
});

View File

@ -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);

View File

@ -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();

View File

@ -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 };
};

View File

@ -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();

View File

@ -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";
}

View File

@ -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}}
}