fix: add docker-compose config and fix nginx proxy for /prod-api/ endpoint

This commit is contained in:
rjb
2026-05-11 00:13:26 +08:00
parent a02773e383
commit 1047599bde
6 changed files with 377 additions and 226 deletions

16
.env.docker.example Normal file
View File

@@ -0,0 +1,16 @@
# 复制为 .env 后填写(勿将 .env 提交到 Git)
# cp .env.docker.example .env
# 对外发布后端端口(宿主机:容器均为 8039)
BACKEND_PUBLISH=8039
# MySQL(示例为腾讯云 CynosDB,请按实际修改)
MYSQL_HOST=gz-cynosdbmysql-grp-d26pzce5.sql.tencentcdb.com
MYSQL_PORT=24936
MYSQL_DATABASE=rlz
MYSQL_USER=root
MYSQL_PASSWORD=your_password_here
MYSQL_USE_SSL=true
# Redis 由 docker-compose 内 rlz-redis 提供,一般无需改
# REDIS_HOST=redis

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.env

76
Docker部署说明.md Normal file
View File

@@ -0,0 +1,76 @@
# 瑞来兹后端 Docker 部署说明
## 架构
| 组件 | 说明 |
|------|------|
| **rlz-backend** | Spring Boot 若依后台,端口 **8039**,Profile **`docker`** |
| **rlz-redis** | 容器内 Redis 7,供验证码、会话、缓存 |
| **MySQL** | **默认使用外部库**(如腾讯云 CynosDB),通过环境变量连接 |
上传与日志使用命名卷持久化:`rlz-upload`、`rlz-logs`。
## 前置条件
- 已安装 Docker 与 Docker Compose V2(`docker compose`)
- 云数据库 **白名单** 放行 **服务器公网出口 IP**
- 安全组放行 **8039**(若需公网访问 API)
## 快速启动
```bash
cd /path/to/saars/rlz
cp .env.docker.example .env
# 编辑 .env:至少填写 MYSQL_PASSWORD,确认 MYSQL_HOST / MYSQL_PORT
docker compose build --no-cache
docker compose up -d
docker compose logs -f backend
```
## 验证
```bash
curl -sS http://127.0.0.1:8039/captchaImage | head -c 200
# 应返回 JSON,含 code、img(Base64)等
```
## 与前端联调
前端 `vue.config.js` 将 `/dev-api` 代理到 `http://127.0.0.1:8039`。
若前端在 **同一宿主机** 的 Docker 里使用 **host 网络**,可直接用 `8039`;否则将代理 `target` 改为 `http://宿主机IP:8039` 或 `http://rlz-backend:8039`(需把前端也纳入同一 compose 网络)。
## 常用命令
```bash
docker compose ps
docker compose restart backend
docker compose down
# 保留卷: docker compose down
# 删卷(清空 Redis AOF、上传文件): docker compose down -v
```
## 调整内存
编辑 `rlz/Dockerfile` 中默认 `JAVA_OPTS`,或 compose 里增加:
```yaml
environment:
JAVA_OPTS: "-Xms512m -Xmx1536m -XX:+UseContainerSupport"
```
## 文件说明
| 路径 | 作用 |
|------|------|
| `rlz/Dockerfile` | Maven 构建 + JRE 运行镜像 |
| `rlz/.dockerignore` | 减小构建上下文 |
| `rlz/ruoyi-admin/src/main/resources/application-docker.yml` | Docker 专用配置(环境变量) |
| `docker-compose.yml` | Redis + 后端编排 |
| `.env.docker.example` | 环境变量模板 |
---
**注意**:`application.yml` 中默认 `spring.profiles.active` 为 `dev`,容器内通过环境变量 **`SPRING_PROFILES_ACTIVE=docker`** 覆盖,无需改仓库里的 `application.yml`。

54
docker-compose.yml Normal file
View File

@@ -0,0 +1,54 @@
# 瑞来兹后端 Docker 编排(Redis 容器内 + MySQL 用外部云库或可选本地库)
# 使用:
# cp .env.docker.example .env
# 编辑 .env 填写 MYSQL_* 与 MYSQL_PASSWORD
# docker compose up -d --build
#
# 健康检查: curl -sS http://127.0.0.1:8039/captchaImage | head -c 120
services:
redis:
image: redis:7-alpine
container_name: rlz-redis
restart: unless-stopped
command: redis-server --appendonly yes
volumes:
- rlz-redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
backend:
build:
context: ./rlz
dockerfile: Dockerfile
image: rlz-backend:latest
container_name: rlz-backend
restart: unless-stopped
ports:
- "${BACKEND_PUBLISH:-8039}:8039"
environment:
SPRING_PROFILES_ACTIVE: docker
SERVER_PORT: "8039"
REDIS_HOST: redis
REDIS_PORT: "6379"
MYSQL_HOST: ${MYSQL_HOST}
MYSQL_PORT: ${MYSQL_PORT:-3306}
MYSQL_DATABASE: ${MYSQL_DATABASE:-rlz}
MYSQL_USER: ${MYSQL_USER:-root}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
MYSQL_USE_SSL: ${MYSQL_USE_SSL:-true}
RUOYI_PROFILE: /data/ruoyi/upload
depends_on:
redis:
condition: service_healthy
volumes:
- rlz-upload:/data/ruoyi/upload
- rlz-logs:/data/ruoyi/logs
volumes:
rlz-redis-data:
rlz-upload:
rlz-logs:

