Kubernetes 部署
本小节,讲解如何将前端 + 后端项目,使用 Kubernetes(K8s),部署到一个已有的 K8s 集群中。
整体链路如下:
浏览器 ──NodePort 30080──> yudao-ui-admin(Nginx + 前端静态文件)
│ /admin-api/、/app-api/、/infra/ws
↓ Service:yudao-server:48080
yudao-server(Deployment)──> 外部 MySQL + 外部 Redis
注意:服务器的 IP 地址。
- K8s 节点 IP:192.168.0.213(用于 NodePort 访问)
- MySQL、Redis 所在服务器的内网 IP:192.168.0.100
下属所有涉及到 IP 的配置,需要替换成你自己的。
友情提示:
- 本文是入门级的最小部署示例:后端、前端各 1 个副本,MySQL、Redis 使用集群外已就绪的实例,不在 K8s 中部署有状态的中间件,也不涉及高可用。
- 本文的后端镜像、Spring 环境变量名,与仓库
script/docker-compose目录(《Docker Compose 部署》)保持一致,建议先用它在单机上跑通。不同的是,K8s 中的 MySQL、Redis 是外部实例,账号密码请按实际填写。
# 1. 环境准备
# 1.1 前置条件
| 项目 | 要求 |
|---|---|
| K8s 集群 | 已就绪,并且可以使用 kubectl 命令管理 |
| 镜像仓库 | Harbor、阿里云 ACR 等任一镜像仓库。构建机可以 docker push,K8s 节点可以拉取 |
| MySQL | 已创建 ruoyi-vue-pro 库,并完成 SQL 初始化(见「1.3 初始化数据库」);有一个拥有该库权限的账号(下文以 yudao 为例);K8s 的 Pod 可以访问它的 3306 端口 |
| Redis | K8s 的 Pod 可以访问它的 6379 端口;有密码时记下密码 |
| 构建机 | 安装了 JDK、Maven、Node、Docker,用于编译项目、构建并推送镜像 |
注意:MySQL、Redis 的地址,不能填 `127.0.0.1` 或 `localhost`!
Pod 中的 127.0.0.1 指向的是 Pod 自己,而不是 K8s 节点或你的电脑。需要填写 Pod 可以访问到的内网 IP 或域名。
# 1.2 确认后端分支与 JDK 版本
后端项目有 3 个分支,JDK 版本、运行镜像、Redis 配置前缀都不同,必须一一对应,不能混用:
| 后端分支 | JDK | Spring Boot | yudao-server/Dockerfile 的运行镜像 | Redis 环境变量 |
|---|---|---|---|---|
master | 8 | 2.7 | eclipse-temurin:8-jre | SPRING_REDIS_HOST、SPRING_REDIS_PORT、SPRING_REDIS_DATABASE |
master-jdk17 | 17 / 21 | 3.5 | eclipse-temurin:17-jre | SPRING_DATA_REDIS_HOST、SPRING_DATA_REDIS_PORT、SPRING_DATA_REDIS_DATABASE |
master-jdk25 | 25 | 4.x | eclipse-temurin:25-jre | 同 master-jdk17 |
下文以 master-jdk17 分支为完整示例。如果你使用 master 分支(JDK 8),需要把下文 ConfigMap、Secret 中的 SPRING_DATA_REDIS_*,改成 SPRING_REDIS_*。运行镜像直接使用对应分支自带的 yudao-server/Dockerfile 即可。
# 1.3 初始化数据库
在 MySQL 中,创建 ruoyi-vue-pro 库,并导入项目的 sql/mysql/ruoyi-vue-pro.sql、sql/mysql/quartz.sql 两个 SQL 文件。可以参考 《Docker 部署》 使用 Navicat 导入,也可以在后端项目根目录下,使用有建库权限的账号执行如下命令:
## ① 创建数据库
mysql -h 192.168.0.100 -P 3306 -uroot -p -e 'CREATE DATABASE IF NOT EXISTS `ruoyi-vue-pro` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'
## ② 导入项目的表结构与初始数据
mysql -h 192.168.0.100 -P 3306 -uroot -p ruoyi-vue-pro < sql/mysql/ruoyi-vue-pro.sql
## ③ 导入 Quartz 定时任务的表结构
mysql -h 192.168.0.100 -P 3306 -uroot -p ruoyi-vue-pro < sql/mysql/quartz.sql
## ④(可选)创建应用账号 yudao,只授权 ruoyi-vue-pro 库。已有账号时跳过这一步
mysql -h 192.168.0.100 -P 3306 -uroot -p --default-character-set=utf8mb4 <<'SQL'
CREATE USER 'yudao'@'%' IDENTIFIED BY '改成你的 MySQL 密码';
GRANT ALL PRIVILEGES ON `ruoyi-vue-pro`.* TO 'yudao'@'%';
SQL
- 第 ④ 步的密码,就是后文 Secret 中填写的 MySQL 密码。如果报
ERROR 1396错误,说明yudao账号已存在:请直接使用它的实际密码,不要重置。
疑问:为什么要导入 quartz.sql?
application-dev.yaml 开启了 Quartz 定时任务,并使用数据库存储。如果缺少 QRTZ_ 开头的表,后端会启动失败。
# 2. 构建并推送镜像
注意:镜像地址的替换!!!
下文的镜像地址,统一写作 registry.example.com/yudao/xxx:1.0.0,它是一个占位符,需要全部替换成你的镜像仓库地址。例如说:
- Harbor:
harbor.your-company.com/yudao - 阿里云 ACR:
registry.cn-hangzhou.aliyuncs.com/你的命名空间
每次发布,都使用新的版本号(例如说 1.0.1),不要使用 latest 标签,否则 K8s 不会感知到镜像变化,也不方便回滚。
先执行 docker login registry.example.com 命令,登录你的镜像仓库。
# 2.1 后端镜像
在后端项目的根目录下,执行如下命令:
## ① 编译后端,构建出 yudao-server/target/yudao-server.jar
mvn clean package -Dmaven.test.skip=true
## ② 构建镜像:构建上下文是 yudao-server 目录,使用该目录下项目自带的 Dockerfile
docker build --platform linux/amd64 -t registry.example.com/yudao/yudao-server:1.0.0 yudao-server
## ③ 推送镜像
docker push registry.example.com/yudao/yudao-server:1.0.0
- 项目自带的
yudao-server/Dockerfile中,执行的是COPY ./target/yudao-server.jar app.jar,所以构建上下文必须是yudao-server目录。这与script/docker-compose/docker-compose.yml中的build.context: ../../yudao-server是同一个 Dockerfile。
友情提示:`--platform linux/amd64` 参数
如果你在 Apple 芯片的 Mac 上构建镜像,默认构建出 arm64 架构的镜像,在 amd64 的 K8s 节点上会报 exec format error 错误。请根据 K8s 节点的 CPU 架构,设置该参数。
# 2.2 前端镜像
和 Docker 部署时把前端文件放到宿主机目录不同,K8s 中推荐把前端构建产物,直接打包进 Nginx 镜像。
# 第一步,构建前端
前端的打包方式,和 《Linux 部署》 的「3. 部署前端」小节一致。不同的是,前端的“后端地址”要填写 K8s 中 Nginx 的访问地址,即 http://192.168.0.213:30080:
| 前端项目 | 配置文件 | 修改的配置项 | 打包命令 => 产物目录 |
|---|---|---|---|
yudao-ui-admin-vue3 | .env.prod | VITE_BASE_URL='http://192.168.0.213:30080',VITE_API_URL=/admin-api 保持不变 | pnpm build:prod => dist-prod |
yudao-ui-admin-vben | apps/web-antd/.env.production | VITE_BASE_URL=http://192.168.0.213:30080,VITE_GLOB_API_URL=/admin-api | pnpm build:antd => apps/web-antd/dist |
- 前端的
VITE_APP_CAPTCHA_ENABLE,要与下文 ConfigMap 中的YUDAO_CAPTCHA_ENABLE一致,默认都是true。 yudao-ui-admin-vue2同理,把VUE_APP_BASE_API改成 Nginx 的访问地址即可。
# 第二步,构建镜像
① 新建一个 yudao-ui-admin-image 目录,将前端构建产物目录复制为它的 dist 子目录(dist 下直接是 index.html 等文件)。
② 在 yudao-ui-admin-image 目录下,新建 Dockerfile 文件,内容如下:
FROM nginx:1.28-alpine
## 将前端构建产物,复制到 Nginx 的默认站点目录
COPY dist/ /usr/share/nginx/html/
③ 执行如下命令,构建并推送镜像:
cd yudao-ui-admin-image
docker build --platform linux/amd64 -t registry.example.com/yudao/yudao-ui-admin:1.0.0 .
docker push registry.example.com/yudao/yudao-ui-admin:1.0.0
Nginx 的配置文件,不打进镜像,而是在下文通过 ConfigMap 挂载,方便修改。
# 3. 编写 K8s 清单
在构建机上,新建一个 k8s 目录,在其中创建如下 4 个文件。文件名的数字前缀,决定了 kubectl apply -f k8s/ 时的执行顺序。
k8s
├── 00-namespace.yaml # 命名空间
├── 01-config.yaml # ConfigMap + Secret:后端配置
├── 02-yudao-server.yaml # 后端:Deployment + Service
└── 03-yudao-ui-admin.yaml # 前端:Nginx 配置 ConfigMap + Deployment + Service
# 3.1 00-namespace.yaml
所有资源,都部署在 yudao 命名空间下:
apiVersion: v1
kind: Namespace
metadata:
name: yudao
# 3.2 01-config.yaml
后端通过环境变量,覆盖 Jar 包中 application-dev.yaml 的 MySQL、Redis 配置。非敏感配置放在 ConfigMap,密码等敏感配置放在 Secret:
apiVersion: v1
kind: ConfigMap
metadata:
name: yudao-server-config
namespace: yudao
data:
TZ: "Asia/Shanghai"
SPRING_PROFILES_ACTIVE: "dev"
JAVA_OPTS: "-Xms512m -Xmx512m -Djava.security.egd=file:/dev/./urandom"
# ① MySQL:master 主库 + slave 从库。application-dev.yaml 中的 slave 是“模拟从库”,没有真正的从库时,也指向主库
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_URL: "jdbc:mysql://192.168.0.100:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true"
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_SLAVE_URL: "jdbc:mysql://192.168.0.100:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true"
# ② Redis:master-jdk17、master-jdk25 分支使用 SPRING_DATA_REDIS_*;master(JDK 8)分支改成 SPRING_REDIS_*
SPRING_DATA_REDIS_HOST: "192.168.0.100"
SPRING_DATA_REDIS_PORT: "6379"
SPRING_DATA_REDIS_DATABASE: "1"
# ③ 其它:与 script/docker-compose 保持一致
YUDAO_CAPTCHA_ENABLE: "true" # 需与前端打包时的 VITE_APP_CAPTCHA_ENABLE 一致
YUDAO_DEMO: "false" # 关闭演示模式,避免禁止写操作
---
apiVersion: v1
kind: Secret
metadata:
name: yudao-server-secret
namespace: yudao
type: Opaque
stringData:
# 重要!!!以下账号密码,按外部 MySQL、Redis 的实际情况填写
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_USERNAME: "yudao"
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_PASSWORD: "改成你的 MySQL 密码"
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_SLAVE_USERNAME: "yudao"
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_SLAVE_PASSWORD: "改成你的 MySQL 密码"
# Redis 密码(master 分支为 SPRING_REDIS_PASSWORD)。Redis 未设置密码时,删除这一行
SPRING_DATA_REDIS_PASSWORD: "改成你的 Redis 密码"
友情提示:
- ConfigMap、Secret 中的 Key,就是 Spring Boot 的环境变量名。例如说,
SPRING_DATASOURCE_DYNAMIC_DATASOURCE_MASTER_URL对应spring.datasource.dynamic.datasource.master.url配置项。环境变量的优先级,高于 Jar 包中的application-dev.yaml。 - 如果你有真正的 MySQL 从库,把
SLAVE_URL改成从库的地址即可。 SPRING_DATA_REDIS_DATABASE是 Redis 的库索引。如果多个环境共用一个 Redis,建议使用不同的库索引,避免缓存的 Key 互相覆盖。- 包含密码的
01-config.yaml,不要提交到 Git 仓库。
# 3.3 02-yudao-server.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: yudao-server
namespace: yudao
spec:
replicas: 1
selector:
matchLabels:
app: yudao-server
template:
metadata:
labels:
app: yudao-server
spec:
# imagePullSecrets: # 私有镜像仓库时,取消注释,见「4. 部署与验证」的第二步
# - name: yudao-registry
containers:
- name: yudao-server
image: registry.example.com/yudao/yudao-server:1.0.0 # 重要!!!替换成你的镜像地址
ports:
- containerPort: 48080
envFrom:
- configMapRef:
name: yudao-server-config
- secretRef:
name: yudao-server-secret
resources:
requests:
cpu: 500m
memory: 768Mi
limits:
memory: 1Gi
# 探针:使用 TCP 方式,只检查 48080 端口是否已经监听
startupProbe: # 启动探针:最多等待 30 x 10 = 300 秒启动完成
tcpSocket:
port: 48080
periodSeconds: 10
failureThreshold: 30
readinessProbe: # 就绪探针:未就绪时,Service 不会把流量转发给它
tcpSocket:
port: 48080
periodSeconds: 10
livenessProbe: # 存活探针:连续失败时,重启容器
tcpSocket:
port: 48080
periodSeconds: 20
failureThreshold: 3
---
apiVersion: v1
kind: Service
metadata:
name: yudao-server
namespace: yudao
spec:
selector:
app: yudao-server
ports:
- port: 48080
targetPort: 48080
疑问:为什么探针使用 TCP,而不是 HTTP?
项目的接口默认需要登录,/actuator 等监控端点也不应该依赖它来做存活判断。所以这里使用 TCP 探针,它只能说明端口已经监听,不代表 MySQL、Redis 一定可用,后者需要通过「4. 部署与验证」中的步骤确认。
探针的更多说明,可见 K8s 官方的 《Configure Liveness, Readiness and Startup Probes》 (opens new window) 文档。
# 3.4 03-yudao-ui-admin.yaml
Nginx 通过 K8s Service 的名字 yudao-server,访问后端:
apiVersion: v1
kind: ConfigMap
metadata:
name: yudao-ui-admin-nginx
namespace: yudao
data:
default.conf: |
server {
listen 80;
server_name _;
client_max_body_size 32m; ## 与后端 spring.servlet.multipart.max-request-size 保持一致
location / { ## 前端项目
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /admin-api/ { ## 后端项目 - 管理后台
proxy_pass http://yudao-server:48080/admin-api/; ## yudao-server 是后端 Service 的名字
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_buffering off; ## 流式响应(SSE)不缓冲
proxy_read_timeout 300s;
}
location /app-api/ { ## 后端项目 - 用户 App
proxy_pass http://yudao-server:48080/app-api/;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_buffering off;
proxy_read_timeout 300s;
}
location /infra/ws { ## 后端项目 - WebSocket
proxy_pass http://yudao-server:48080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
proxy_read_timeout 600s;
}
}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: yudao-ui-admin
namespace: yudao
spec:
replicas: 1
selector:
matchLabels:
app: yudao-ui-admin
template:
metadata:
labels:
app: yudao-ui-admin
spec:
# imagePullSecrets:
# - name: yudao-registry
containers:
- name: nginx
image: registry.example.com/yudao/yudao-ui-admin:1.0.0 # 重要!!!替换成你的镜像地址
ports:
- containerPort: 80
volumeMounts:
- name: nginx-conf
mountPath: /etc/nginx/conf.d # 替换镜像自带的 default.conf
readinessProbe:
tcpSocket:
port: 80
periodSeconds: 10
volumes:
- name: nginx-conf
configMap:
name: yudao-ui-admin-nginx
---
apiVersion: v1
kind: Service
metadata:
name: yudao-ui-admin
namespace: yudao
spec:
type: NodePort
selector:
app: yudao-ui-admin
ports:
- port: 80
targetPort: 80
nodePort: 30080 # 通过 http://节点IP:30080 访问
友情提示:
- Nginx 启动时,需要能解析
yudao-server这个 Service 名,所以02-yudao-server.yaml要先于它创建(文件名前缀已经保证了顺序)。 - 本文为了简单,使用 NodePort 对外暴露。如果你的集群有 Ingress Controller 或 LoadBalancer,也可以改用它们,此时前端构建时的后端地址,要改成对应的域名。
# 4. 部署与验证
# 第一步,创建命名空间
kubectl apply -f k8s/00-namespace.yaml
# 第二步,配置镜像拉取凭证(可选)
如果你的镜像仓库是私有的,执行如下命令创建凭证,并取消 02、03 文件中 imagePullSecrets 的注释:
kubectl -n yudao create secret docker-registry yudao-registry \
--docker-server=registry.example.com \
--docker-username=你的账号 \
--docker-password=你的密码
# 第三步,检查 Pod 到 MySQL、Redis 的连通性
在部署后端之前,先使用临时 Pod,从 K8s 集群内部,检查 MySQL、Redis 是否可以访问:
## ① 检查 MySQL:返回 system_users 表的记录数、以及 QRTZ_LOCKS 表,说明地址、账号密码、SQL 初始化都正确
kubectl -n yudao run mysql-check --rm -it --restart=Never --image=mysql:8.4 \
--env="MYSQL_PWD=改成你的 MySQL 密码" -- \
mysql -h 192.168.0.100 -P 3306 -uyudao ruoyi-vue-pro \
-e 'SELECT COUNT(*) FROM system_users; SHOW TABLES LIKE "QRTZ_LOCKS";'
## ② 检查 Redis:返回 PONG。Redis 未设置密码时,去掉 --env 参数
kubectl -n yudao run redis-check --rm -it --restart=Never --image=redis:7.4-alpine \
--env="REDISCLI_AUTH=改成你的 Redis 密码" -- \
redis-cli -h 192.168.0.100 -p 6379 -n 1 ping
- 这里通过
MYSQL_PWD、REDISCLI_AUTH环境变量传递密码,避免出现在命令行参数中。账号密码与01-config.yaml的 Secret 保持一致。
# 第四步,部署
kubectl apply -f k8s/
## 等待后端、前端部署完成
kubectl -n yudao rollout status deploy/yudao-server --timeout=600s
kubectl -n yudao rollout status deploy/yudao-ui-admin
# 第五步,验证
① 执行 kubectl -n yudao get pods -o wide 命令,查看到 Pod 的状态都是 Running,READY 都是 1/1。
② 执行如下命令,查看后端的启动日志。看到 Started YudaoServerApplication,说明启动完成:
kubectl -n yudao logs deploy/yudao-server | grep "Started YudaoServerApplication"
③ 执行如下命令,验证 Nginx => 后端 Service => 后端 Pod => MySQL 的完整链路:
## 返回 {"code":401,"data":null,"msg":"账号未登录"},说明 Nginx => 后端的转发正常
curl http://192.168.0.213:30080/admin-api/
## 返回 {"code":0,"data":1,"msg":""},说明后端可以从 MySQL 查询到默认租户
curl -G --data-urlencode "name=芋道源码" http://192.168.0.213:30080/admin-api/system/tenant/get-id-by-name
友情提示:如果 NodePort 访问不通,可以先跳过 Nginx,直接验证后端。
执行 kubectl -n yudao port-forward svc/yudao-server 48080:48080 命令,然后在另一个终端执行 curl http://127.0.0.1:48080/admin-api/ 命令。
④ 请求 http://192.168.0.213:30080 (opens new window) 地址,成功访问前端项目。使用默认账号 admin、密码 admin123 登录。
⑤ 登录成功后,执行如下命令,验证 后端 => Redis 正常,可以看到登录的访问令牌:
kubectl -n yudao run redis-check --rm -it --restart=Never --image=redis:7.4-alpine \
--env="REDISCLI_AUTH=改成你的 Redis 密码" -- \
redis-cli -h 192.168.0.100 -p 6379 -n 1 --scan --pattern 'oauth2_access_token:*'
# 5. 日常运维
# 5.1 更新
① 后端:重新编译,构建并推送新版本号的镜像(例如说 1.0.1),然后执行如下命令滚动更新:
kubectl -n yudao set image deploy/yudao-server yudao-server=registry.example.com/yudao/yudao-server:1.0.1
kubectl -n yudao rollout status deploy/yudao-server --timeout=600s
② 前端:同理,更新 yudao-ui-admin 镜像即可。
③ 配置:修改 01-config.yaml 后,执行 kubectl apply -f k8s/01-config.yaml,再执行 kubectl -n yudao rollout restart deploy/yudao-server 让 Pod 重新读取环境变量。
④ 回滚:执行 kubectl -n yudao rollout undo deploy/yudao-server 命令,回滚到上一个版本。
建议同步修改 k8s 目录下 YAML 中的镜像版本号,保证清单与集群一致。
# 5.2 查看日志
kubectl -n yudao logs -f deploy/yudao-server --tail=200 ## 后端日志
kubectl -n yudao logs deploy/yudao-server --previous ## 容器重启后,查看上一次崩溃的日志
kubectl -n yudao describe pod -l app=yudao-server ## 查看 Pod 事件:拉取镜像、探针失败等
容器内的日志文件(/root/logs/yudao-server.log)会随 Pod 删除而丢失。如果需要长期保存,可以接入集群的日志采集方案。
# 5.3 扩容
后端改成多副本前,需要注意:
- WebSocket 的消息发送,默认是
local本地模式(yudao.websocket.sender-type),多副本时需要改成redis等模式。具体可见 《WebSocket 实时通信》 文档。 - Quartz 定时任务已经是数据库集群模式,多副本可以正常工作。
# 5.4 删除
kubectl delete namespace yudao
它会删除本文创建的所有 K8s 资源。MySQL、Redis 在集群外部,不受影响。
# 6. 常见问题
| 现象 | 可能的原因 |
|---|---|
Pod 状态为 ImagePullBackOff | 镜像地址占位符没有替换;私有仓库没有配置 imagePullSecrets;节点无法访问镜像仓库 |
日志报 exec format error | 镜像的 CPU 架构与节点不一致,参见「2.1 后端镜像」的 --platform 参数 |
Pod 状态为 CrashLoopBackOff | 执行 kubectl -n yudao logs deploy/yudao-server --previous 查看原因。常见的是 MySQL / Redis 地址填了 127.0.0.1、密码错误、漏导 quartz.sql、Redis 环境变量与分支不匹配 |
| 启动很慢,被重启 | 资源不足或启动超过 300 秒,可以调大 startupProbe 的 failureThreshold 或 resources |
| 访问页面报 502 | 后端还没有就绪,执行 kubectl -n yudao get endpoints yudao-server 确认有可用的 Pod IP |
前端请求跑到了 localhost:48080 等地址 | 前端构建时,没有把后端地址改成 http://节点IP:30080,参见「2.2 前端镜像」 |
# 666. 更多说明
① 如果你只有一台服务器,更推荐使用 《Docker Compose 部署》。
② 如果需要 HTTPS 访问,建议在 Ingress 或集群外的负载均衡上配置证书,可参考 《HTTPS 证书》 文档申请证书。