Deployment
受众:开发者 摘要:部署模式(dev / test / prod / lite)/ 目录语义 / 生产部署建议 / Nginx 静态优先完整配置(根入口 / 五端入口 / API 代理 / WebSocket 101)。
本文档说明运行模式、目录语义和生产部署建议。
如果你需要一份可直接复制到服务器执行的完整流程,先看 生产部署样板。
Profile
常用 profile:
| Profile | 用途 |
|---|---|
dev | 本地开发,默认激活 |
test | 测试环境 |
prod | 生产环境 |
lite | 历史兼容 / 隔离诊断的单进程模式,不推荐普通开发或生产使用 |
设置方式:
SPRING_PROFILES_ACTIVE=prod java -jar spring-open-application.jar开发时通常直接使用 Maven:
./mvnw -pl spring-open-application spring-boot:run默认不需要额外声明运行目录。未配置 APP_STORAGE_DIR / APP_PUBLIC_DIR 时,应用会分别使用 ${user.dir}/storage 和 ${user.dir}/public;本地开发如果直接从 spring-open-application 模块目录启动,框架会自动识别仓库布局并把默认目录回退到仓库根目录下的 storage/ 和 public/。
全新 checkout 或执行过 mvn clean 后,先运行 ./mvnw -pl spring-open-application -am install -DskipTests -DskipITs -DskipFrontend 安装应用依赖,再运行应用模块。不要把 -am 和 spring-boot:run 组合使用,避免 Maven 把运行目标施加到 reactor 里的 POM 模块。
如果需要把运行产物放到其他目录,可以使用环境变量覆盖:
APP_STORAGE_DIR="$(pwd)/storage" \
APP_PUBLIC_DIR="$(pwd)/public" \
./mvnw -pl spring-open-application spring-boot:run目录语义
| 目录 | 说明 |
|---|---|
public/ | 公开静态资源,可挂载到站点根路径 |
storage/ | 私有持久化根目录 |
storage/database/ | H2 lite 模式数据库文件 |
storage/runtime/logs/ | 应用日志 |
storage/runtime/ | 运行临时文件,可删除 |
storage/runtime/cache/ | 文件缓存,可删除 |
默认 Tomcat 运行目录:
${APP_STORAGE_DIR:${user.dir}/storage}/runtime/tomcat默认日志目录:
${APP_STORAGE_DIR:${user.dir}/storage}/runtime/logspublic 目录
public/ 会挂载到站点根路由:
public/test.txt -> http://localhost:8000/test.txt本地公开存储 disk 默认也写入 public/。生产环境建议用 Nginx 或对象存储承载公开文件,应用只保存相对路径或对象 key,避免域名变更导致历史内容失效。
storage 目录
storage/ 用于私有持久化文件、密钥、证书、H2 文件库、日志和运行产物。
生产环境如需单独挂载私有持久化目录,可以使用:
/home/app/storage并通过可选环境变量 APP_STORAGE_DIR 指定:
APP_STORAGE_DIR=/home/app/storage公开静态目录默认使用 ${user.dir}/public;需要独立挂载时,可通过可选环境变量 APP_PUBLIC_DIR 指定:
APP_PUBLIC_DIR=/home/app/public数据库迁移
项目使用 Liquibase。
默认 changelog:
classpath:/db/changelog/db.changelog-master.xml默认表名:
database_changelog
database_changelog_lock生产环境首次启动前应确认数据库账号有建表和改表权限。上线后不要手动修改已经执行过的 changelog。
文档站点打包
正式文档 Markdown 源文件放在根目录 docs/。
前端构建工程放在 views/docs/,避免根目录被 package.json、node_modules 等 Node 工程文件污染。后续后台管理 Vue3、H5、PC、uniapp 等前端工程也统一放入 views/,再由对应 Maven 模块决定是否构建和复制产物。
服务器构建文档站点:
./mvnw -pl spring-open-views/spring-open-view-docs package -DskipTests -DskipITs -Dbuild.skip.views=false也可以直接执行:
./mvnw package -DskipTestsspring-open-view-docs 是 spring-open-views 下的 POM 构建模块,默认 build.skip.views=false,因此根项目 package 会自动执行文档站点构建。使用 IDEA 时,直接在 Maven 面板双击根项目 spring-open Lifecycle 里的 package 即可。
应用模块不依赖 docs 模块,避免 spring-open-application 被文档构建逻辑污染。
如果当前不想构建文档前端资源,可以通过 Maven property 显式跳过:
./mvnw package -Dbuild.skip.views=true -DskipTestsdocs 模块会自动执行:
- 下载固定版本 Node.js 和 pnpm。
- 在
views/docs/执行pnpm install --frozen-lockfile。 - 执行
pnpm docs:build。 - 将 VitePress 产物导出到
public/docs。
build.skip.views 默认是 false。命令行 -Dbuild.skip.views=true 可以跳过前端构建。
应用启动后,public/ 会挂载到站点根路由,因此默认可以访问:
http://localhost:8000/docs/public/docs 是打包产物目录,public/.gitignore 会忽略 docs/,实际构建出来的 HTML、JS、CSS 不提交 Git。生产环境也可以直接让 Nginx 指向该目录,或者把这份产物复制到任意公开路径。
文档站点按相对路径构建,并在导出时按页面层级重写资源与站内链接,不写死 /docs/assets/...、/help/assets/... 或 /assets/...。导出产物保留 VitePress 客户端增强能力,支持搜索和右上角明暗外观切换;同时会注入运行时 base 识别逻辑,因此线上可以挂载到任意路径,例如 /docs/、/help/、/manual/,不需要重新构建。
hashmap.json 是 VitePress 客户端路由元数据,不能在导出阶段删除。它不是源码依赖文件,也不会污染 Node 工程。
Nginx 使用 root /path/to/public 时,public/docs 可以直接作为 /docs/ 访问。若要换成 /help/ 或 /manual/,可以把产物复制到对应公开目录,不需要重新构建。推荐为文档目录单独补一段静态查找规则:
location ^~ /docs/ {
index index.html;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
add_header Pragma "no-cache" always;
expires off;
try_files $uri $uri.html $uri/ =404;
error_page 404 /docs/404.html;
}这段配置遵循 VitePress 静态部署思路:按真实文件查找,不把所有请求回退到 index.html。^~ 很关键:它会让 /docs/ 下的 HTML、JS、CSS、JSON 都命中这段规则,避免被全站 location ~ .*\.(js|css)?$ 这类缓存规则覆盖。否则浏览器可能继续使用旧的 VitePress 客户端 JS,看起来就像“重新打包后功能又回到旧的失效状态”。
如果部署到其他任意层级目录,例如 /help/ 或 /product/manual/,只需要把 public/docs 复制到对应公开目录,并把上面的 location ^~ /docs/ 改成实际路径;构建产物内部使用相对路径,不需要重新构建。
参考:VitePress 部署文档。
本地预览文档仍直接使用 VitePress:
cd views/docs
pnpm install
pnpm docs:dev构建产物不提交 Git,只在 Maven 构建时进入 public/docs。
生产配置
生产环境建议:
- 使用外部 MySQL 或 PostgreSQL。
- 使用外部 Redis。
- 配置强随机
APP_SECRET,或为 JWT / 支付 / Web3 等高敏能力单独配置专用密钥。 - 支付、短信、邮件、存储、Webhook 等密钥放入环境变量、部署平台密钥或数据库配置。
- 私钥、证书和敏感文件放入
storage/keys或独立安全挂载目录。 - 公开文件优先使用对象存储或 Nginx。
示例:
APP_NAME=spring-open \
APP_SECRET=change_me_long_random_secret \
APP_STORAGE_DIR=/home/app/storage \
SPRING_PROFILES_ACTIVE=prod \
DB_HOST=127.0.0.1 \
DB_PORT=3306 \
DB_DATABASE=spring_open \
DB_USERNAME=spring_open \
DB_PASSWORD=spring_open \
REDIS_HOST=127.0.0.1 \
REDIS_PORT=6379 \
REDIS_DATABASE=0 \
java -jar spring-open-application.jarLite 模式
lite 模式只保留给历史兼容、隔离诊断或极少数单进程验证场景;普通开发不推荐使用:
- H2 文件数据库。
- file cache。
- memory queue。
- memory scheduler。
- Sa-Token memory store。
- 不依赖外部 Redis。
启动示例:
SPRING_PROFILES_ACTIVE=lite java -jar spring-open-application.jarlite 不建议用于普通开发、生产或多实例部署。
默认 H2 文件连接使用 DATABASE_TO_UPPER=false,用于保证同一个 storage/database/spring_open.db 在停止后再次启动时,Liquibase 能正确复用 DATABASE_CHANGELOG 表。H2 大小写模式属于文件库创建期配置,已存在的 lite 数据库不要在不同大小写模式 URL 间切换;如果需要保留旧数据,应导出 / 导入迁移或继续显式使用原 DB_URL。
Nginx 静态优先部署
裸机 java -jar 部署时,推荐前置 Nginx 采用静态优先模式:Nginx 直接读取 public/ 下的前端资源(五端视图 + 文档站 + 公开文件),只把动态请求代理到 Java 应用。这样前端访问不经过 Java,静态资源更快、应用更专注于 API。
动态请求只有 5 类需要代理到后端:
/api/** REST API,含 /api/ws WebSocket
/actuator/** 健康 / 信息 / 指标
/swagger-ui(.html) OpenAPI UI
/v3/api-docs** OpenAPI 元数据其余路径全部由 Nginx 按静态文件查找,未命中再回退到对应入口的 index.html。
站点关闭或维护使用 spring.open.site.access.* 配置控制,常用环境变量为 SPRING_OPEN_SITE_ACCESS_STATUS、SPRING_OPEN_SITE_ACCESS_MESSAGE、SPRING_OPEN_SITE_ACCESS_RESUME_AT、SPRING_OPEN_SITE_ACCESS_ALLOW_ADMIN 和 SPRING_OPEN_SITE_ACCESS_ALLOW_API_DOCS。部署层不要拦截 /api/v1/site,也不要阻断 App / PC 静态入口;前端需要先加载静态壳并读取站点信息接口,才能向用户展示关闭 / 维护说明和预计恢复时间。
完整配置
以下是与项目内置 Nginx(docker/nginx/default.conf.template)等价的裸机配置,后端按默认端口 8000,public/ 作为静态根目录:
upstream spring-open-backend {
server 127.0.0.1:8000;
}
server {
listen 80;
server_name your.domain.com;
absolute_redirect off;
root /home/app/public;
index index.html;
client_max_body_size 50m;
proxy_http_version 1.1;
proxy_set_header Host $http_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_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
# 健康检查:不经过 Java,供负载均衡探活
location = /nginx-health {
access_log off;
add_header Content-Type text/plain;
return 200 "ok\n";
}
# WebSocket 精确代理;必须放在普通 /api/ 之前,匹配前端 ws(s)://host/api/ws?... 连接
location = /api/ws {
proxy_pass http://spring-open-backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_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_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
# API 代理到 Java 应用
location /api/ {
set $spring_open_connection "";
if ($http_upgrade != "") {
set $spring_open_connection "upgrade";
}
proxy_pass http://spring-open-backend;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $spring_open_connection;
proxy_set_header Host $http_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_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
# Actuator / OpenAPI 代理到 Java 应用
location ~ ^/(?:actuator|swagger-ui(?:\.html)?|v3/api-docs)(?:/|$) {
proxy_pass http://spring-open-backend;
}
# 五端裸入口:统一补尾部斜杠,避免相对静态资源被解析到站点根路径
location ~ ^/(?<view>admin|app|pc|install|docs)$ {
return 301 /$view/$is_args$args;
}
# 五端入口:带扩展名的静态资源严格按真实文件查找,未命中即 404
location ~ ^/(?<view>admin|app|pc|install|docs)/.+\.[A-Za-z0-9]+$ {
try_files $uri =404;
}
# 五端入口:无扩展名路径回退到各自 index.html,支持前端 history 路由
location ~ ^/(?<view>admin|app|pc|install|docs)(?:/.*)?$ {
try_files $uri $uri/index.html /$view/index.html;
}
# 根入口:回退到站点根 index.html
location / {
try_files $uri $uri/ /index.html;
}
}规则说明
| 规则 | 作用 |
|---|---|
五端裸入口 location ~ ^/(?<view>...)$ | 把 /install 这类路径重定向到 /install/,防止 ./assets/*.js 被浏览器解析成 /assets/*.js 后拿到 HTML |
根入口 location / | 站点根静态资源,未命中回退根 index.html |
五端静态资源 …/.+\.ext$ | admin / app / pc / install / docs 下带扩展名的资源严格按文件查找,未命中返回 404,便于定位真实资源缺失,不误回退 HTML |
五端 SPA 回退 …(?:/.*)?$ | 无扩展名路径(前端路由)回退到 /<view>/index.html,浏览器可直接输入或刷新内部路由 |
| WebSocket 代理 | /api/ws 精确代理到后端,必须放在普通 /api/ 前面,并按 Upgrade / Connection 完成 101 切换 |
| API 代理 | /api/** 代理到后端;proxy_pass 保留原始 URI,不追加 /api/ 后缀 |
| Actuator / OpenAPI 代理 | actuator / swagger-ui / v3/api-docs 代理到后端 |
健康检查 /nginx-health | Nginx 本地返回 ok,不打到 Java,供探活 |
/api/ws 注意事项
WebSocket 默认入口统一为 /api/ws。这样 REST API 和实时连接都走 /api/,一套反向代理、转发头、HTTPS 终止和安全策略即可覆盖动态请求,也避免根路径 /ws 与静态站点或 SPA fallback 规则冲突。
采用精确 WebSocket 代理时需要注意:
location = /api/ws必须放在普通location /api/前面,真实握手应返回101 Switching Protocols。proxy_pass后面不要写/api/或/api/ws后缀,应让 Nginx 原样转发 URI。/api/**级别的 WAF、限流、审计、鉴权和日志规则要兼容 WebSocket 长连接,不要把/api/ws当普通 REST 请求强制短超时、改写请求体或吞掉 Upgrade 头。- 旧客户端如果仍使用
/ws,需要同步改配置;确需平滑迁移时,可以由部署层临时转发到/api/ws,但项目默认文档和前端配置都以/api/ws为准。
推荐显式拆出 location = /api/ws,让前端报错中的 /api/ws?... 和 Nginx 配置一一对应,也避免服务器面板已有静态 / API 规则误吞 WebSocket Upgrade。普通 REST API 继续使用 location /api/。两个 proxy_pass 都不要追加 /api/ 或 /api/ws 后缀:
location = /api/ws {
proxy_pass http://spring-open-backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_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_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location /api/ {
proxy_pass http://spring-open-backend;
proxy_http_version 1.1;
proxy_set_header Host $http_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_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}反向代理头与站点地址识别
应用按收到的请求 scheme / host / port 推断站点地址(spring.open.site.auto-detect-url,见 config.md)。上面配置已透传 X-Forwarded-Proto / X-Forwarded-Host / X-Forwarded-Port;HTTPS 终止在 Nginx 时,需按 Spring Boot 反向代理规范启用可信转发头,应用才能识别为 https 和外部域名。WebSocket 连接地址也由 GET /api/v1/site 的 websocketUrl 下发,前端在子路径或 HTTPS 部署下据此发现正确地址。
文档站缓存
文档站(public/docs)是 VitePress 静态产物,建议单独补一段按真实文件查找的规则,避免被全站缓存规则覆盖导致客户端 JS 失效(详见本文档「文档站点打包」一节的 location ^~ /docs/)。
与 Docker 部署的关系
项目内置镜像已自带等价的静态优先 Nginx。Docker 场景(容器内置 Nginx、或服务器 Nginx 前置一体化容器、Compose 拆分)见 docker.md。两者静态优先规则一致;区别只在后端 upstream 指向容器端口还是本机 java -jar 进程。
私有文件与网关
文件私有下载走签名 URL 入口(/api/v1/file/records/download,见 storage.md),已包含在 /api/** 代理规则内。公开文件优先用对象存储或直接由 Nginx 静态承载,应用只保存 disk + path,不固化域名。