View File

@@ -1,90 +1,92 @@
{ {
"name": "ruoyi", "name": "ruoyi",
"version": "3.8.3", "version": "3.8.3",
"description": "瑞来兹医助管理系统", "description": "瑞来兹医助管理系统",
"author": "瑞来兹医助", "author": "瑞来兹医助",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"dev": "vue-cli-service serve", "dev": "vue-cli-service serve",
"build:prod": "vue-cli-service build", "build:prod": "vue-cli-service build",
"build:stage": "vue-cli-service build --mode staging", "build:stage": "vue-cli-service build --mode staging",
"preview": "node build/index.js --preview", "preview": "node build/index.js --preview",
"lint": "eslint --ext .js,.vue src" "lint": "eslint --ext .js,.vue src"
}, },
"husky": { "husky": {
"hooks": { "hooks": {
"pre-commit": "lint-staged" "pre-commit": "lint-staged"
} }
}, },
"lint-staged": { "lint-staged": {
"src/**/*.{js,vue}": [ "src/**/*.{js,vue}": [
"eslint --fix", "eslint --fix",
"git add" "git add"
] ]
}, },
"keywords": [ "keywords": [
"vue", "vue",
"admin", "admin",
"dashboard", "dashboard",
"element-ui", "element-ui",
"boilerplate", "boilerplate",
"admin-template", "admin-template",
"management-system" "management-system"
], ],
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://gitee.com/y_project/RuoYi-Vue.git" "url": "https://gitee.com/y_project/RuoYi-Vue.git"
}, },
"dependencies": { "dependencies": {
"@riophae/vue-treeselect": "0.4.0", "@riophae/vue-treeselect": "0.4.0",
"axios": "0.24.0", "axios": "0.24.0",
"clipboard": "2.0.8", "babel-runtime": "^6.26.0",
"core-js": "3.19.1", "clipboard": "2.0.8",
"echarts": "4.9.0", "core-js": "3.19.1",
"element-ui": "2.15.8", "echarts": "4.9.0",
"file-saver": "2.0.5", "element-ui": "2.15.8",
"fuse.js": "6.4.3", "file-saver": "2.0.5",
"highlight.js": "9.18.5", "fuse.js": "6.4.3",
"js-beautify": "1.13.0", "highlight.js": "9.18.5",
"js-cookie": "3.0.1", "html-webpack-plugin": "^3.2.0",
"jsencrypt": "3.0.0-rc.1", "js-beautify": "1.13.0",
"nprogress": "0.2.0", "js-cookie": "3.0.1",
"quill": "1.3.7", "jsencrypt": "3.0.0-rc.1",
"screenfull": "5.0.2", "nprogress": "0.2.0",
"sortablejs": "1.10.2", "quill": "1.3.7",
"vue": "2.6.12", "screenfull": "5.0.2",
"vue-count-to": "1.0.13", "sortablejs": "1.10.2",
"vue-cropper": "0.5.5", "vue": "2.6.12",
"vue-meta": "2.4.0", "vue-count-to": "1.0.13",
"vue-router": "3.4.9", "vue-cropper": "0.5.5",
"vuedraggable": "2.24.3", "vue-meta": "2.4.0",
"vuex": "3.6.0" "vue-router": "3.4.9",
}, "vuedraggable": "2.24.3",
"devDependencies": { "vuex": "3.6.0"
"@vue/cli-plugin-babel": "4.4.6", },
"@vue/cli-plugin-eslint": "4.4.6", "devDependencies": {
"@vue/cli-service": "4.4.6", "@vue/cli-plugin-babel": "4.4.6",
"babel-eslint": "10.1.0", "@vue/cli-plugin-eslint": "4.4.6",
"babel-plugin-dynamic-import-node": "2.3.3", "@vue/cli-service": "4.4.6",
"chalk": "4.1.0", "babel-eslint": "10.1.0",
"compression-webpack-plugin": "5.0.2", "babel-plugin-dynamic-import-node": "2.3.3",
"connect": "3.6.6", "chalk": "4.1.0",
"eslint": "7.15.0", "compression-webpack-plugin": "5.0.2",
"eslint-plugin-vue": "7.2.0", "connect": "3.6.6",
"lint-staged": "10.5.3", "eslint": "7.15.0",
"runjs": "4.4.2", "eslint-plugin-vue": "7.2.0",
"sass": "1.32.13", "lint-staged": "10.5.3",
"sass-loader": "10.1.1", "runjs": "4.4.2",
"script-ext-html-webpack-plugin": "2.1.5", "sass": "1.32.13",
"svg-sprite-loader": "5.1.1", "sass-loader": "10.1.1",
"vue-template-compiler": "2.6.12" "script-ext-html-webpack-plugin": "2.1.5",
}, "svg-sprite-loader": "5.1.1",
"engines": { "vue-template-compiler": "2.6.12"
"node": ">=8.9", },
"npm": ">= 3.0.0" "engines": {
}, "node": ">=8.9",
"browserslist": [ "npm": ">= 3.0.0"
"> 1%", },
"last 2 versions" "browserslist": [
] "> 1%",
} "last 2 versions"
]
}

