简介:面向需要在阿里云Kubernetes环境中部署前后端分离应用的开发者,这份资源完整整合了Vue2、Nginx、Spring Boot 2.5与Nacos 2.0.3的部署链路,可解决从前端静态资源打包上传、后端镜像构建到注册中心高可用编排的系列问题,尤其适合已掌握容器基础、想在真实云环境快速落地的读者。压缩包共16个文件,包括8个YAML编排文件、2个Shell一键部署脚本、2个Dockerfile、1个SQL初始化脚本、Nginx配置、后端JAR包及前端压缩包,整体大小34.69MB;其中YAML覆盖前端应用、后端服务和Nacos集群,Shell脚本封装发布入口,SQL用于初始化Nacos依赖的数据库,目录结构便于按需复用。目前已有1341人学习下载,资源提供了带注释可改的YAML、可直接执行的部署脚本、关联Nacos持久化的MySQL建表SQL以及Nginx代理配置,覆盖部署中常见的容器编排、服务发现、配置注入和网络暴露等环节,能明显缩短环境搭建与排错时间。
1. 先别急着写镜像:单机 docker-compose 能跑,不代表 K8s 能跑
本地 docker-compose 里把 Nacos 2.0.3、SpringBoot 2.5 后端、Vue2 + Nginx 前端凑到一起,几分钟就能起服务;但把这套全家桶迁到阿里云 K8s 集群后,最常见的翻车现场是:后端日志里 Nacos 客户端反复报连接失败,前端刷新一下子路由就 404,Pod 还被探针打得反复重启。问题基本不在代码,而在 Nacos 2.x 多出来的 gRPC 端口、K8s 内部的 Service DNS 与 Pod IP 网络,以及探针健康检查这三件事,它们跟本机 docker 网络完全不是一回事。这篇按我当时部署某跨平台系统的顺序记录:从 ACK 集群和镜像仓库准备,到 Nacos 2.0.3、SpringBoot 2.5、Vue2 + Nginx 逐个落 Pod,最后用一条链路验证到底。适合已经在本地跑通、正准备迁到云上 K8s 的开发者。
2. 环境准备:ACK 集群、命名空间与镜像仓库先对齐
2.1 版本矩阵:先把容易起冲突的依赖定死
部署这套技术栈,最忌讳的是每个组件都用「最新版」。Nacos 2.0.3 是标题里指定的版本,客户端 SDK 也必须走 2.x 这条线;SpringBoot 2.5 对应 Spring Cloud 和 Spring Cloud Alibaba 的版本是有限定的,依赖搭错,服务能启动但就是注册不上,排查起来非常折磨人。我先把跑通的版本组合列成一张表,后面所有 YAML 和镜像都按这个表来。
| 组件 | 版本 / 镜像 | 说明 |
|---|---|---|
| Kubernetes | ACK 托管版 1.24 上下 | 1.22 之后 Ingress API 必须走 networking.k8s.io/v1 |
| Nacos | nacos/nacos-server:v2.0.3 | 注册中心,2.0 起多出 gRPC 端口 |
| SpringBoot | 2.5.x | 后端基础框架 |
| Spring Cloud Alibaba | 2021.0.1.0 一档 | 需与 SpringBoot 2.5、SpringCloud 2020.0.x 对齐 |
| 后端运行镜像 | openjdk:8u345-jre-alpine | 体积小,K8s 场景分发快 |
| 前端基础镜像 | nginx:1.24-alpine | 托管 Vue2 构建产物 |
SpringCloud Alibaba 版本我吃过一次亏:一开始图省事选了新版本,结果 nacos-discovery starter 的客户端与 2.0.3 服务端的兼容性出现怪问题,注册请求发出去没有响应,后来锁到 2021.0.1.0 这一档才消停。我的习惯是跑通之后把所有依赖版本写进 pom 的 properties,不允许随便升级。
2.2 创建 ACK 集群和业务命名空间
阿里云 ACK 托管版不用自己运维 Master 节点,按节点池收费,适合先把业务跑起来。我一般建 2 台 4C8G 的 worker 节点,系统盘 40G,部署这套前端加后端加 Nacos 足够。集群创建完成后,kubectl 连接信息会生成一个 kubeconfig,下载后放到本地 ~/.kube/config,先确认能连上:
kubectl get nodes能列出节点后,我给这套环境建一个独立命名空间,免得和别的应用混在一起,后面所有资源都打到这个 namespace 里:
kubectl create namespace app-demo kubectl label namespace app-demo app=app-demo命名空间的作用不只是隔离,它还决定了 Service 的 DNS 名称。后面 Nacos 地址我会写成 nacos-server:8848,而不是 127.0.0.1,就是依赖同一个 namespace 内 Service DNS 解析。跨 namespace 访问时地址要带上 namespace 后缀,形如 nacos-server.app-demo.svc.cluster.local:8848,这个在排查网络问题时要能反应过来。
2.3 镜像仓库先建好,避免部署时临时赶
K8s 节点要从仓库拉镜像,阿里云容器镜像服务(ACR)个人版足够用。我习惯在 ACR 里建一个命名空间叫 app-images,再建两个镜像仓库分别放后端和前端。本地先登录,再打 tag 推送,三步固定流程:
docker login registry.example.com docker tag biz-server:1.0.0 registry.example.com/app-images/biz-server:1.0.0 docker push registry.example.com/app-images/biz-server:1.0.0registry.example.com 换成你实际 ACR 仓库地址;docker login 的用户名密码就是开通镜像服务时设置的专用密码,不是登录阿里云控制台的密码。前端镜像同理,先构建再推送,tag 保持一致,后面 deployment 里的 image 字段直接复用。
镜像推完之后要注意 imagePullPolicy。如果镜像 tag 是 1.0.0 这种固定版本,我习惯显式写成 IfNotPresent,节点本地有镜像就不重复拉取。如果 tag 写成 latest,那 policy 要设成 Always,但生产不建议这么写,因为无法通过 tag 区分版本。
3. 部署注册中心 Nacos 2.0.3 与后端 SpringBoot 2.5
3.1 Nacos 2.0.3 用 StatefulSet 而不是 Deployment
Nacos 是有状态服务,即使单机模式也涉及数据文件持久化,所以我用 StatefulSet 加 volumeClaimTemplates,而不是普通 Deployment。单机 standalone 模式用内嵌 derby 存储,适合测试和预发;生产环境建议换 MySQL 数据源或做三节点集群,那是另一套配置,这里先把单机跑通。
部署文件里最容易被忽略的是 Service 端口。Nacos 2.0 的服务端不只是监听 8848,客户端从 1.4 版本开始使用 gRPC 长连接,端口号是主端口加 1000 和 1001,也就是 9848 和 9849。9848 供客户端连接,9849 供服务端之间通信。我见过太多人只暴露 8848,结果后端注册时报连接被拒。下面这份 YAML 把 8848 和 9848 都映射到 NodePort,方便本地联调:
apiVersion: v1 kind: Service metadata: name: nacos-server namespace: app-demo spec: selector: app: nacos-server ports: - name: http-8848 port: 8848 targetPort: 8848 nodePort: 30848 - name: grpc-9848 port: 9848 targetPort: 9848 nodePort: 30849 type: NodePort --- apiVersion: apps/v1 kind: StatefulSet metadata: name: nacos-server namespace: app-demo spec: serviceName: nacos-server replicas: 1 selector: matchLabels: app: nacos-server template: metadata: labels: app: nacos-server spec: containers: - name: nacos-server image: nacos/nacos-server:v2.0.3 ports: - name: http-8848 containerPort: 8848 - name: grpc-9848 containerPort: 9848 - name: grpc-9849 containerPort: 9849 env: - name: MODE value: "standalone" volumeMounts: - name: nacos-data mountPath: /home/nacos/data volumeClaimTemplates: - metadata: name: nacos-data spec: accessModes: ["ReadWriteOnce"] resources: requests: storage: 20GiMODE 环境变量设为 standalone 表示单机模式;volumeClaimTemplates 会自动给 Pod 分配一块云盘,Pod 被删掉重建后数据还在。注意 9849 这里也映射了容器端口,但 Service 没暴露它,单机模式不需要对外提供这个端口。
部署完后用 kubectl apply -f nacos.yaml 创建资源,然后等待 Pod 变成 Running:
kubectl get pods -n app-demo -o wide | grep nacos看到 READY 为 1/1 后,浏览器访问 http://节点公网IP:30848/nacos,能打开控制台就说明 8848 通了。控制台默认账号密码是 nacos/nacos,生产环境务必改掉默认密码。
3.2 后端 bootstrap.yml 别把 Nacos 地址写死
SpringBoot 2.5 接 Nacos 依赖 bootstrap 上下文,需要引入 spring-cloud-starter-bootstrap 这个依赖,否则 bootstrap.yml 不生效,容易踩。bootstrap.yml 里我一般这么写:
spring: application: name: biz-server cloud: nacos: discovery: server-addr: ${NACOS_ADDR:127.0.0.1:8848} ip: ${POD_IP:}server-addr 用环境变量 NACOS_ADDR 注入,K8s 部署时指向 nacos-server:8848,本地开发时默认 127.0.0.1:8848,一套配置两边都能跑。ip 字段是重点:在 K8s 里如果不显式指定,客户端上报给 Nacos 的 IP 可能是内网 Pod IP,但某些网络插件下取到的地址不一定正确,导致控制台里能看到服务但实际调用不通。我通过环境变量 POD_IP 把 Pod 的真实 IP 传进去,保证服务列表里显示的地址可用。
启动类上记得加 @EnableDiscoveryClient 注解,依赖用 nacos-discovery starter。版本对齐到 Spring Cloud Alibaba 2021.0.1.0,客户端内置的 nacos-client 版本就是 2.x,能和 Nacos 2.0.3 服务端正常完成 gRPC 握手。
3.3 后端 Dockerfile 用多阶段构建
后端镜像我习惯用多阶段构建:构建阶段用 maven 镜像,运行阶段用 openjdk 镜像,最终镜像里不残留 maven 和源码。Dockerfile 如下:
FROM maven:3.8.6-openjdk-8 AS build WORKDIR /build COPY pom.xml . RUN mvn dependency:resolve -B COPY src ./src RUN mvn package -DskipTests -q FROM openjdk:8u345-jre-alpine COPY --from=build /build/target/biz-server.jar /app/app.jar ENTRYPOINT ["java", "-Xms512m", "-Xmx512m", "-jar", "/app/app.jar"]先单独 COPY pom.xml 并执行 dependency:resolve,是为了把依赖拉取结果做成 docker 缓存层,改代码后重新构建镜像不用重新下载依赖。如果项目里有私有 maven 仓库,需要在 maven 镜像或 settings.xml 里配置好。JVM 参数 512m 对齐节点资源,4C8G 节点上跑两个副本不紧张。
如果你没有在服务器上构建镜像的习惯,也可以在本地直接 mvn package,然后用一个更简单的 Dockerfile 只 COPY jar 包。两种方式产物一样,看团队习惯。
3.4 后端 Deployment:探针和启动顺序是隐蔽的坑
K8s 不会保证 Nacos 先启动、后端后启动,两个 Deployment 同时提交,后端可能比 Nacos 先进入 Running。SpringBoot 启动速度快,但 Nacos 客户端重试机制能扛住短暂不可用,真正的问题是探针。如果 readinessProbe 通过 /actuator/health 检查,而项目的 health 聚合了 Nacos 状态,注册中心没就绪时探针就失败,Pod 被判定未就绪,K8s 可能重启容器,形成死循环。我的 deployment 写法:
apiVersion: apps/v1 kind: Deployment metadata: name: biz-server namespace: app-demo spec: replicas: 2 selector: matchLabels: app: biz-server template: metadata: labels: app: biz-server spec: imagePullSecrets: - name: acr-secret containers: - name: biz-server image: registry.example.com/app-images/biz-server:1.0.0 ports: - name: http containerPort: 8080 env: - name: NACOS_ADDR value: "nacos-server:8848" - name: POD_IP valueFrom: fieldRef: fieldPath: status.podIP readinessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 30 periodSeconds: 10 failureThreshold: 3 --- apiVersion: v1 kind: Service metadata: name: biz-server namespace: app-demo spec: selector: app: biz-server ports: - name: http port: 8080 targetPort: 8080 type: ClusterIPNACOS_ADDR 指向 nacos-server:8848,利用 K8s 内部 DNS 解析到 Nacos 的 ClusterIP,比写死某个节点 IP 可靠。POD_IP 通过 fieldRef 从 Pod 元数据获取,对应 bootstrap.yml 里的 ${POD_IP}。readinessProbe 的 initialDelaySeconds 我给了 30 秒,SpringBoot 应用启动加 Nacos 注册,时间紧张的话也可以改成 startupProbe 给足启动窗口。
后端 Service 类型是 ClusterIP,集群内通过 biz-server:8080 访问,Ingress 层也会转发到这个 Service。两个后端副本都注册到 Nacos,服务间调用走负载均衡,这是部署后端的基本形态。
4. 部署前端 Vue2 + Nginx 与 Ingress 路由
4.1 Vue2 构建产物:publicPath 和接口前缀先对齐
前端项目是 Vue2,构建方式通常是 npm install 加 npm run build。我们项目里用的构建脚本是 build:prod,产物输出到 dist 目录。构建命令我习惯固定成一套:
npm ci --registry=https://registry.npmmirror.com npm run build:prodnpm ci 比 npm install 更适合在部署环境使用,它严格按 package-lock.json 安装,不会因为依赖版本漂移出现构建结果不一致。npm 源用镜像站加快速度,这个视团队网络情况而定。构建前要确认 publicPath:如果 Ingress 里前端挂在根路径 /,publicPath 保持相对路径或 / 即可;如果前端要挂在 /app 之类子路径,publicPath、nginx root 和 Ingress path 三处必须一致,这个不一致是前端白屏的高发原因。
构建完成后把 dist 目录打成镜像推送到仓库,和推送后端的流程一致:
docker build -t registry.example.com/app-images/frontend:1.0.0 . docker push registry.example.com/app-images/frontend:1.0.04.2 Nginx 配置:try_files 是 Vue2 history 路由的后悔药
Vue2 项目如果用了 vue-router 的 history 模式,路由是前端控制的,不存在真实的路径文件。用户访问 /detail/123 时,nginx 会去磁盘找这个文件,找不到就返回 404。解决方式是加 try_files,把不存在的路径全部回退到 index.html。我的 nginx.conf 长这样:
server { listen 80; server_name _; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { return 404; } }try_files 依次尝试原始路径、目录、最后 index.html,这样前端路由在刷新时也能正确加载。至于 /api 的 location,我故意返回 404,因为接口转发统一放在 Ingress 层做,如果请求落到了这个 location,说明 Ingress 配置有问题,返回 404 比返回 index.html 更容易暴露问题。如果你想让 nginx 自己转发接口,可以在这里配置转发到后端 Service,但那样 Ingress 只需要指向前端一个服务,两种方案不要混用。
4.3 前端 Dockerfile 和 frontend.yaml
前端镜像不需要 JDK,也不需要 node_modules,只要 dist 产物加 nginx 配置。Dockerfile 很简单:
FROM nginx:1.24-alpine COPY dist/ /usr/share/nginx/html/ COPY nginx.conf /etc/nginx/conf.d/default.confnginx 基础镜像里已有默认配置,我用自定义的 nginx.conf 覆盖 default.conf,避免默认配置干扰。前端 Deployment 副本数可以比后端多,因为纯静态资源,nginx 内存占用很小:
apiVersion: apps/v1 kind: Deployment metadata: name: frontend namespace: app-demo spec: replicas: 2 selector: matchLabels: app: frontend template: metadata: labels: app: frontend spec: imagePullSecrets: - name: acr-secret containers: - name: frontend image: registry.example.com/app-images/frontend:1.0.0 ports: - name: http containerPort: 80 readinessProbe: httpGet: path: / port: 80 initialDelaySeconds: 10 periodSeconds: 5 --- apiVersion: v1 kind: Service metadata: name: frontend namespace: app-demo spec: selector: app: frontend ports: - name: http port: 80 targetPort: 80 type: ClusterIP探针直接请求根路径,nginx 返回 200 就算就绪。前端 img 请求静态资源不经过后端,这种情况用 httpGet 探针没问题。
4.4 Ingress 路由:/ 到前端,/api 到后端
Ingress 是整个集群的统一入口,阿里云 ACK 自带 nginx ingress controller,资源创建后自动绑定一个公网 SLB 地址。域名解析到这个地址后,所有流量先进 Ingress,再按规则分发。我们的规则是根路径 / 给前端,/api 开头的请求给后端:
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: app-ingress namespace: app-demo annotations: nginx.ingress.kubernetes.io/rewrite-target: /$2 spec: ingressClassName: nginx rules: - host: app.example.com http: paths: - path: /api(/|$)(.*) pathType: ImplementationSpecific backend: service: name: biz-server port: number: 8080 - path: / pathType: Prefix backend: service: name: frontend port: number: 80这里的 rewrite-target 作用是把 /api/ 前缀去掉再转发给后端。如果后端接口路径本身带了 /api 前缀,比如 controller 的 RequestMapping 是 /api/login,那就不需要 rewrite,直接把 path 写成 /api 加 Prefix 类型即可。两种方式按后端实际接口路径选一种,不要同时用。ingressClassName 要跟集群里的 IngressClass 一致,ACK 上默认叫 nginx,如果集群里改过名字,这里要对应改。
Ingress 配置完成后,用 kubectl get ingress -n app-demo 查看状态,ADDRESS 列会出现公网入口 IP。域名解析那里把 app.example.com 指向这个 IP,等解析生效就能访问了。
5. 常见问题排查:注册不上、404、重启循环、镜像拉不下来
5.1 现象:后端日志反复报 Connection refused,Nacos 控制台却能打开
后端启动后,日志里出现类似Connection refused或no available server的报错,但浏览器访问 Nacos 控制台 8848 端口是通的。很多人第一次遇到会怀疑是用户名密码问题,实际不是。
原因:Nacos 2.x 服务端除了 8848,还用 9848 端口做 gRPC 长连接。后端 nacos-client 是 2.x 版本,注册时会自动从 8848 加 1000 得到 9848 去建立连接。如果 Service 只暴露了 8848,9848 不通,注册必然失败。
解决:把 Service 里补上 grpc-9848 端口映射,也就是第 3.1 节里那份 YAML 的写法。如果集群内部走 ClusterIP,只要 Service 里有 9848 端口即可;如果要通过节点访问,需要再开 NodePort 映射。验证方法是用 kubectl exec 进入任意 Pod,执行 telnet nacos-server 9848,能通说明端口层没问题。
5.2 现象:Nacos 控制台能看到服务,但服务间调用时报 UnknownHost
后端已经注册到 Nacos,控制台服务列表里显示了一个 10.x.x.x 的地址,但服务 A 调服务 B 时偶尔报 UnknownHostException,或者一段时间后又连不上。
原因:客户端上报的 IP 不是稳定可路由的地址。Pod 重建后 IP 会变,如果注册的是 Pod IP,旧实例下线后服务发现里短暂保留旧地址,调用就会失败。另外某些网络环境下客户端取到的是错误的网卡地址。
解决:给 bootstrap.yml 的 discovery 配置显式传入 POD_IP 环境变量,也就是第 3.2 节的写法,让 Nacos 里展示的地址始终是当前 Pod 的真实 IP。同时服务间调用尽量走 K8s Service DNS,而不是直接依赖 Nacos 里看到的 IP 去拼地址,这样 Pod 重建后 Service 会自动把流量转发到新 Pod。
5.3 现象:前端首页能打开,点进子路由再刷新就 404
浏览器直接访问域名首页正常,但进入 /detail/123 之类的页面后按 F5 刷新,nginx 返回 404。后端接口一切正常,静态资源加载也正常。
原因:Vue2 的 history 路由模式下,浏览器刷新会真实请求 /detail/123 这个路径,nginx 在磁盘上找不到对应文件,按默认配置返回 404。这是 nginx 托管 SPA 应用最典型的坑,本地 dev server 正常是因为 dev server 内置了 history fallback,nginx 没有。
解决:nginx.conf 里加上try_files $uri $uri/ /index.html;,将不存在的路径回退到 index.html。如果加了之后仍然偶发 404,检查 Ingress 的 path 规则,根路径 / 必须是 Prefix 类型,并且不要和 /api 规则顺序互相覆盖。
5.4 现象:后端 Pod 一直重启,READY 显示 0/1
kubectl get pods 看到 biz-server 的 READY 是 0/1,RESTARTS 次数持续增长,查看 Pod 事件全是 readiness 探针失败。
原因:readinessProbe 配置的 initialDelaySeconds 太短,SpringBoot 应用还没完成启动和 Nacos 注册,探针就开始执行。/actuator/health 返回非 200,K8s 判定容器未就绪,不断重启容器,反而让应用永远没有机会完成启动,形成死循环。如果 health 又聚合了 Nacos 健康状态,Nacos 本身还在启动时,这个问题更明显。
解决:给后端配置 startupProbe,把启动窗口放宽到 60 秒左右,readinessProbe 的 initialDelaySeconds 调到 30 秒以上。也可以把探针路径从 /actuator/health 改成 /actuator/health/liveness,只检查进程存活,不检查依赖组件状态。等应用起来后,再通过 readiness 控制流量接入。探针参数要按实际启动速度调整,不要照抄网上的模板。
5.5 现象:Pod 一直 ImagePullBackOff,节点拉不到镜像
后端和前端镜像都推到了 ACR,deployment 也写好并 apply 了,但 Pod 事件显示 ImagePullBackOff,describe 里能看到 Failed to pull image 的报错。
原因:镜像仓库是私有的,K8s 节点没有仓库凭证,默认拉取会认证失败。这个和本地 docker login 没关系,节点上的 kubelet 不会读取你本机的 docker 配置。
解决:在命名空间里创建一个 docker-registry 类型的 Secret,然后在 deployment 的 pod template 里引用它:
kubectl create secret docker-registry acr-secret \ --docker-server=registry.example.com \ --docker-username=你的用户名 \ --docker-password=你的密码 \ -n app-demodeployment 的 spec.template.spec 下加 imagePullSecrets 字段,指向 acr-secret。如果多个 deployment 都要用,逐个加上,或者通过给 namespace 配置默认 imagePullSecret 的方式统一处理。这里创建完 Secret 后,已经存在的 Pod 不会自动生效,需要 rollout restart 一次。
6. 装完验证一条链路:页面、接口、注册中心一路走通
部署全部完成后,建议固定按下面的顺序验证一遍,不要只看 Pod 状态是 Running 就收工。
第一步看 Pod。kubectl get pods -n app-demo -o wide,确认 nacos-server、biz-server、frontend 都是 Running 状态,READY 为 1/1,RESTARTS 没有异常增长。如果有重启记录,回到第 5.4 节检查探针参数。
第二步看注册。kubectl logs deployment/biz-server -n app-demo --tail=200,搜索 nacos 相关日志,正常情况下能看到服务注册成功的输出。同时打开 Nacos 控制台,地址是 http://节点公网IP:30848/nacos,登录后在服务管理里应该能看到 biz-server 这个服务名,实例数为 2,对应两个后端 Pod,实例 IP 与控制台里 Pod IP 一致。
第三步看前端。curl -I http://app.example.com/,期望返回 HTTP/1.1 200。浏览器打开域名,页面正常渲染,按 F12 切到 Network 面板,刷新一个带子路由的页面,确认页面不 404,静态资源全部 200。
第四步看业务链路。在 Network 面板里找一个真实接口请求,比如登录接口,确认返回 JSON 而不是 index.html 的 HTML 内容。我自己习惯再用 curl 模拟一次:
curl -X POST http://app.example.com/api/login \ -H 'Content-Type: application/json' \ -d '{"username":"admin","password":"123456"}'返回 JSON 说明 Ingress 的 /api 转发规则生效,后端 Service 也正常。
第五步做滚动更新验证。kubectl rollout restart deployment/biz-server -n app-demo,等到新 Pod 全部 Ready 后,回 Nacos 控制台看实例数:正常情况下旧实例注销、新实例注册,最终实例数恢复到 2,并且所有实例健康。这一步最能暴露注册中心配置问题,很多环境部署完能跑,但一发布就掉注册。
从那以后,我每次部署这套环境都会强制走完这五步,尤其是最后一步滚动更新,它能把 Nacos 注册、网络解析、探针配置的所有问题一次性逼出来。只有完整跑过这条链路,我才敢说环境是真的装好了。希望帮到你。
本文还有配套的精品资源,点击获取