跳到内容

Deployment

受众:开发者 摘要:部署模式(dev / test / prod / lite)/ 目录语义 / 生产部署建议 / Nginx 静态优先完整配置(根入口 / 五端入口 / API 代理 / WebSocket 101)。

本文档说明运行模式、目录语义和生产部署建议。

如果你需要一份可直接复制到服务器执行的完整流程,先看 生产部署样板

Profile

常用 profile:

Profile用途
dev本地开发,默认激活
test测试环境
prod生产环境
lite历史兼容 / 隔离诊断的单进程模式,不推荐普通开发或生产使用

设置方式:

bash
SPRING_PROFILES_ACTIVE=prod java -jar spring-open-application.jar

开发时通常直接使用 Maven:

bash
./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 安装应用依赖,再运行应用模块。不要把 -amspring-boot:run 组合使用,避免 Maven 把运行目标施加到 reactor 里的 POM 模块。

如果需要把运行产物放到其他目录,可以使用环境变量覆盖:

bash
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 运行目录:

text
${APP_STORAGE_DIR:${user.dir}/storage}/runtime/tomcat

默认日志目录:

text
${APP_STORAGE_DIR:${user.dir}/storage}/runtime/logs

public 目录

public/ 会挂载到站点根路由:

text
public/test.txt -> http://localhost:8000/test.txt

本地公开存储 disk 默认也写入 public/。生产环境建议用 Nginx 或对象存储承载公开文件,应用只保存相对路径或对象 key,避免域名变更导致历史内容失效。

storage 目录

storage/ 用于私有持久化文件、密钥、证书、H2 文件库、日志和运行产物。

生产环境如需单独挂载私有持久化目录,可以使用:

text
/home/app/storage

并通过可选环境变量 APP_STORAGE_DIR 指定:

bash
APP_STORAGE_DIR=/home/app/storage

公开静态目录默认使用 ${user.dir}/public;需要独立挂载时,可通过可选环境变量 APP_PUBLIC_DIR 指定:

bash
APP_PUBLIC_DIR=/home/app/public

数据库迁移

项目使用 Liquibase。

默认 changelog:

text
classpath:/db/changelog/db.changelog-master.xml

默认表名:

text
database_changelog
database_changelog_lock

生产环境首次启动前应确认数据库账号有建表和改表权限。上线后不要手动修改已经执行过的 changelog。

文档站点打包

正式文档 Markdown 源文件放在根目录 docs/

前端构建工程放在 views/docs/,避免根目录被 package.jsonnode_modules 等 Node 工程文件污染。后续后台管理 Vue3、H5、PC、uniapp 等前端工程也统一放入 views/,再由对应 Maven 模块决定是否构建和复制产物。

服务器构建文档站点:

bash
./mvnw -pl spring-open-views/spring-open-view-docs package -DskipTests -DskipITs -Dbuild.skip.views=false

也可以直接执行:

bash
./mvnw package -DskipTests

spring-open-view-docsspring-open-views 下的 POM 构建模块,默认 build.skip.views=false,因此根项目 package 会自动执行文档站点构建。使用 IDEA 时,直接在 Maven 面板双击根项目 spring-open Lifecycle 里的 package 即可。

应用模块不依赖 docs 模块,避免 spring-open-application 被文档构建逻辑污染。

如果当前不想构建文档前端资源,可以通过 Maven property 显式跳过:

bash
./mvnw package -Dbuild.skip.views=true -DskipTests

docs 模块会自动执行:

  1. 下载固定版本 Node.js 和 pnpm。
  2. views/docs/ 执行 pnpm install --frozen-lockfile
  3. 执行 pnpm docs:build
  4. 将 VitePress 产物导出到 public/docs

build.skip.views 默认是 false。命令行 -Dbuild.skip.views=true 可以跳过前端构建。

应用启动后,public/ 会挂载到站点根路由,因此默认可以访问:

text
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/,可以把产物复制到对应公开目录,不需要重新构建。推荐为文档目录单独补一段静态查找规则:

nginx
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:

bash
cd views/docs
pnpm install
pnpm docs:dev

构建产物不提交 Git,只在 Maven 构建时进入 public/docs

生产配置

生产环境建议:

  • 使用外部 MySQL 或 PostgreSQL。
  • 使用外部 Redis。
  • 配置强随机 APP_SECRET,或为 JWT / 支付 / Web3 等高敏能力单独配置专用密钥。
  • 支付、短信、邮件、存储、Webhook 等密钥放入环境变量、部署平台密钥或数据库配置。
  • 私钥、证书和敏感文件放入 storage/keys 或独立安全挂载目录。
  • 公开文件优先使用对象存储或 Nginx。

示例:

bash
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.jar

Lite 模式

lite 模式只保留给历史兼容、隔离诊断或极少数单进程验证场景;普通开发不推荐使用:

  • H2 文件数据库。
  • file cache。
  • memory queue。
  • memory scheduler。
  • Sa-Token memory store。
  • 不依赖外部 Redis。

启动示例:

bash
SPRING_PROFILES_ACTIVE=lite java -jar spring-open-application.jar

lite 不建议用于普通开发、生产或多实例部署。

默认 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 类需要代理到后端:

text
/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_STATUSSPRING_OPEN_SITE_ACCESS_MESSAGESPRING_OPEN_SITE_ACCESS_RESUME_ATSPRING_OPEN_SITE_ACCESS_ALLOW_ADMINSPRING_OPEN_SITE_ACCESS_ALLOW_API_DOCS。部署层不要拦截 /api/v1/site,也不要阻断 App / PC 静态入口;前端需要先加载静态壳并读取站点信息接口,才能向用户展示关闭 / 维护说明和预计恢复时间。

完整配置

以下是与项目内置 Nginx(docker/nginx/default.conf.template)等价的裸机配置,后端按默认端口 8000public/ 作为静态根目录:

nginx
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-healthNginx 本地返回 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 后缀:

nginx
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/sitewebsocketUrl 下发,前端在子路径或 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,不固化域名。

Released under the Apache License 2.0.