feat(web): 部署方式由 Docker 改为 Linux 原生站点(nginx + systemd)

需求变更:Web 服务端不再使用 Docker,改为直接运行在 Linux 上的网站。

- 移除 deploy/docker-compose.yml、web-server/Dockerfile、
  web-server/admin/Dockerfile、web-server/.dockerignore
- 新增 deploy/install.sh:建服务用户与目录 → 装运行期依赖 → 迁移
  → 注册 systemd 服务 → 配置 nginx 站点 → 健康检查,一键完成
- 新增 deploy/systemd/opendance-api.service:ProtectSystem=strict 等
  安全加固,ReadWritePaths 限定为上传目录
- 新增 deploy/nginx/opendance.conf:同站点托管管理端静态资源 +
  反代 /api /health /static,client_max_body_size 512m,SPA 回退
- 新增 deploy/uninstall.sh:默认保留数据,--purge 才删除
- 新增配置项 HOST(AppConfig.bindHost):原生部署下 Node 只监听
  127.0.0.1,不直接对外暴露,实测外部 IP 不可达
- 同步更新 contracts/rest-api.yaml、DSH.md、project.yaml、README.txt、
  tasks/008、tasks/010 与发布打包脚本
This commit is contained in:
2026-09-14 22:42:56 +08:00
parent 0c133986b4
commit 75498db1c0
18 changed files with 516 additions and 315 deletions
-11
View File
@@ -1,11 +0,0 @@
node_modules
dist
coverage
storage
data
.env
.env.*
!.env.example
*.log
test
admin
-50
View File
@@ -1,50 +0,0 @@
# OpenDance Studio Web 服务端 —— 生产镜像
#
# 构建上下文为 web-server/(与 contracts/database.md、deploy/docker-compose.yml 一致)。
# 镜像内**不包含 MySQL**:数据库为外部现有实例,通过环境变量注入连接信息。
# ---------- 构建阶段 ----------
FROM node:22-alpine AS builder
WORKDIR /app
# 先装依赖,最大化利用层缓存
COPY package.json package-lock.json ./
RUN npm ci --no-audit --no-fund
# 编译 TypeScript
COPY tsconfig.json tsconfig.build.json nest-cli.json ./
COPY src ./src
RUN npm run build
# 仅保留运行时依赖
RUN npm prune --omit=dev
# ---------- 运行阶段 ----------
FROM node:22-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production \
PORT=3000 \
STORAGE_DIR=/data/storage
# 迁移需要编译后的 data-source.js + typeorm CLI(均为运行时依赖)
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./package.json
COPY scripts ./scripts
COPY .env.example ./.env.example
# 非 root 运行;上传目录为卷挂载点
RUN mkdir -p /data/storage && chown -R node:node /data /app
USER node
VOLUME ["/data/storage"]
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
CMD ["node", "dist/main.js"]
-17
View File
@@ -1,17 +0,0 @@
# ---- 构建阶段 ----
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci || npm install
COPY . .
RUN npm run build
# ---- 运行阶段 ----
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
+9
View File
@@ -33,6 +33,14 @@ export class AppConfig {
return this.config.get<number>('port') ?? 3000;
}
/**
* 监听地址。原生部署(nginx 前置)时设为 127.0.0.1,
* 仅允许本机反代访问,不直接暴露 Node 进程。
*/
get bindHost(): string {
return this.config.get<string>('bindHost') ?? '0.0.0.0';
}
get storageDir(): string {
return this.config.get<string>('storageDir') ?? resolve(process.cwd(), 'storage');
}
@@ -75,6 +83,7 @@ function bool(value: string | undefined, fallback = false): boolean {
export const configuration = () => ({
nodeEnv: process.env.NODE_ENV ?? 'development',
port: Number(process.env.PORT ?? 3000),
bindHost: process.env.HOST ?? '0.0.0.0',
storageDir: process.env.STORAGE_DIR ?? resolve(process.cwd(), 'storage'),
uploadMaxBytes: Number(process.env.UPLOAD_MAX_BYTES ?? 512 * 1024 * 1024),
assimpBin: process.env.ASSIMP_BIN ?? 'assimp',
+3 -3
View File
@@ -54,10 +54,10 @@ async function bootstrap(): Promise<void> {
app.enableShutdownHooks();
const db = config.db;
await app.listen(config.port, '0.0.0.0');
await app.listen(config.port, config.bindHost);
logger.log(`OpenDance Studio Web 服务端已启动:http://0.0.0.0:${config.port}`);
logger.log(`健康检查:http://0.0.0.0:${config.port}/health`);
logger.log(`OpenDance Studio Web 服务端已启动:http://${config.bindHost}:${config.port}`);
logger.log(`健康检查:http://${config.bindHost}:${config.port}/health`);
logger.log(`数据库:${db.host}:${db.port}/${db.name}(现有实例,本服务不管理 MySQL)`);
logger.log(`文件存储:${storageDir}`);
logger.log(`契约版本:${JSON.stringify(config.contractVersions)}`);