跳到内容

生产部署样板

本文档提供一套可直接复制的生产部署样板。更细的 Docker 说明看 Docker,运行模式和 Nginx 原理看 部署

推荐拓扑

生产环境优先使用静态优先部署:

text
Nginx
  ├── public/ 静态资源:/admin/ /app/ /pc/ /install/ /docs/
  ├── /api/**       -> Java 应用(含 /api/ws WebSocket)
  ├── /actuator/**  -> Java 应用
  └── /swagger-ui** -> Java 应用

数据库和 Redis 推荐独立部署;快速交付或小规模内网试用可以使用一体化镜像。

目录规划

服务器建议目录:

text
/home/app/
├── public/              # 对外公开静态资源
├── storage/             # 私有持久化文件、日志、安装锁、缓存
├── docker/
│   └── data/            # Docker Compose 依赖服务数据
└── releases/            # 可选:版本包和回滚备份

必须持久化:

目录原因
/home/app/storage私有文件、安装状态、日志、缓存、H2 lite 数据库
/home/app/public公开上传文件和前端静态产物
/home/app/docker/data/mysqlCompose MySQL 数据
/home/app/docker/data/redisCompose Redis 数据

方案一:一体化镜像

适合快速交付、演示、低运维成本部署。

bash
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 的场景。

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

bash
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-orphans

Compose 默认:

  • MySQL 8.4
  • PostgreSQL 18
  • Redis 8
  • Nginx 1.27
  • 应用容器使用 SPRING_OPEN_RUNTIME_MODE=app

外层 Nginx 样板

如果容器或 Java 应用监听本机 127.0.0.1:8080,站点 Nginx 可以这样写:

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 静态优先部署」完整配置。

环境变量样板

生产最小配置:

bash
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-token

APP_SECRET 是应用共享密钥,默认会被 JWT、AI / Open Platform 加密 key 等能力作为统一兜底;如果你已经为某个能力配置专用密钥,专用密钥优先。prod profile 不做启动期强密码门禁,生产安全由部署配置、网关和密钥管理保证;安装入口默认开启,需要限制访问时配置高强度 SPRING_OPEN_INSTALL_TOKEN

以上是部署样板,不是生产安全默认值。正式上线前必须按实际环境替换或补齐:

  • 使用强随机 APP_SECRET;如需独立 JWT 密钥,显式配置 SPRING_OPEN_AUTH_JWT_SECRET_KEY,多实例必须保持一致。
  • 替换 DB_PASSWORDDB_ROOT_PASSWORDREDIS_PASSWORD,不要沿用开发环境里的 spring_openroot 或空密码。
  • 安装入口生产默认开启,必须配置高强度 SPRING_OPEN_INSTALL_TOKEN;安装完成后保留安装锁,并在网关层限制安装入口访问范围。
  • 关闭或隔离开发型 Provider:支付 mock、AI simulated / mock、本地对象存储和本地 Web3 / 回调样例只适合开发、演示或 smoke。生产需要配置真实 Provider、真实回调地址和服务端密钥。
  • application-prod.yml 默认只暴露 /actuator/health 且不返回详细信息;如果需要开放 infometricsprometheus 或 Swagger / OpenAPI,必须在网关层配置鉴权、IP 白名单或内网访问策略。
  • 将真实支付、短信、邮件、对象存储、AI、Web3 RPC、Open Platform AppSecret 等敏感值放在环境变量、密钥管理或配置中心中,不写入 Git。

Prometheus 指标抓取:

bash
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

部署完成后至少检查:

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

WebSocket 必须用真实 Upgrade 检查,普通 curl /api/ws 返回 404 不代表失败。前端默认连接 /api/ws?...,Nginx 样板里的 location = /api/ws 必须保留在 location /api/ 之前。

切换应用时区(存量数据换算)

业务时间以 datetime 墙钟值存储,语义即当时的应用时区(SPRING_OPEN_APP_TIME_ZONE)。生产运行后切换应用时区等于重新解释全部存量时间,属重大运维操作,必须整体换算,不允许"只改环境变量"半切:

  1. 冻结写入:全部应用节点下线(或数据库切只读),做全量备份。

  2. 生成换算 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;
  3. 人工甄别例外列:纯"墙钟语义"字段(如每日任务触发点、营业时间段配置)表达的是本地钟点而非时刻,不参与换算;逐列确认后从生成的语句中剔除。

  4. 执行并抽验:在副本库演练通过后于生产执行;抽取资金流水、Key 过期、定价 effective_at 等关键表核对换算前后对应同一时刻(可用 Time.shift(value, from, to) 在应用侧交叉验证)。

  5. 切换配置:所有节点与数据库容器统一更新 APP_TZ / SPRING_OPEN_APP_TIME_ZONE 后再恢复写入;多区域集群必须一次性全量切换。

回滚

回滚前保留日志:

bash
docker logs springopen > /home/app/releases/springopen-failed.log 2>&1

回滚顺序:

  1. 停止新版本容器。
  2. 切回上一版本镜像或上一份 jar。
  3. 恢复数据库备份;如果只执行兼容新增迁移,可评估不恢复。
  4. 恢复旧配置。
  5. 重新执行上线 smoke。

更多升级和回滚细节见 升级指南

Released under the Apache License 2.0.