简介:面向在阿里云Kubernetes中搭建前后端分离应用的开发与运维人员,这份资源提供了Vue2+Nginx+Spring Boot 2.5+Nacos 2.0.3的整套部署参考。包内共16个文件,核心为8个YAML编排文件,另含2个Shell部署脚本、2个Dockerfile、1个SQL初始化脚本、1个Nginx配置、1个可执行Jar包和1个前端静态资源压缩包,整体约34.69MB;各模块按组件分目录存放,便于快速定位。已有1341人参与学习下载。方案覆盖Nacos有状态集群的StatefulSet部署、MySQL数据初始化、前端Vue项目镜像构建、后端Spring Boot服务接入以及Ingress统一网关配置;其中deploy.sh可串联镜像构建与资源下发,nacos-mysqlConfig.yaml等文件细化了配置中心所需的数据库准备。通过这套资源,读者能直接获得从容器镜像到K8s资源调度的完整链路,减少在云上逐个调试YAML和Deployment的时间成本,适合作为K8s落地项目的初始模板或排错参考,也便于按业务需求二次扩展。
1. 把 Vue2 + SpringBoot 2.5 + Nacos 2.0.3 搬上托管 K8s:这份资源解决的不是部署命令,而是微服务的整合链路
让一套老技术栈完整落到 K8s 上,真正的难点从来不是单个组件怎么打镜像。Vue2 前端、Nginx、SpringBoot 2.5、Nacos 2.0.3,四个组件单独看都有成熟方案,一旦放进同一个集群就开始互相纠缠:Nacos 节点在 Pod 里找不到彼此、SpringBoot 注册地址错乱、前端刷新路由白屏。这份资源就是围绕这条完整链路整理的——前端多阶段构建、Nacos 集群的 StatefulSet 编排、SpringBoot 配置注入、Ingress 与 Nginx 路由规则,外加我在某云厂商托管容器服务上实测踩过的坑。适合已经会用 kubectl 基础操作、但第一次做微服务容器化的后端或运维/DevOps 开发,照着能一条链路跑通,遇到问题也知道去哪找原因。
2. 镜像仓库与集群规划:动手写 YAML 前先把边界定死
2.1 命名空间与镜像仓库:环境隔离的第一道闸
我接手的这类项目,第一件事从来不是写 deployment,而是先把命名空间和镜像仓库的边界定义清楚,否则后面每次发布都在和「配错环境」「拉错镜像」作斗争。
集群侧我会按环境拆三套命名空间:dev、staging、prod。每套环境的 Nacos、SpringBoot、前端都在各自命名空间里跑,资源配额按环境单独设置,staging 里有人压测,不会抢走生产环境的 CPU。很多团队用一套命名空间加 label 区分环境,省事是省事,但遇到 NetworkPolicy 和配额调优时就很别扭——label 拦不住资源竞争。
镜像仓库侧同理,仓库路径按「项目名/组件名」组织,tag 带上明确的版本标识。我习惯的规则是:dev 环境用 latest 或日期 tag,staging 和 prod 用三段式版本号。这样生产部署前必须显式指定版本,杜绝「全环境都拉 latest」的隐患。登录、构建、推送的完整命令如下:
docker login --username=某账号 registry.example.com docker build -t registry.example.com/模拟项目X/vue2-web:1.0.0 ./frontend docker push registry.example.com/模拟项目X/vue2-web:1.0.0 docker build -t registry.example.com/模拟项目X/springboot-api:1.0.0 ./backend docker push registry.example.com/模拟项目X/springboot-api:1.0.0tag 里的三段版本号逻辑是:主版本对应架构或接口变更,次版本对应功能迭代,构建号对应每次构建递增。构建号要能和 CI 流水线的运行序号对应上,出问题后才能直接定位「是哪一次构建的产物」,比时间戳精确得多。这里的registry.example.com是占位符,实际换成你所在云厂商的镜像仓库地址即可。
另外,镜像仓库的访问凭证建议单独配置,不要直接写死在 CI 脚本里。K8s 侧用 ImagePullSecret 引用仓库凭证,部署时 Pod 才能拉取私有镜像。这一步漏掉,Pod 会一直停在 ImagePullBackOff,日志只说拉取失败,不细看你根本想不到是凭证问题。
2.2 Vue2 前端镜像:多阶段构建与控制 50MB 以内
前端镜像最常见的问题,是直接在 node 基础镜像里跑 Nginx,构建工具、依赖、源码全堆在一起,镜像体积轻松超过 1GB,拉取慢、启动也慢。正确做法是构建阶段和运行阶段分开:构建阶段用 node 环境出 dist 产物,运行阶段只留 Nginx 加静态文件。
# 阶段一:构建 Vue2 产物 FROM node:16-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm install --registry=https://registry.npmmirror.com COPY . . RUN npm run build # 阶段二:Nginx 运行时 FROM nginx:1.23-alpine COPY --from=build /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80AS build给第一阶段起了名字,第二阶段的COPY --from=build只把/app/dist目录拷过来。node 镜像、node_modules、源码在最终镜像里全部不存在,体积实测能压到 50MB 以内。npm install 用的是国内镜像源,首次构建时间能从十分钟级别降到两三分钟。
这个 Dockerfile 有两个翻车点。第一,COPY package*.json和COPY . .分两步写,是为了利用 Docker 的层缓存:依赖没变时,npm install 那一层直接命中缓存,不用每次重装。
提示:层缓存的前提是 package.json 和 package-lock.json 没有变化。依赖一旦变更,npm install 那层必然会重新执行,这是 Docker 缓存的正常机制,不是 bug。
第二,如果你在前端构建脚本里读取环境变量,要记住 Vue2 的 .env 是构建期读取的,运行时传给容器的环境变量进不了打包后的 JS。这个问题我在第五章踩坑实录里单独讲。
2.3 SpringBoot 2.5 后端镜像:JDK 基础镜像与 jar 包拷贝
SpringBoot 镜像相对简单,核心是把构建好的 jar 包放进 JDK 镜像里跑。这里有个选型细节:SpringBoot 2.5 默认支持 Java 8 和 Java 11,我用 openjdk:8 的 JRE 镜像而不是完整 JDK 镜像,体积能小一半,运行阶段不需要编译器,JRE 足够。
FROM openjdk:8u342-jre WORKDIR /app COPY target/模拟项目X-api.jar /app/api.jar ENV JAVA_OPTS="-Xms512m -Xmx512m -XX:MaxMetaspaceSize=256m" ENV SPRING_PROFILES_ACTIVE=prod EXPOSE 8080 ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app/api.jar"]JAVA_OPTS和SPRING_PROFILES_ACTIVE都设成环境变量而不是写死,是为了在 deployment 的 env 里按环境覆盖。SpringBoot 2.5 原生支持SPRING_PROFILES_ACTIVE环境变量对应 spring.profiles.active,不需要在代码里做任何加工。
最后一行ENTRYPOINT用sh -c包一层,是因为${JAVA_OPTS}里的变量需要 Shell 展开。直接写ENTRYPOINT ["java", "-jar"]这种 exec 形式,运行期就没办法注入额外 JVM 参数了,这也是很多人改了 JAVA_OPTS 却不生效的原因。
3. Nacos 2.0.3 集群化部署:为什么 StatefulSet 是必选项,参数又该怎么定
3.1 选型理由:Deployment 跑 Nacos 的问题在哪
如果你搜过 Nacos 的 K8s 部署方案,会看到有人用 Deployment 跑单机 Nacos,也有人用官方 Helm Chart 一把梭。单机模式适合本地联调,生产环境用 Deployment 跑单机 Nacos 有两个致命问题:一是 Pod 重建后数据丢失——内置 Derby 把数据写在容器文件系统里,Pod 一删全没;二是没有集群容错,Nacos 挂了整个微服务调用链都跟着瘫。
集群模式必须上 StatefulSet。原因在于 Nacos 节点之间需要稳定的网络标识:StatefulSet 给每个 Pod 分配固定有序的 hostname(nacos-0、nacos-1、nacos-2),配合 Headless Service,每个节点都能用固定域名被其他节点和客户端找到。Deployment 的 Pod 名字带随机后缀,每次重建就变一次,集群节点互相注册时拿到的都是临时地址,根本组不成稳定集群。
3.2 Headless Service 与 StatefulSet:核心 YAML 拆解
先看 Service。Nacos 2.0.3 对外要暴露的端口不止 8848,还有 9848 的 gRPC 端口,这两个都是客户端通信的命脉。
apiVersion: v1 kind: Service metadata: name: nacos-headless namespace: prod labels: app: nacos spec: type: ClusterIP clusterIP: None ports: - port: 8848 name: http - port: 9848 name: rpc - port: 9849 name: old-rpc selector: app: nacosclusterIP: None是 Headless Service 的标识。普通 Service 会分配一个虚拟 IP 做负载均衡,Headless Service 不给虚拟 IP,DNS 查询直接返回所有后端 Pod 的 IP 列表。Nacos 集群节点通过nacos-0.nacos-headless.prod.svc.cluster.local这类域名互相访问,Pod 重建、IP 变化都不影响集群关系。
端口方面,8848 是控制台和 HTTP API,9848 是 Nacos 2.0 的 gRPC 主通信端口,9849 是兼容旧客户端的端口。客户端连接时写 8848,服务端会自动协商到 9848,但 Service、安全组、网络策略必须把 9848 一并放行,少一个端口就会出现「控制台正常、客户端连不上」的怪现象,这个坑在 5.4 节详细展开。
再看 StatefulSet 的核心配置:
apiVersion: apps/v1 kind: StatefulSet metadata: name: nacos namespace: prod spec: serviceName: nacos-headless replicas: 3 selector: matchLabels: app: nacos template: metadata: labels: app: nacos spec: containers: - name: nacos image: nacos/nacos-server:v2.0.3 ports: - containerPort: 8848 - containerPort: 9848 env: - name: NACOS_SERVERS value: "nacos-0.nacos-headless.prod.svc.cluster.local:8848,nacos-1.nacos-headless.prod.svc.cluster.local:8848,nacos-2.nacos-headless.prod.svc.cluster.local:8848" - name: NACOS_REPLICAS value: "3" - name: SPRING_DATASOURCE_PLATFORM value: "mysql" - name: MYSQL_SERVICE_HOST value: "mysql-svc.prod.svc.cluster.local" - name: MYSQL_SERVICE_DB_NAME value: "nacos_config" - name: MYSQL_SERVICE_USER value: "nacos" - name: MYSQL_SERVICE_PASSWORD value: "改成实际密码" - name: JVM_XMS value: "1g" - name: JVM_XMX value: "1g" - name: JVM_XMN value: "512m" volumeMounts: - name: data mountPath: /home/nacos/data volumeClaimTemplates: - metadata: name: data spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 20GiserviceName: nacos-headless把 StatefulSet 和前面的 Headless Service 绑定,这是 Pod 能拿到稳定域名的前提。NACOS_SERVERS是集群节点发现的关键参数,三个节点的地址完全由 Headless Service 的 DNS 域名组成,而不是 Pod IP。当某个节点重建后,K8s 会重新分配 IP,但域名不变,Nacos 集群能自动恢复。
SPRING_DATASOURCE_PLATFORM=mysql和 MYSQL 系列变量,告诉 Nacos 把配置数据存到外部 MySQL 而不是内置 Derby。
注意:集群模式下 Derby 不支持多节点共享数据,外部 MySQL 是生产环境硬性要求,不是可选优化。不配的话,三个节点各自维护一份数据,配置写进去互相看不见。
volumeClaimTemplates给每个 Pod 自动创建独立的 PVC,这是 StatefulSet 和 Deployment 在存储上的本质区别。storageClassName 我用了云厂商的 SSD 类型,PV 和 PVC 由平台自动绑定。Nacos 的 home 目录挂了 20Gi,实际用不了这么多,但预留多点总比扩容时停服务强。
3.3 MySQL 初始化:Nacos 2.0.3 集群模式的隐藏前置条件
接上文,这里有个特别容易漏的环节:Nacos 启动之前,MySQL 里必须已经有 Nacos 的表结构。官方镜像不自带初始化脚本,直接启动会看到日志里一堆「表不存在」的报错。
常见做法是先把 Nacos 官方包里的nacos-mysql.sql导入目标库:
mysql -h mysql-svc -u nacos -p nacos_config < nacos-mysql.sql导入后确认几张关键表存在,比如config_info、config_info_aggr、his_config_info,这些是配置中心最核心的数据表。我见过有人跳过这步直接起集群,日志里全是报错,排查了半天以为是端口或者内存问题。另外,MySQL 字符集要设成 utf8mb4,配置内容里一旦有特殊字符,默认字符集会直接写入失败。这步做好,Nacos 集群启动才会干净。
4. SpringBoot 2.5 与前端 Nginx 的部署编排:配置注入和路由回退是重头戏
4.1 版本搭配:Spring Cloud Alibaba 与 SpringBoot 2.5 怎么对齐
SpringBoot 2.5 跑在 Nacos 2.0.3 上,中间隔着一层 Spring Cloud Alibaba。版本不对是启动阶段最隐蔽的问题。我在模拟项目X里用的是 Spring Cloud Alibaba 2021.0.x 对应的 starter,在 pom.xml 里固定版本:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <version>2021.1</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> <version>2021.1</version> </dependency>这套组合对应 Nacos 客户端 1.x 的通信能力,和 Nacos 服务端 2.0.3 实测稳定。如果换成更新的版本线,客户端和服务端的 gRPC 兼容性会有细微差异,表现是「服务注册成功但配置中心拉取超时」。这种问题最磨人,它不直接报错,只是行为异常,日志看起来一切都是好的。
建议在 pom 里显式写版本,而不是继承 Spring Cloud Alibaba 的 BOM。显式版本的好处是排查时能一眼确定客户端版本,BOM 隐式管理会让依赖树变得难追踪。SpringBoot 2.5 的父 POM 不动,单独引入这两个 starter 就行,两者不会冲突。
4.2 Deployment 环境变量:把 Nacos 地址和 Pod IP 传进去
SpringBoot 服务的 Deployment,核心不是镜像,是环境变量。Nacos 地址、环境 profile、JVM 参数,全部通过 env 注入,配套的 Service 负责让前端 Nginx 和微服务之间互相解析。
apiVersion: v1 kind: Service metadata: name: springboot-api namespace: prod spec: selector: app: springboot-api ports: - port: 8080 targetPort: 8080 --- apiVersion: apps/v1 kind: Deployment metadata: name: springboot-api namespace: prod spec: replicas: 2 selector: matchLabels: app: springboot-api template: metadata: labels: app: springboot-api spec: containers: - name: api image: registry.example.com/模拟项目X/springboot-api:1.0.0 ports: - containerPort: 8080 env: - name: SPRING_PROFILES_ACTIVE value: "prod" - name: SPRING_CLOUD_NACOS_DISCOVERY_SERVER_ADDR value: "nacos-headless.prod.svc.cluster.local:8848" - name: SPRING_CLOUD_NACOS_CONFIG_SERVER_ADDR value: "nacos-headless.prod.svc.cluster.local:8848" - name: JAVA_OPTS value: "-Xms512m -Xmx512m -XX:MaxMetaspaceSize=256m"SPRING_CLOUD_NACOS_DISCOVERY_SERVER_ADDR和SPRING_CLOUD_NACOS_CONFIG_SERVER_ADDR是 Spring Cloud Alibaba 自动读取的标准环境变量,分别对应注册中心和配置中心地址。这里填的是 Headless Service 的集群内域名,不是某个 Nacos Pod 的 IP。域名解析由 K8s 内部 DNS 负责,任何时刻都能解析到存活的 Nacos 节点,比写死 IP 稳妥得多。
Service 和 Deployment 写在同一个 YAML 里用---分隔,kubectl apply一次就能两件资源都建出来。Service 的 selector 只按app: springboot-api选后端 Pod,集群内其他服务访问springboot-api.prod.svc.cluster.local:8080就能打到后端的两个副本上。
如果服务之间需要互相调用,注册进 Nacos 的地址必须是调用方可达的地址。默认情况下,Spring Cloud Alibaba 用 Pod 的容器 IP 注册,同一个命名空间内没问题;跨命名空间调用就得用 Service 域名或者额外配置。我一般让一组微服务待在同一命名空间内互相访问,从源头规避掉大部分网络平面问题。
4.3 Vue2 history 路由回退:Nginx try_files 与接口代理
前端部署到 K8s 时,Nginx 配置里最关键的是try_files那一行。Vue2 开了 history 模式路由后,浏览器地址栏里是真实 URL,但静态目录里没有对应文件,Nginx 默认行为是找文件、失败、返回 404。刷新二级页面白屏,就是这里来的。
Nginx 配置长这样:
server { listen 80; server_name _; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://springboot-api.prod.svc.cluster.local:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }try_files $uri $uri/ /index.html的含义是按顺序尝试:先找真实文件$uri,找不到找目录$uri/,还找不到就回退到/index.html,路由交给 Vue Router 接管。这行配置解决了 history 模式的刷新 404。代价是「路径不存在也返回 200」,这对前端单页应用是期望行为,但后端接口路径不要被这层接住,所以接口单独放在 location /api/ 下。
proxy_pass指向 SpringBoot 的 K8s Service 域名。这里有个细节:proxy_pass 末尾带/api/时,转发保留完整路径,后端 Controller 的 RequestMapping 也要带 /api 前缀;如果改成http://springboot-api.prod.svc.cluster.local:8080/,会把/api前缀剥掉再转发。两种写法对应两种后端设计,经常见到前后端约定不一致导致 404,先和后端确认接口路径再定这个配置。
这个 nginx.conf 要在构建镜像时一并打进去,也就是 2.2 节 Dockerfile 里的COPY nginx.conf /etc/nginx/conf.d/default.conf。改了配置必须重新构建并推送镜像,光改本地文件不重新打包等于白改。
5. 避坑实录:Nacos 反复重启、注册地址错乱、前端 404、gRPC 断连
5.1 Nacos 节点反复 CrashLoopBackOff
现象:Nacos 三个 Pod 全部进入 CrashLoopBackOff,kubectl logs里看到 Derby 数据库文件锁冲突,或者 JVM 内存溢出的 OutOfMemoryError。
原因:最常见的是没接 MySQL。Nacos 2.0.3 集群模式强制要求数据持久化到共享存储,内置 Derby 只允许单节点访问,多节点同时操作就会互相锁文件。另一个常见原因是 JVM 堆给太小,Nacos 2.0 的 gRPC 服务在启动阶段会额外占用堆外内存,512M 堆在小规格节点上很容易触发 OOM。
解决:先确认环境变量里SPRING_DATASOURCE_PLATFORM=mysql和 MySQL 地址是否已配置,没配就补上,并把 nacos-mysql.sql 初始化脚本导入目标库。内存方面,把 JVM_XMS 和 JVM_XMX 至少调到 1g,同时保证 Pod 的resources.limits.memory大于「堆 + 元空间 + 堆外」的总量,一般留 50% 余量,堆设满的话 Pod 会被整体 OOMKilled。
5.2 SpringBoot 服务注册到 Nacos 的 IP 不对
现象:Nacos 控制台能看到服务列表,但服务间调用报 Connection timed out,日志里显示注册 IP 和实际访问目标对不上。
原因:Spring Cloud Alibaba 默认用 Pod 的容器 IP 做注册地址。如果微服务分布在多个命名空间,或者集群启用了 NetworkPolicy,Pod IP 之间不一定互通。还有一种情况是节点有多块网卡,自动探测拿到了错误的网卡地址。
解决:在 application.yml 里显式指定注册 IP,配合 downward API 注入 Pod IP:
spring: cloud: nacos: discovery: server-addr: ${SPRING_CLOUD_NACOS_DISCOVERY_SERVER_ADDR} ip: ${MY_POD_IP}Deployment 里补上下面的 env,Pod IP 就会注入到环境变量:
env: - name: MY_POD_IP valueFrom: fieldRef: fieldPath: status.podIPfieldRef.fieldPath: status.podIP是 K8s 标准做法,把 Pod 自己的 IP 写进环境变量。指定 ip 之后,注册到 Nacos 的地址一定是服务可达的。如果你用的是 Calico 这类网络插件,Pod IP 在集群内本来就互通,这个问题基本不会出现;跨命名空间访问时还是要靠 Service 域名而不是裸 Pod IP。
5.3 Vue2 前端刷新页面 404
现象:用户从首页跳转到子页面正常,按 F5 刷新就白屏,控制台显示 404。
原因:Nginx 没配try_files,拿着 URL 路径去静态目录找文件,找不到就返回 404,而 Vue2 的 history 模式路由根本没有对应的真实文件。
解决:在 Nginx 的 location / 里加上try_files $uri $uri/ /index.html;,完整写法见 4.3 节。注意别手抖写成try_files $uri $uri/ index.html;,也不能漏掉空格,Nginx 对路径解析非常敏感。改完配置记得重新构建镜像并推送,我见过有人本地改了配置忘了重新打包,排查半天才发现 Pod 里跑的还是旧镜像。
5.4 Nacos 2.0 的 gRPC 端口被挡
现象:Nacos 控制台能打开,SpringBoot 也显示注册成功,但客户端日志里反复出现 gRPC 连接失败的报错,配置中心的配置始终拉不下来。
原因:Nacos 2.0 的客户端通信走 9848 端口的 gRPC,不是 8848 的 HTTP。只放行 8848 的话,客户端无法建立长连接。更隐蔽的是,Nacos 服务端的 gRPC 端口会基于 8848 自动偏移计算,如果 8848 被别的进程占用导致服务端实际监听端口偏移,9848 也会跟着偏移,客户端用默认端口自然连不上。
解决:把 8848、9848、9849 三个端口在 Service、负载均衡、安全组三层全部放行。排查时在 Nacos Pod 里执行ss -lntp,看服务端实际监听的端口是不是 9848,别只信文档默认值。客户端配置仍然写 8848,服务端会协商到实际 gRPC 端口,只要 9848 可达就行。
6. 验证与调优:三条命令加一组资源参数,把部署闭环
6.1 三条命令验证完整链路
部署顺序建议是 Nacos → SpringBoot → 前端,每层验证通过再往下走。
kubectl get pods -n prod -o wide kubectl logs -f nacos-0 -n prod | tail -50 kubectl exec -it springboot-api-任意Pod名 -n prod -- curl "http://nacos-headless.prod.svc.cluster.local:8848/nacos/v1/ns/service/list?pageNo=1&pageSize=100"第一条看所有 Pod 的状态和重启次数,第二条确认 Nacos 节点日志里出现集群节点互相感知的记录,第三条从 SpringBoot Pod 内部请求 Nacos 的服务列表接口,验证注册链路真正打通——这一步能确认服务发现是通的,而不是只在控制台上看着正常。
6.2 资源限额:request 和 limit 必须分开设
稳定运行之后,重点是资源配额。只写 request 不写 limit,CPU 会被无限制抢占,流量一上来 Nacos 心跳超时,服务被误判为不健康。我最终的配置习惯是:
resources: requests: cpu: 500m memory: 1Gi limits: cpu: "2" memory: 2GiJVM 堆要小于 Pod 内存 limit,并留出元空间和堆外内存的余量——limit 2Gi 时堆最大给到 1.5G。另外,Nginx 的 worker_processes 默认是 auto,在 K8s 里可能拿到宿主机一样的核数,内存占用被放大,建议在 nginx.conf 里固定写成worker_processes 2;。
从那以后,我每次在托管 K8s 上部署一套新的微服务,都会强制走一遍同一个收尾动作:先看 Nacos 三个 Pod 是不是 Ready,再进 SpringBoot Pod 里 curl 一次服务列表,最后在前端页面刷新三次确认路由没有 404。这套动作五分钟能定位八成部署问题。希望帮到你。
本文还有配套的精品资源,点击获取