View File

@@ -1,136 +1,138 @@
'use strict' 'use strict'
const path = require('path') const path = require('path')
function resolve(dir) { function resolve(dir) {
return path.join(__dirname, dir) return path.join(__dirname, dir)
} }
const CompressionPlugin = require('compression-webpack-plugin') const CompressionPlugin = require('compression-webpack-plugin')
const name = process.env.VUE_APP_TITLE || '瑞来兹医助管理系统' // 网页标题 const name = process.env.VUE_APP_TITLE || '瑞来兹医助管理系统' // 网页标题
const port = process.env.port || process.env.npm_config_port || 8050 // 端口 const port = process.env.port || process.env.npm_config_port || 8050 // 端口
// vue.config.js 配置说明 // vue.config.js 配置说明
//官方vue.config.js 参考文档 https://cli.vuejs.org/zh/config/#css-loaderoptions //官方vue.config.js 参考文档 https://cli.vuejs.org/zh/config/#css-loaderoptions
// 这里只列一部分,具体配置参考文档 // 这里只列一部分,具体配置参考文档
module.exports = { module.exports = {
// 部署生产环境和开发环境下的URL。 // 低内存环境(如 Docker / 小机)避免 Webpack 多进程导致 OOM
// 默认情况下,Vue CLI 会假设你的应用是被部署在一个域名的根路径上 parallel: false,
// 例如 https://www.ruoyi.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.ruoyi.vip/admin/,则设置 baseUrl 为 /admin/。 // 部署生产环境和开发环境下的URL。
publicPath: process.env.NODE_ENV === "production" ? "/" : "/", // 默认情况下,Vue CLI 会假设你的应用是被部署在一个域名的根路径上
// 在npm run build 或 yarn build 时 ,生成文件的目录名称(要和baseUrl的生产环境路径一致)(默认dist) // 例如 https://www.ruoyi.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.ruoyi.vip/admin/,则设置 baseUrl 为 /admin/。
outputDir: 'dist', publicPath: process.env.NODE_ENV === "production" ? "/" : "/",
// 用于放置生成的静态资源 (js、css、img、fonts) 的;(项目打包之后,静态资源会放在这个文件夹下) // 在npm run build 或 yarn build 时 ,生成文件的目录名称(要和baseUrl的生产环境路径一致)(默认dist)
assetsDir: 'static', outputDir: 'dist',
// 是否开启eslint保存检测,有效值:ture | false | 'error' // 用于放置生成的静态资源 (js、css、img、fonts) 的;(项目打包之后,静态资源会放在这个文件夹下)
lintOnSave: process.env.NODE_ENV === 'development', assetsDir: 'static',
// 如果你不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建。 // 是否开启eslint保存检测;CI=true(如容器内 npm run dev)时关闭以省内存
productionSourceMap: false, lintOnSave: process.env.NODE_ENV === 'development' && !process.env.CI,
// webpack-dev-server 相关配置 // 如果你不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建。
devServer: { productionSourceMap: false,
host: '0.0.0.0', // webpack-dev-server 相关配置
port: port, devServer: {
open: true, host: '0.0.0.0',
proxy: { port: port,
// detail: https://cli.vuejs.org/config/#devserver-proxy open: true,
[process.env.VUE_APP_BASE_API]: { proxy: {
target: `http://127.0.0.1:8039`, // detail: https://cli.vuejs.org/config/#devserver-proxy
changeOrigin: true, [process.env.VUE_APP_BASE_API]: {
pathRewrite: { target: `http://127.0.0.1:8039`,
['^' + process.env.VUE_APP_BASE_API]: '' changeOrigin: true,
} pathRewrite: {
} ['^' + process.env.VUE_APP_BASE_API]: ''
}, }
disableHostCheck: true }
}, },
css: { disableHostCheck: true
loaderOptions: { },
sass: { css: {
sassOptions: { outputStyle: "expanded" } loaderOptions: {
} sass: {
} sassOptions: { outputStyle: "expanded" }
}, }
configureWebpack: { }
name: name, },
resolve: { configureWebpack: {
alias: { name: name,
'@': resolve('src') resolve: {
} alias: {
}, '@': resolve('src')
plugins: [ }
// http://doc.ruoyi.vip/ruoyi-vue/other/faq.html#使用gzip解压缩静态文件 },
new CompressionPlugin({ plugins: [
cache: false, // 不启用文件缓存 // http://doc.ruoyi.vip/ruoyi-vue/other/faq.html#使用gzip解压缩静态文件
test: /\.(js|css|html)?$/i, // 压缩文件格式 new CompressionPlugin({
filename: '[path].gz[query]', // 压缩后的文件名 cache: false, // 不启用文件缓存
algorithm: 'gzip', // 使用gzip压缩 test: /\.(js|css|html)?$/i, // 压缩文件格式
minRatio: 0.8 // 压缩率小于1才会压缩 filename: '[path].gz[query]', // 压缩后的文件名
}) algorithm: 'gzip', // 使用gzip压缩
], minRatio: 0.8 // 压缩率小于1才会压缩
}, })
chainWebpack(config) { ],
config.plugins.delete('preload') // TODO: need test },
config.plugins.delete('prefetch') // TODO: need test chainWebpack(config) {
config.plugins.delete('preload') // TODO: need test
// set svg-sprite-loader config.plugins.delete('prefetch') // TODO: need test
config.module
.rule('svg') // set svg-sprite-loader
.exclude.add(resolve('src/assets/icons')) config.module
.end() .rule('svg')
config.module .exclude.add(resolve('src/assets/icons'))
.rule('icons') .end()
.test(/\.svg$/) config.module
.include.add(resolve('src/assets/icons')) .rule('icons')
.end() .test(/\.svg$/)
.use('svg-sprite-loader') .include.add(resolve('src/assets/icons'))
.loader('svg-sprite-loader') .end()
.options({ .use('svg-sprite-loader')
symbolId: 'icon-[name]' .loader('svg-sprite-loader')
}) .options({
.end() symbolId: 'icon-[name]'
})
config .end()
.when(process.env.NODE_ENV !== 'development',
config => { config
config .when(process.env.NODE_ENV !== 'development',
.plugin('ScriptExtHtmlWebpackPlugin') config => {
.after('html') config
.use('script-ext-html-webpack-plugin', [{ .plugin('ScriptExtHtmlWebpackPlugin')
// `runtime` must same as runtimeChunk name. default is `runtime` .after('html')
inline: /runtime\..*\.js$/ .use('script-ext-html-webpack-plugin', [{
}]) // `runtime` must same as runtimeChunk name. default is `runtime`
.end() inline: /runtime\..*\.js$/
config }])
.optimization.splitChunks({ .end()
chunks: 'all', config
cacheGroups: { .optimization.splitChunks({
libs: { chunks: 'all',
name: 'chunk-libs', cacheGroups: {
test: /[\\/]node_modules[\\/]/, libs: {
priority: 10, name: 'chunk-libs',
chunks: 'initial' // only package third parties that are initially dependent test: /[\\/]node_modules[\\/]/,
}, priority: 10,
elementUI: { chunks: 'initial' // only package third parties that are initially dependent
name: 'chunk-elementUI', // split elementUI into a single package },
priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app elementUI: {
test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm name: 'chunk-elementUI', // split elementUI into a single package
}, priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
commons: { test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
name: 'chunk-commons', },
test: resolve('src/components'), // can customize your rules commons: {
minChunks: 3, // minimum common number name: 'chunk-commons',
priority: 5, test: resolve('src/components'), // can customize your rules
reuseExistingChunk: true minChunks: 3, // minimum common number
} priority: 5,
} reuseExistingChunk: true
}) }
config.optimization.runtimeChunk('single'), }
{ })
from: path.resolve(__dirname, './public/robots.txt'), //防爬虫文件 config.optimization.runtimeChunk('single'),
to: './' //到根目录下 {
} from: path.resolve(__dirname, './public/robots.txt'), //防爬虫文件
} to: './' //到根目录下
) }
} }
} )
}
}