feat(web): 移除 Docker 与反代部署,改为 Linux 业务服务并自带管理端托管

需求变更:Web 端只保留业务需求,反向代理交由另外的服务处理,
本项目仅提供 IIS / nginx 反代说明文档,不部署反代。

主要改动:
- web-server: 新增管理端静态托管(ADMIN_DIR、SPA 深链接回退),
  反代因此只需一条全匹配转发规则;新增 adminDir 配置项
- deploy/install.sh: 删除 nginx 安装与站点配置逻辑,改为原生 Linux
  服务部署(systemd → OpenRC → SysVinit → 后台进程四级降级),
  打印反代交接指引
- deploy/ctl.sh: 启停/状态/日志脚本
- deploy/proxy/: 新增反代说明文档(README.md、nginx.conf.example、
  web.config.example),标注上传上限与超时必须同步调整
- 删除 deploy/nginx/、web-server/admin/nginx.conf、Dockerfile、
  docker-compose.yml、.dockerignore
- scripts/package-release.sh: 锚定排除规则 + 产物完整性校验
- 文档同步:README.txt、DSH.md、project.yaml、tasks/008、tasks/010
This commit is contained in:
2026-09-15 01:25:19 +08:00
parent 653d9d168c
commit 8c76b0a599
17 changed files with 581 additions and 244 deletions
+5 -1
View File
@@ -7,9 +7,13 @@
NODE_ENV=production
PORT=3000
# 监听地址。本地开发用 0.0.0.0(便于同网段设备联调);
# 生产原生部署应设为 127.0.0.1,仅由 nginx 反向代理访问
# 生产环境若反代与后端同机,建议改为 127.0.0.1
HOST=0.0.0.0
# 管理端静态站点目录(默认取 ../admin)。本服务自带管理端页面,
# 反向代理只需把请求整体转发到本端口,见 deploy/proxy/README.md
ADMIN_DIR=../admin
# ---- 现有 MySQL 实例(外部提供,本项目不部署 MySQL)----
# 当前部署:MariaDB 10.11 @ 192.168.150.10
DB_HOST=192.168.150.10
+10 -13
View File
@@ -61,24 +61,21 @@ npm run build # 产物在 dist/
`vite.config.ts` 使用 `base: './'`,产物使用相对路径引用,可挂在任意子路径下。
**生产部署(Linux 原生站点,不使用 Docker)**
**生产部署(业务服务自带管理端,不需要反代托管静态文件)**
构建产物 `dist/` 由同一站点的 nginx 直接托管,API 由 nginx 反向代理到本机 Node 进程。
整个过程由仓库根目录的 `deploy/install.sh` 一键完成:
构建产物 `dist/` 会被安装到 `$APP_DIR/admin`,并由 **NestJS 服务自身**托管
(含 SPA 深链接回退)。因此反向代理只需要把请求**整体转发**到后端端口即可,
不需要在代理层再配置静态站点或 `try_files`。
```bash
# 仓库根目录
sudo bash deploy/install.sh
# 仓库根目录,一键部署后端(含管理端)
bash deploy/install.sh
```
对应的 nginx 站点配置见 `deploy/nginx/opendance.conf`:
- `root` 指向安装后的 `admin/` 目录,`try_files $uri $uri/ /index.html` 支持 SPA 路由;
- `/api/`、`/health`、`/static/` 反向代理到 `127.0.0.1:3000`(Node 只监听回环);
- `client_max_body_size 512m` 与后端 `UPLOAD_MAX_BYTES` 保持一致。
本目录下的 `nginx.conf` 仅用于「把管理端单独托管到另一个 nginx 实例」的场景,
其中的 `proxy_pass http://api:3000` 需要按实际情况改成后端地址。
- 后端通过 `ADMIN_DIR` 定位管理端产物,默认 `../admin`
- 反代配置样例:`deploy/proxy/nginx.conf.example`(nginx)、
`deploy/proxy/web.config.example`(IIS)
- 完整说明:`deploy/proxy/README.md`
## 目录结构
-39
View File
@@ -1,39 +0,0 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
gzip on;
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
gzip_min_length 1024;
# SPA 路由回退
location / {
try_files $uri $uri/ /index.html;
}
# 上传大文件(模型 / 动作)时不要过早断开
client_max_body_size 512m;
location /api/ {
proxy_pass http://api:3000;
proxy_http_version 1.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;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}
location /health {
proxy_pass http://api:3000;
proxy_http_version 1.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;
}
}
+13 -2
View File
@@ -34,8 +34,9 @@ export class AppConfig {
}
/**
* 监听地址。原生部署(nginx 前置)时设为 127.0.0.1,
* 仅允许本机反代访问,不直接暴露 Node 进程。
* 监听地址。
* 反向代理与后端同机时建议设为 127.0.0.1(仅本机可访问);
* 代理在其他机器上时必须为 0.0.0.0,并用防火墙限制来源。
*/
get bindHost(): string {
return this.config.get<string>('bindHost') ?? '0.0.0.0';
@@ -45,6 +46,15 @@ export class AppConfig {
return this.config.get<string>('storageDir') ?? resolve(process.cwd(), 'storage');
}
/**
* 管理端静态站点目录。
* 本服务自带管理端页面,反向代理只需把请求整体转发到本端口即可,
* 不需要代理层再单独托管静态文件(见 deploy/proxy/README.md)。
*/
get adminDir(): string {
return this.config.get<string>('adminDir') || resolve(process.cwd(), '..', 'admin');
}
get uploadMaxBytes(): number {
return this.config.get<number>('uploadMaxBytes') ?? 512 * 1024 * 1024;
}
@@ -85,6 +95,7 @@ export const configuration = () => ({
port: Number(process.env.PORT ?? 3000),
bindHost: process.env.HOST ?? '0.0.0.0',
storageDir: process.env.STORAGE_DIR ?? resolve(process.cwd(), 'storage'),
adminDir: process.env.ADMIN_DIR ?? resolve(process.cwd(), '..', 'admin'),
uploadMaxBytes: Number(process.env.UPLOAD_MAX_BYTES ?? 512 * 1024 * 1024),
assimpBin: process.env.ASSIMP_BIN ?? 'assimp',
contractsDir: process.env.CONTRACTS_DIR ?? '',
+40 -1
View File
@@ -1,8 +1,9 @@
import { Logger, ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { NextFunction, Request, Response } from 'express';
import { existsSync, mkdirSync } from 'node:fs';
import { resolve } from 'node:path';
import { extname, join, resolve } from 'node:path';
import { AppModule } from './app.module';
import { AppConfig, configuration, validateEnv } from './config/configuration';
@@ -51,6 +52,39 @@ async function bootstrap(): Promise<void> {
app.useStaticAssets(resolve(config.contractsDir), { prefix: '/contracts/', index: false });
}
// ---- 管理端静态站点 ----
// 本服务同时提供管理端页面,因此反向代理只需要把全部请求转发到本端口,
// 不需要在代理层再单独配置一个静态站点(IIS / nginx 示例见 deploy/proxy/)。
const adminDir = resolve(config.adminDir);
const adminIndex = join(adminDir, 'index.html');
if (existsSync(adminIndex)) {
app.useStaticAssets(adminDir, { index: 'index.html', fallthrough: true });
// SPA 深链接回退:/songs、/models 这类前端路由在磁盘上没有对应文件,
// 需要返回 index.html 交给前端路由处理。
// 注意必须放行后端路径,否则会吞掉 API 请求。
app.use((req: Request, res: Response, next: NextFunction) => {
if (req.method !== 'GET' && req.method !== 'HEAD') return next();
const path = req.path;
if (
path.startsWith('/api/') ||
path === '/health' ||
path.startsWith('/static/') ||
path.startsWith('/contracts/')
) {
return next();
}
// 带扩展名的请求视为静态资源,未命中就让它 404,不要回退成 HTML
if (extname(path) !== '') return next();
res.sendFile(adminIndex);
});
} else {
logger.warn(
`未找到管理端构建产物(${adminIndex}),本服务仅提供 API。` +
'如需管理界面,请部署 admin/ 目录并设置 ADMIN_DIR。',
);
}
app.enableShutdownHooks();
const db = config.db;
@@ -60,6 +94,11 @@ async function bootstrap(): Promise<void> {
logger.log(`健康检查:http://${config.bindHost}:${config.port}/health`);
logger.log(`数据库:${db.host}:${db.port}/${db.name}(现有实例,本服务不管理 MySQL)`);
logger.log(`文件存储:${storageDir}`);
logger.log(
existsSync(adminIndex)
? `管理端站点:http://${config.bindHost}:${config.port}/(由本服务直接提供)`
: '管理端站点:未部署(仅 API)',
);
logger.log(`契约版本:${JSON.stringify(config.contractVersions)}`);
}