diff --git a/.env.example b/.env.example index 91b7992..049db21 100644 --- a/.env.example +++ b/.env.example @@ -6,5 +6,9 @@ DATABASE_URL="postgresql://surclaw:surclaw_dev_password@localhost:5433/surclaw?s SESSION_SECRET="change-me-in-production" SESSION_COOKIE_SECURE="false" FILE_STORAGE_DIR="./uploads" +RUN_DB_MIGRATIONS="true" +RUN_DB_SEED="true" +DOCKER_STARTUP_RETRIES=30 +DOCKER_STARTUP_RETRY_DELAY=2 VITE_API_PROXY_TARGET="http://localhost:3100" VITE_ENABLE_LOCAL_FALLBACK="true" diff --git a/AGENTS.md b/AGENTS.md index 48ff7d9..b85f593 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -123,6 +123,8 @@ npm run test:e2e ├── playwright.config.ts # Playwright E2E 配置,启动/复用 3001 前端和 3100 测试 API ├── tsconfig.json # TypeScript 配置 ├── index.html # Vite HTML 入口 +├── scripts/ +│ └── docker-api-entrypoint.sh # API 容器启动时执行迁移、seed 和服务启动 ├── e2e/ │ ├── helpers.ts # E2E 真实 API 登录和造数工具 │ ├── audit-and-route-guards.spec.ts @@ -218,6 +220,7 @@ npm run test:e2e │ ├── features.md │ ├── testing.md │ ├── data-storage.md +│ ├── docker.md │ ├── deployment.md │ ├── security.md │ ├── progress.md @@ -309,6 +312,8 @@ npm run test:e2e API 默认 JSON/urlencoded 请求体上限为 `100mb`,由 `API_BODY_LIMIT` 控制;Nginx 同步配置了 `client_max_body_size 100m`,用于承载迁移期报告 HTML、图片/关键帧和通用文件 Data URL 上传。 +Docker API 容器通过 `scripts/docker-api-entrypoint.sh` 启动,默认先执行 `prisma migrate deploy` 和 `prisma db seed`,再启动 NestJS。可用 `RUN_DB_MIGRATIONS=false`、`RUN_DB_SEED=false` 关闭自动初始化;完整说明见 `docs/docker.md`。 + Docker 前端默认暴露 `http://localhost:4002`,并额外暴露自签名 HTTPS 演示入口 `https://localhost:4443`。浏览器麦克风 API 不能在普通局域网 HTTP 页面中由应用代码强行开启;语音听写演示优先使用 HTTPS 入口,或用 Chrome/Edge 的 `--unsafely-treat-insecure-origin-as-secure` 临时标记 HTTP 来源。 ### `server/prisma/schema.prisma` diff --git a/Dockerfile.server b/Dockerfile.server index 06922ea..aeb1a7d 100644 --- a/Dockerfile.server +++ b/Dockerfile.server @@ -12,10 +12,13 @@ FROM node:20-alpine WORKDIR /app ENV NODE_ENV=production COPY package*.json ./ -RUN npm ci --omit=dev +RUN npm ci COPY --from=builder /app/server/dist ./server/dist +COPY --from=builder /app/server/src ./server/src COPY --from=builder /app/server/prisma ./server/prisma COPY --from=builder /app/prisma.config.ts ./prisma.config.ts COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma +COPY scripts/docker-api-entrypoint.sh ./scripts/docker-api-entrypoint.sh +RUN chmod +x ./scripts/docker-api-entrypoint.sh EXPOSE 3100 -CMD ["node", "server/dist/main.js"] +CMD ["./scripts/docker-api-entrypoint.sh"] diff --git a/README.md b/README.md index 5aae952..87f78d1 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,7 @@ cp .env.example .env.local - [docs/features.md](./docs/features.md):功能真实性盘点。 - [docs/testing.md](./docs/testing.md):测试文档。 - [docs/data-storage.md](./docs/data-storage.md):本地数据存储说明。 +- [docs/docker.md](./docs/docker.md):Docker Compose 一键部署、初始化、健康检查和生产变量。 - [docs/security.md](./docs/security.md):安全边界和风险。 - [docs/backendization-plan.md](./docs/backendization-plan.md):后端化、用户化改造方案。 @@ -241,10 +242,11 @@ docker-compose down - `web` 服务使用 Nginx 托管前端 `dist/`。 - Docker 前端同时暴露 `http://localhost:4002` 和自签名证书的 `https://localhost:4443`;麦克风听写建议使用 HTTPS 演示入口。 -- `api` 服务运行 NestJS 后端,并把上传文件目录挂载到 `uploads_data` volume。 +- `api` 服务运行 NestJS 后端,启动时默认执行 `prisma migrate deploy` 和 `prisma db seed`,并把上传文件目录挂载到 `uploads_data` volume。 - `db` 服务运行 PostgreSQL 16。 - `nginx.conf` 已配置 SPA 路由回退、`/api` 反向代理和 `100m` 请求体上限。 - `nginx.conf` 已支持 `/api/speech/iat` WebSocket upgrade。 +完整 Docker 说明见 [docs/docker.md](./docs/docker.md)。 ## 当前限制 diff --git a/docker-compose.yaml b/docker-compose.yaml index ef594fc..8811f72 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -11,6 +11,11 @@ services: - "5433:5432" volumes: - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U surclaw -d surclaw"] + interval: 10s + timeout: 5s + retries: 10 api: build: @@ -26,12 +31,21 @@ services: SESSION_SECRET: change-me-in-production SESSION_COOKIE_SECURE: "false" FILE_STORAGE_DIR: /app/uploads + RUN_DB_MIGRATIONS: "true" + RUN_DB_SEED: "true" ports: - "3002:3100" depends_on: - - db + db: + condition: service_healthy volumes: - uploads_data:/app/uploads + healthcheck: + test: ["CMD-SHELL", "node -e \"fetch('http://localhost:3100/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""] + interval: 10s + timeout: 5s + retries: 12 + start_period: 20s web: build: @@ -43,7 +57,13 @@ services: - "4002:80" - "4443:443" depends_on: - - api + api: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1/ || exit 1"] + interval: 10s + timeout: 5s + retries: 6 volumes: postgres_data: diff --git a/docs/README.md b/docs/README.md index ccc8832..3684764 100644 --- a/docs/README.md +++ b/docs/README.md @@ -16,6 +16,7 @@ - [后端化与用户化改造方案](./backendization-plan.md):前后端拆分、数据模型、API、权限、安全和迁移阶段。 - [数据与存储](./data-storage.md):`localStorage` / `sessionStorage` 键、数据生命周期和迁移点。 - [部署运行](./deployment.md):本地开发、后端 API、构建、Docker Compose 和 Nginx 部署方式。 +- [Docker 化部署](./docker.md):完整 Compose 服务、初始化脚本、健康检查、HTTPS、生产变量和备份恢复。 - [安全说明](./security.md):当前安全边界、敏感信息风险和生产化建议。 ## 模块文档 diff --git a/docs/deployment.md b/docs/deployment.md index 8fce0ae..9c41789 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -1,6 +1,6 @@ # 部署运行 -如果是第一次安装项目,建议先阅读 [安装与初始设置](./installation.md)。本文档主要记录开发运行、质量检查、环境变量和 Docker/Nginx 部署边界。 +如果是第一次安装项目,建议先阅读 [安装与初始设置](./installation.md)。Docker Compose 的完整一键部署、健康检查、初始化脚本和生产变量见 [Docker 化部署](./docker.md)。本文档主要记录开发运行、质量检查、环境变量和 Docker/Nginx 部署边界。 ## 本地开发 @@ -65,6 +65,9 @@ AI 和语音密钥由后端 Settings API 保存并由代理使用,前端不再 - `SESSION_SECRET`:Session Cookie 签名密钥。 - `SESSION_COOKIE_SECURE`:是否只通过 HTTPS 发送 Session Cookie。本地 HTTP/Compose 默认 `false`,生产 HTTPS 应设为 `true`。 - `FILE_STORAGE_DIR`:后端文件目录。Docker Compose 默认 `/app/uploads`,并挂载到 `uploads_data` volume。 +- `RUN_DB_MIGRATIONS`:Docker API 容器启动时是否执行 `prisma migrate deploy`,默认 `true`。 +- `RUN_DB_SEED`:Docker API 容器启动时是否执行 `prisma db seed`,默认 `true`。 +- `DOCKER_STARTUP_RETRIES` / `DOCKER_STARTUP_RETRY_DELAY`:Docker API 启动脚本等待数据库、migration 和 seed 的重试次数与间隔。 - `VITE_API_PROXY_TARGET`:前端开发服务器 `/api` 代理目标。直接运行后端用 `http://localhost:3100`;连接 Docker Compose API 用 `http://localhost:3002`。 - `VITE_ENABLE_LOCAL_FALLBACK`:生产构建是否允许本地兼容回退。开发模式默认启用,生产默认关闭。 @@ -83,6 +86,8 @@ docker-compose up -d --build - `db`:PostgreSQL 16,暴露 `localhost:5433`。 - `uploads_data`:后端文件持久化 volume。 +`api` 容器启动时默认会等待数据库健康,执行 `prisma migrate deploy` 和 `prisma db seed`,再启动 NestJS API。可通过 `RUN_DB_MIGRATIONS` 和 `RUN_DB_SEED` 关闭自动初始化。 + 构建流程: - `Dockerfile` 使用 Node 构建 `dist/`。 @@ -90,6 +95,8 @@ docker-compose up -d --build - `Dockerfile.server` 构建并运行 NestJS API。 - `nginx.conf` 已配置 SPA 路由回退、`/api` 反向代理、`100m` 请求体上限和自签名 HTTPS 演示入口。 +更完整的 Docker 说明、生产变量、证书和备份恢复见 [Docker 化部署](./docker.md)。 + ## 麦克风访问 浏览器不允许普通局域网 HTTP 页面调用麦克风,代码无法绕过这个限制。Docker 演示环境建议使用: diff --git a/docs/docker.md b/docs/docker.md new file mode 100644 index 0000000..92b975c --- /dev/null +++ b/docs/docker.md @@ -0,0 +1,185 @@ +# Docker 化部署 + +本文档说明如何用 Docker Compose 启动完整系统,以及正式部署前需要替换的配置。第一次安装也可参考 [安装与初始设置](./installation.md)。 + +## 服务组成 + +`docker-compose.yaml` 启动三个服务: + +| 服务 | 容器名 | 说明 | 宿主机端口 | +| --- | --- | --- | --- | +| `web` | `tuwen_web` | Nginx 托管前端静态文件,并代理 `/api` 到后端 | `4002`、`4443` | +| `api` | `tuwen_api` | NestJS API、Session、AI/语音代理、文件上传 | `3002` | +| `db` | `tuwen_db` | PostgreSQL 16 | `5433` | + +持久化 volume: + +- `postgres_data`:PostgreSQL 数据。 +- `uploads_data`:报告图片、签名、模板图片、视频和关键帧文件。 + +## 一键启动 + +```bash +docker compose up -d --build +``` + +首次启动时,`api` 容器会自动执行: + +1. 等待 PostgreSQL 健康。 +2. `prisma migrate deploy`,把 `server/prisma/migrations` 应用到数据库。 +3. `prisma db seed`,写入默认医院、部门、账号、模板和系统设置。 +4. 启动 NestJS API。 + +访问地址: + +```text +前端 HTTP: http://localhost:4002 +前端 HTTPS: https://localhost:4443 +API 健康: http://localhost:3002/api/health +``` + +查看状态: + +```bash +docker compose ps +``` + +查看日志: + +```bash +docker compose logs -f api +docker compose logs -f web +docker compose logs -f db +``` + +停止服务: + +```bash +docker compose down +``` + +连数据库和上传文件一起清空: + +```bash +docker compose down -v +``` + +## 初始化开关 + +API 容器启动脚本为 `scripts/docker-api-entrypoint.sh`,可用环境变量控制初始化: + +| 变量 | 默认值 | 说明 | +| --- | --- | --- | +| `RUN_DB_MIGRATIONS` | `true` | 启动 API 前执行 `prisma migrate deploy`。 | +| `RUN_DB_SEED` | `true` | 启动 API 前执行 `prisma db seed`。当前 seed 使用 upsert,不会清空业务数据。 | +| `DOCKER_STARTUP_RETRIES` | `30` | migration/seed 等待重试次数。 | +| `DOCKER_STARTUP_RETRY_DELAY` | `2` | 每次重试间隔秒数。 | + +如果正式环境希望由运维流水线单独执行 migration,可在 compose 或环境文件中设置: + +```yaml +RUN_DB_MIGRATIONS: "false" +RUN_DB_SEED: "false" +``` + +然后手动执行: + +```bash +docker compose exec api npm run prisma:deploy +docker compose exec api npm run prisma:seed +``` + +## 端口和代理 + +- 容器内 API 监听 `3100`,宿主机暴露 `3002`。 +- Nginx 对外暴露 `4002` 和 `4443`。 +- Nginx 将 `/api/` 反向代理到 `api:3100/api/`。 +- `/api/speech/iat` 使用同一条 `/api` 代理路径,并通过 WebSocket upgrade 转发。 +- `client_max_body_size 100m` 与后端 `API_BODY_LIMIT=100mb` 对齐,用于图文报告、图片、视频关键帧和 Data URL 上传。 + +## HTTPS 和麦克风 + +Chrome 不允许普通局域网 HTTP 页面调用麦克风。当前 `Dockerfile` 会生成一个只适合本机演示的自签名证书: + +```text +https://localhost:4443 +``` + +如果要用局域网 IP 或域名访问语音听写,应替换 Nginx 证书,并让证书包含实际域名/IP。正式环境建议: + +- 使用医院内网域名。 +- 通过可信 CA 或内网 CA 签发证书。 +- 把 `SESSION_COOKIE_SECURE` 设为 `true`。 +- 把 `CORS_ORIGIN` 改成真实前端 HTTPS 来源。 + +## 生产部署前必须修改 + +`docker-compose.yaml` 当前适合演示和院内试运行,生产前至少修改: + +- `POSTGRES_PASSWORD`:替换默认数据库密码。 +- `DATABASE_URL`:与新数据库密码保持一致。 +- `SESSION_SECRET`:替换为高强度随机值。 +- `CORS_ORIGIN`:只保留真实前端来源。 +- `SESSION_COOKIE_SECURE`:HTTPS 部署时设为 `true`。 +- `RUN_DB_SEED`:不需要每次启动 seed 时可设为 `false`。 +- AI/讯飞演示凭据:通过系统设置替换为正式凭据;已经暴露过的密钥应轮换。 +- HTTPS 证书:替换自签名本机证书。 + +## 备份与恢复 + +数据库备份: + +```bash +docker compose exec db pg_dump -U surclaw surclaw > surclaw_backup.sql +``` + +上传文件备份: + +```bash +docker run --rm -v surclaw_system_uploads_data:/data -v "$PWD":/backup alpine \ + tar czf /backup/uploads_backup.tgz -C /data . +``` + +恢复数据库前通常需要停服务或进入维护窗口: + +```bash +docker compose exec -T db psql -U surclaw -d surclaw < surclaw_backup.sql +``` + +## 常见问题 + +### API 容器一直 unhealthy + +查看日志: + +```bash +docker compose logs -f api +``` + +常见原因: + +- `DATABASE_URL` 密码和 `POSTGRES_PASSWORD` 不一致。 +- 数据库还没健康,等待一段时间或重启 compose。 +- migration 失败,需要检查 `server/prisma/migrations`。 + +### 页面能打开但保存失败 + +检查: + +```bash +curl http://localhost:4002/api/health +curl http://localhost:3002/api/health +docker compose logs -f api +``` + +如果报告图片或关键帧很大,确认 `API_BODY_LIMIT` 和 Nginx `client_max_body_size` 都没有被调小。 + +### 语音听写提示麦克风不可用 + +本机演示使用: + +```text +https://localhost:4443 +``` + +局域网 HTTP 访问无法由前端代码绕过浏览器限制,只能配置 HTTPS 或使用 Chrome/Edge 临时演示参数。 diff --git a/docs/installation.md b/docs/installation.md index 6349a94..f9d78dd 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -38,7 +38,6 @@ docker compose version 第一次启动: ```bash -npm install docker compose up -d --build ``` @@ -62,6 +61,8 @@ curl http://localhost:3002/api/health - `tuwen_api`:NestJS API,容器内监听 `3100`,宿主机端口 `3002`。 - `tuwen_db`:PostgreSQL 16,宿主机端口 `5433`。 +首次启动时,`tuwen_api` 会等待数据库健康,自动执行 `prisma migrate deploy` 和 `prisma db seed`,再启动 API。完整 Docker 部署细节见 [Docker 化部署](./docker.md)。 + 查看状态: ```bash diff --git a/docs/progress.md b/docs/progress.md index db5cf2b..49240e6 100644 --- a/docs/progress.md +++ b/docs/progress.md @@ -19,9 +19,10 @@ - 模板管理新增后保存内容已改为基于当前页面 state 更新,并与本地兼容缓存合并,避免旧缓存把新建模板从列表中冲掉。 - 用户管理、部门管理员约束和部门模板授权已优先接入后端 Users/Departments API;签名上传和模板图片资源已通过 Files API 写入后端文件资源。 - 系统设置、抽帧策略、AI Provider、语音参数和默认模板已优先接入 Settings API,只有开发/显式回退模式下才保留本地缓存回退。 -- Docker/Nginx 静态部署配置已存在。 +- Docker/Nginx/Compose 已可一键启动前端、API 和 PostgreSQL。 - Docker/Nginx 与 NestJS API 已把请求体上限统一到 `100mb`,避免图文报告和文件 Data URL 上传触发默认 413。 - Docker Nginx 已额外提供自签名 HTTPS 演示入口 `4443`,用于浏览器麦克风听写权限;普通局域网 HTTP 仍受浏览器限制。 +- Docker API 容器已增加启动脚本,默认自动执行 Prisma migration 和 seed,并通过 Compose healthcheck 串联数据库、API 和前端启动顺序。 - 开发端口已调整为 `3001`。 - 已补充 Vitest 测试框架和核心功能单元/组件测试。 - 已补充功能盘点,区分真实功能、外部集成、前端演示和预留项。 @@ -84,6 +85,7 @@ | 2026-05-02 | 修正报告草稿后端校验和保存失败提示,补充麦克风启动前置检查。 | | 2026-05-02 | 增加 Nginx 和 NestJS 请求体上限配置,修复大图文报告保存 `request entity too large`。 | | 2026-05-02 | 新增 Docker HTTPS 演示入口和麦克风访问说明,解决非安全上下文下语音听写不可启动的问题。 | +| 2026-05-02 | 补齐 Docker API 启动初始化脚本、Compose healthcheck 和 Docker 化部署文档。 | | 2026-05-02 | 修复模板管理中新建模板后点击保存内容导致模板从列表消失的问题,并补充单元测试和 E2E。 | | 2026-05-02 | 模板管理新增 HTML 模板包导出/导入。 | | 2026-05-02 | 修复报告编辑器新增 AI 可编辑区域后 AI 撰写下拉栏不立即更新的问题,并补充 E2E。 | diff --git a/package.json b/package.json index b52b121..7f00136 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "test": "vitest run", "test:e2e": "playwright test", "prisma:generate": "prisma generate --schema server/prisma/schema.prisma", + "prisma:deploy": "prisma migrate deploy --schema server/prisma/schema.prisma", "prisma:migrate": "prisma migrate dev --schema server/prisma/schema.prisma", "prisma:seed": "prisma db seed --schema server/prisma/schema.prisma" }, diff --git a/scripts/docker-api-entrypoint.sh b/scripts/docker-api-entrypoint.sh new file mode 100644 index 0000000..9f1a5a1 --- /dev/null +++ b/scripts/docker-api-entrypoint.sh @@ -0,0 +1,30 @@ +#!/bin/sh +set -e + +run_with_retry() { + name="$1" + shift + attempts="${DOCKER_STARTUP_RETRIES:-30}" + delay="${DOCKER_STARTUP_RETRY_DELAY:-2}" + current=1 + + until "$@"; do + if [ "$current" -ge "$attempts" ]; then + echo "Docker startup step failed after ${attempts} attempts: ${name}" >&2 + return 1 + fi + echo "Docker startup step not ready (${current}/${attempts}): ${name}. Retrying in ${delay}s..." >&2 + current=$((current + 1)) + sleep "$delay" + done +} + +if [ "${RUN_DB_MIGRATIONS:-true}" = "true" ]; then + run_with_retry "prisma migrate deploy" npx prisma migrate deploy --schema server/prisma/schema.prisma +fi + +if [ "${RUN_DB_SEED:-true}" = "true" ]; then + run_with_retry "prisma db seed" npm run prisma:seed +fi + +exec node server/dist/main.js