生产部署样板
本文档提供一套可直接复制的生产部署样板。更细的 Docker 说明看 Docker,运行模式和 Nginx 原理看 部署。
推荐拓扑
生产环境优先使用静态优先部署:
Nginx
├── public/ 静态资源:/admin/ /app/ /pc/ /install/ /docs/
├── /api/** -> Java 应用(含 /api/ws WebSocket)
├── /actuator/** -> Java 应用
└── /swagger-ui** -> Java 应用数据库和 Redis 推荐独立部署;快速交付或小规模内网试用可以使用一体化镜像。
目录规划
服务器建议目录:
/home/app/
├── public/ # 对外公开静态资源
├── storage/ # 私有持久化文件、日志、安装锁、缓存
├── docker/
│ └── data/ # Docker Compose 依赖服务数据
└── releases/ # 可选:版本包和回滚备份必须持久化:
| 目录 | 原因 |
|---|---|
/home/app/storage | 私有文件、安装状态、日志、缓存、H2 lite 数据库 |
/home/app/public | 公开上传文件和前端静态产物 |
/home/app/docker/data/mysql | Compose MySQL 数据 |
/home/app/docker/data/redis | Compose Redis 数据 |
方案一:一体化镜像
适合快速交付、演示、低运维成本部署。
mkdir -p /home/app/storage /home/app/public
docker rm -f springopen 2>/dev/null || true
docker run -d \
--name springopen \
--restart unless-stopped \
-p 127.0.0.1:8080:80 \
-e TZ=Asia/Shanghai \
-e SPRING_OPEN_RUNTIME_MODE=all-in-one \
-e DB_DATABASE=spring_open \
-e DB_USERNAME=spring_open \
-e DB_PASSWORD='replace-with-strong-database-password' \
-e DB_ROOT_PASSWORD='replace-with-strong-root-password' \
-e REDIS_PASSWORD='replace-with-strong-redis-password' \
-e APP_SECRET='replace-with-at-least-32-random-characters' \
-v /home/app/storage:/home/app/storage \
-v /home/app/public:/home/app/public \
springopen/springopen:1.0.0一体化镜像内部启动 MySQL、Redis、Java 应用和 Nginx。宿主机只暴露 127.0.0.1:8080,再由外层 Nginx 绑定域名。
方案二:Compose 拆分部署
适合开发服务器、测试环境或希望拆分 MySQL / Redis / Nginx 的场景。
cd /home/app/source/docker
APP_SECRET='replace-with-at-least-32-random-characters' \
DB_PASSWORD='replace-with-database-password' \
DB_ROOT_PASSWORD='replace-with-root-password' \
docker compose -f docker-compose.yml \
--profile app \
--profile nginx \
up -d --build --remove-orphans如果使用 PostgreSQL:
APP_SECRET='replace-with-at-least-32-random-characters' \
DB_DRIVER_CLASS_NAME=org.postgresql.Driver \
DB_PASSWORD='replace-with-database-password' \
docker compose -f docker-compose.yml \
--profile postgres \
--profile app \
--profile nginx \
up -d --build --remove-orphansCompose 默认:
- MySQL
8.4 - PostgreSQL
18 - Redis
8 - Nginx
1.27 - 应用容器使用
SPRING_OPEN_RUNTIME_MODE=app
外层 Nginx 样板
如果容器或 Java 应用监听本机 127.0.0.1:8080,站点 Nginx 可以这样写:
server {
listen 80;
server_name example.com;
client_max_body_size 50m;
location = /api/ws {
proxy_pass http://127.0.0.1:8080;
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/ {
set $spring_open_connection "";
if ($http_upgrade != "") {
set $spring_open_connection "upgrade";
}
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
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;
}
location ~ ^/(?:actuator|swagger-ui(?:\.html)?|v3/api-docs)(?:/|$) {
proxy_pass http://127.0.0.1:8080;
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;
}
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
}
}如果你选择让外层 Nginx 直接读取 /home/app/public,使用 部署 中的「Nginx 静态优先部署」完整配置。
环境变量样板
生产最小配置:
APP_NAME=spring-open
APP_SECRET=replace-with-at-least-32-random-characters
APP_STORAGE_DIR=/home/app/storage
SPRING_PROFILES_ACTIVE=prod
# 应用统一时区:多区域集群所有节点必须相同,贯通 JVM / Jackson / JDBC serverTimezone(详见 config.md「应用统一时区」)
# 裸 JVM(非容器)部署建议同时加启动参数 -Duser.timezone=Asia/Shanghai,让启动最早几行日志也使用应用时区
# SPRING_OPEN_APP_TIME_ZONE=Asia/Shanghai
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=spring_open
DB_USERNAME=spring_open
DB_PASSWORD=replace-with-strong-database-password
# DB_TYPE=com.zaxxer.hikari.HikariDataSource
# DB_DRIVER_CLASS_NAME=com.mysql.cj.jdbc.Driver
DB_ROOT_PASSWORD=replace-with-strong-root-password
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=replace-with-strong-redis-password
REDIS_DATABASE=0
# LOG_FILE_NAME=/home/app/storage/runtime/logs/app.log
SPRING_OPEN_INSTALL_ENABLED=true
SPRING_OPEN_INSTALL_TOKEN=replace-with-high-entropy-install-tokenAPP_SECRET 是应用共享密钥,默认会被 JWT、AI / Open Platform 加密 key 等能力作为统一兜底;如果你已经为某个能力配置专用密钥,专用密钥优先。prod profile 不做启动期强密码门禁,生产安全由部署配置、网关和密钥管理保证;安装入口默认开启,需要限制访问时配置高强度 SPRING_OPEN_INSTALL_TOKEN。
以上是部署样板,不是生产安全默认值。正式上线前必须按实际环境替换或补齐:
- 使用强随机
APP_SECRET;如需独立 JWT 密钥,显式配置SPRING_OPEN_AUTH_JWT_SECRET_KEY,多实例必须保持一致。 - 替换
DB_PASSWORD、DB_ROOT_PASSWORD、REDIS_PASSWORD,不要沿用开发环境里的spring_open、root或空密码。 - 安装入口生产默认开启,必须配置高强度
SPRING_OPEN_INSTALL_TOKEN;安装完成后保留安装锁,并在网关层限制安装入口访问范围。 - 关闭或隔离开发型 Provider:支付
mock、AIsimulated/mock、本地对象存储和本地 Web3 / 回调样例只适合开发、演示或 smoke。生产需要配置真实 Provider、真实回调地址和服务端密钥。 application-prod.yml默认只暴露/actuator/health且不返回详细信息;如果需要开放info、metrics、prometheus或 Swagger / OpenAPI,必须在网关层配置鉴权、IP 白名单或内网访问策略。- 将真实支付、短信、邮件、对象存储、AI、Web3 RPC、Open Platform AppSecret 等敏感值放在环境变量、密钥管理或配置中心中,不写入 Git。
Prometheus 指标抓取:
MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE=health,info,metrics,prometheus
MANAGEMENT_PROMETHEUS_METRICS_EXPORT_ENABLED=true告警规则样板位于 docker/prometheus/alerts.yml,覆盖实例宕机、5xx 增长、队列堆积、调度失败、支付回调失败和磁盘剩余空间低。生产接入前可用 promtool check rules docker/prometheus/alerts.yml 做语法检查;没有安装 promtool 时也可以先用 ./scripts/docker smoke 做仓库内样板存在性检查。
上线 smoke
部署完成后至少检查:
curl -i http://127.0.0.1:8080/
curl -s http://127.0.0.1:8080/actuator/health
# 仅在内网或受保护的运维网络启用 prometheus 暴露后检查
curl -s http://127.0.0.1:8080/actuator/prometheus | head
curl -i http://127.0.0.1:8080/admin/
curl -i http://127.0.0.1:8080/app/
curl -i http://127.0.0.1:8080/pc/
curl -i http://127.0.0.1:8080/docs/
curl -i http://127.0.0.1:8080/swagger-ui/index.htmlWebSocket 必须用真实 Upgrade 检查,普通 curl /api/ws 返回 404 不代表失败。前端默认连接 /api/ws?...,Nginx 样板里的 location = /api/ws 必须保留在 location /api/ 之前。
切换应用时区(存量数据换算)
业务时间以 datetime 墙钟值存储,语义即当时的应用时区(SPRING_OPEN_APP_TIME_ZONE)。生产运行后切换应用时区等于重新解释全部存量时间,属重大运维操作,必须整体换算,不允许"只改环境变量"半切:
冻结写入:全部应用节点下线(或数据库切只读),做全量备份。
生成换算 SQL:用 information_schema 枚举所有
datetime列,按固定偏移生成CONVERT_TZ批量更新(固定偏移不依赖 MySQL 时区表;涉及夏令时区域改用区域名并先mysql_tzinfo_to_sql装载时区表):sql-- 示例:Asia/Shanghai(+08:00) → UTC(+00:00) SELECT CONCAT('UPDATE `', TABLE_NAME, '` SET `', COLUMN_NAME, '` = CONVERT_TZ(`', COLUMN_NAME, '`, ''+08:00'', ''+00:00'') WHERE `', COLUMN_NAME, '` IS NOT NULL;') AS stmt FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND DATA_TYPE = 'datetime' ORDER BY TABLE_NAME, COLUMN_NAME;人工甄别例外列:纯"墙钟语义"字段(如每日任务触发点、营业时间段配置)表达的是本地钟点而非时刻,不参与换算;逐列确认后从生成的语句中剔除。
执行并抽验:在副本库演练通过后于生产执行;抽取资金流水、Key 过期、定价
effective_at等关键表核对换算前后对应同一时刻(可用Time.shift(value, from, to)在应用侧交叉验证)。切换配置:所有节点与数据库容器统一更新
APP_TZ/SPRING_OPEN_APP_TIME_ZONE后再恢复写入;多区域集群必须一次性全量切换。
回滚
回滚前保留日志:
docker logs springopen > /home/app/releases/springopen-failed.log 2>&1回滚顺序:
- 停止新版本容器。
- 切回上一版本镜像或上一份 jar。
- 恢复数据库备份;如果只执行兼容新增迁移,可评估不恢复。
- 恢复旧配置。
- 重新执行上线 smoke。
更多升级和回滚细节见 升级指南。