前后台分离项目,代码写得再漂亮,最后一步打包部署要是掉了链子,前面所有功夫全白费。尤其是前后台分离这套架构,前端、后端各自独立构建、独立部署,中间还夹着一个反向代理层,任何一个环节配置对不上,线上就是一片白屏加一串 502。这篇文章我把整个“前后台分离打包部署项目”从思路到落地完整拆一遍,覆盖前端 Vue 打包、后端 Spring Boot 构建、Nginx 路由与缓存配置、Docker 容器化编排这些核心环节,也会把我实际操作中踩过的坑一次说清楚。适合正在做前后台分离项目、准备把项目从本地环境迁到测试服务器或生产环境的人参考,后端是 Java Spring Boot、前端是 Vue 系的场景可以直接照搬思路,其他技术栈也能借方法论。
这里先说明白“前后台分离”在部署层面到底意味着什么。开发环境下,前端跑在 Node 热更新服务器上,后端跑在 Tomcat 内嵌端口上,两者通过代理各干各的,很爽。但生产环境根本没有 Node 进程,也没有 IDE,所有代码都必须被打成静态文件和可执行程序,再由 Web 服务器和进程管理器把它们拉起来。这个从“开发态”到“生产态”的转换,就是打包和部署的核心价值。所以与其说这是一个部署教程,不如说是一次完整的架构落地梳理。
1. 前后台分离部署的整体思路与方案选型
1.1 前后台分离架构下部署的真正难点
前后台分离部署,难的不是“打包”这个动作,而是“打包完之后怎么让它们协作”。开发时前端代理到后端的地址是 localhost:8080,后端连的数据库是本地 root 密码,CORS 全放开无所谓。但到了服务器上,前端静态文件要有人托管,后端进程要常驻且崩溃自动拉起,接口域名不能写死,数据库密码不能裸奔,跨域要么关掉走代理、要么精确配置。这背后是一整套部署规范的建立。
另外,前后台分离还带来了资源组织方式的变化。前端构建产物是一堆带 hash 的 JS/CSS 文件和 index.html,后端产物则是一个可执行 jar 包或 war 包。两者生命周期不同、更新频率不同、回滚策略不同,必须分开管理。很多人第一次部署时图省事,把前端打包好的 dist 直接塞进后端项目的 resources 目录一起发布,这种做法在小 demo 里能跑,但到了真实项目里,前端改一行文案后端就得重新构建发版,非常痛苦。所以从一开始就要明确:前端产物归前端,后端产物归后端,中间用 Nginx 或 CDN 衔接。
1.2 一套能落地的部署架构长什么样
一套经得起生产环境考验的前后台分离部署架构,至少包含这几个角色:前端静态资源服务器(Nginx)、后端应用服务(Spring Boot 内嵌 Tomcat)、反向代理层(同样是 Nginx,可以跟前端静态资源服务器合并)、数据库与中间件(MySQL、Redis)、以及可选的容器化编排层(Docker Compose 或 K8s)。
我实际项目中常用的是“Nginx 双职责”方案:同一个 Nginx 实例既托管前端静态文件,又把 /api 和 /prod-api 这类前缀的请求反向代理到后端服务。这样浏览器永远只跟同一个域名打交道,不存在跨域问题,Cookie 也能顺畅携带,同时运维只需要维护一个入口。如果你有独立域名和 HTTPS 证书,这个方案尤其舒服,证书只需要配一层。
架构确定之后,打包部署就变成了三个标准化步骤:前端构建、后端构建、配置 Nginx 并启动。下面我从环境准备讲起,逐步把每一步的操作细节和参数选择逻辑讲透。
2. 环境准备与项目结构调整
2.1 服务器端要准备哪些基础环境
不管项目多简单,服务器上这几样东西必须齐:JDK(版本与后端项目一致,比如 Java 8 或 Java 17)、Nginx、Git(用于拉取代码)、Docker 与 Docker Compose(如果走容器化)。数据库和 Redis 可以装在宿主机,也可以用 Docker 起,生产环境我建议独立安装或用云数据库,避免容器重启导致数据丢失。
环境版本这里有个常见坑:服务器上 JDK 版本与本地开发版本不一致,明明本地打包运行都正常,放到服务器上 jar 包启动直接报 UnsupportedClassVersionError。这是 JDK 主版本号不匹配导致的,比如本地用 JDK 17 编译的 class 文件,扔到 JDK 8 的运行时里必然跑不起来。所以条件允许时,尽量让本地构建机的 JDK 与服务器运行时 JDK 保持同一大版本。
Nginx 编译安装和 yum/apt 安装都行,但注意 Nginx 版本不要太老,至少 1.18 以上,对 HTTP/2、gzip 静态压缩的支持更完善。安装完先用 nginx -v 确认版本,再检查配置文件语法 nginx -t,避免后面写配置时基础环境出问题。
2.2 前端项目的打包前配置调整
前端项目里,最影响打包结果的就是publicPath、路由模式、接口请求地址这几个配置。Vue CLI 项目在vue.config.js里通过publicPath控制静态资源的引用基础路径,Vite 项目则是base配置。很多人的前端打包后白屏,就是publicPath写错了。如果你的站点部署在域名根路径下,publicPath可以配成'/';如果要部署在http://域名/xxx/这样的子路径下,就得设成'/xxx/'。否则页面加载时 index.html 引用 JS 和 CSS 的路径全部对不上,自然白屏。
路由模式这块,如果用了 Vite 或 Vue Router 的 HTML5 History 模式(createWebHistory),打包部署后访问非首页路径刷新会 404,因为服务器上根本不存在那个路径对应的物理文件。这个问题必须通过 Nginx 的 try_files 配置做回退,后面我会给出完整的 Nginx 配置片段。如果你不想处理这个问题,可以把路由模式改成 Hash 模式(createWebHashHistory),URL 会多一个 # 号,但部署上省心很多。
接口地址的配置要单独拎出来说。我见过太多项目把接口请求地址写死在代码里,比如axios.defaults.baseURL = 'http://localhost:8080/api',本地开发没问题,一打包到服务器,所有请求全打到本地回环地址上,直接废掉。正确处理方式是区分环境,生产环境走相对路径/api,由 Nginx 把/api反向代理到后端真实地址。这样前端产物里不包含任何服务器 IP 或域名,迁移环境时不用重新打包。
2.3 后端项目的打包前配置调整
后端项目打包前,最需要关注的是配置文件里的环境相关项。Spring Boot 项目通常有application.yml、application-dev.yml、application-prod.yml,打包前要确认spring.profiles.active设置成了生产环境。如果你有多套环境,强烈建议用 Maven profile 结合 Spring Boot profile 做联动,打包时通过-Pprod参数指定。
数据源信息、Redis 地址这些敏感配置,生产环境不要硬编码在源码里。常见做法是把配置外置,比如 jar 包同级的config/目录下放置application-prod.yml,或者通过启动参数--spring.config.location=file:/path/to/config/指定外部配置。Spring Boot 的应用优先级中,外部配置文件优先级高于 jar 包内部的配置文件,所以你不用重新打包就能修改生产环境配置,这非常实用。我的习惯是 jar 包里只放开发环境的默认配置,生产环境配置全部外置,这样打包一次可以部署到多台服务器,每台只需改自己的配置即可。
3. 前端打包实操:从 Vue 项目到 Nginx 可托管的静态资源
3.1 生产构建与开发构建的差异
开发时npm run dev启动的是开发服务器,带热更新、带源码映射,构建速度慢但调试方便。生产环境要的是npm run build,这个命令会做代码压缩、tree shaking、资源指纹(hash)等优化,输出到一个 dist 目录。Vue CLI 默认输出到项目根目录的dist文件夹,Vite 默认输出到项目根目录的dist文件夹。
构建命令跑完先别急着上传,先做三件事:检查 dist 目录结构是否完整(index.html、static 或 assets 目录、favicon 等)、打开本地预览确认页面渲染正常(可以用npx serve dist起一个本地静态服务)、检查控制台有没有资源加载 404 或接口请求报错。很多人这一步跳过,直接把 dist 丢到服务器上,结果白屏了也分不清是前端资源路径问题还是后端接口问题。
3.2 前端构建命令的常见选择与参数
如果你用的是 npm,那么生产构建命令一般是npm run build,但二次封装时可以加参数。Vite 项目构建时可以指定--outDir控制输出目录,也可以通过环境变量VITE_APP_ENV=production vite build让项目代码读取不同的环境变量文件。
构建阶段还需要留意依赖安装的问题。npm install与npm ci是有区别的:npm install会根据 package.json 的语义化版本范围安装依赖,可能装到新版本;npm ci严格按照 package-lock.json 锁定文件安装,保证构建环境的一致性。生产环境建议使用npm ci,并且把node_modules目录彻底删掉再装,避免本地残留的依赖影响构建结果。如果项目里有原生模块或者依赖了特定平台的二进制文件,构建机与服务器的操作系统不一致也可能引发问题,尽量保证构建环境纯净。
3.3 打包后布局异常之类的问题怎么排查
热词里“vue 打包后布局异常”是个高频问题。这类问题通常集中在几个原因:第一,CSS 文件没有被正确加载,页面样式完全丢失,这时打开控制台大概率是 CSS 文件 404,基本都是publicPath或base配置错了;第二,引入的第三方 UI 库样式顺序变了,打包后 CSS 被压缩合并,某些覆盖样式失效,解决方法是把自定义样式放到入口文件最后引入,或者开启 CSS 模块化避免全局污染;第三,字体文件和图片路径不对,构建后资源文件没有正确复制到 dist 目录,需要检查打包配置里的 asset 处理规则。
另外,如果前端项目用到了 Web Worker 或者动态引入(dynamic import)的模块,打包后这些异步加载的 chunk 也容易出现路径问题。排查思路很简单:F12 打开开发者工具,看 Network 面板里哪些请求 404 了,然后顺着 URL 反推构建配置里对应的路径设置,百分之八九十的问题都能通过 publicPath 修正解决。
3.4 Webpack 打包优化配置的小经验
如果项目体积大、首屏加载慢,可以针对 Webpack 做几个常规优化。第一是开启 gzip 压缩,Nginx 端开启gzip on,前端可以在打包时用 compression-webpack-plugin 预生成.gz文件,这样 Nginx 可以直接返回压缩包,减少服务器 CPU 开销。第二是代码分割,Webpack 的splitChunks配置可以把第三方库(vendor)与业务代码分离,让浏览器缓存更持久。第三是生产环境关闭 source map,productionSourceMap: false,否则会大大增加打包体积,也容易暴露源码。
这些优化不是必须的,但对于项目体量较大、用户分布广的场景,收益很明显。具体的配置参数需要根据项目实际情况调整,这里不展开每个插件的完整配置,重点是理解“预压缩 + 代码分割 + 关闭调试信息”这三个方向。
4. 后端打包实操:Spring Boot 项目从 IDEA 到可执行 jar
4.1 Maven 多模块项目的 package 流程
Spring Boot 后端项目如果是单模块,直接执行mvn clean package就好。但真实项目更多的是多模块结构,比如 parent 工程下包含 common、system、admin 等多个子模块。这种项目打包时要特别注意打包顺序:先安装被依赖的基础模块到本地仓库,再打包可执行模块。如果直接在最外层执行mvn clean package,Maven 会根据 reactor 顺序自动处理模块依赖,通常没问题;但如果某个模块依赖的是本地仓库里不存在的快照版本,就要先mvn install基础模块。
另一个常见报错是“intellij+maven项目打包报错”,表现形式五花八门:编译失败、测试失败、依赖找不到。编译失败先看具体的报错信息,很多时候是 Lombok 注解处理器与 JDK 版本不兼容导致的,需要检查 Lombok 版本是否支持当前 JDK。测试失败可以执行mvn clean package -DskipTests跳过测试,但建议先在本地跑一遍关键测试确认代码没大问题。依赖找不到则多半是私服地址没有配置,或者某个依赖没有从远程仓库拉取成功。
4.2 构建可执行 jar 的关键配置
Spring Boot 项目要打成可执行的 fat jar(包含内嵌 Tomcat 和所有依赖),必须在 pom.xml 里配置spring-boot-maven-plugin。这个插件默认在mvn package阶段会重新打包,生成的可执行 jar 和普通 jar 不同。配置里通常要指定mainClass作为启动入口。
另外,多环境配置可以结合 Maven profile 做资源过滤。比如在 pom.xml 里定义 prod 和 dev 两个 profile,每个 profile 对应不同的application-{profile}.yml,构建时用-Pprod指定激活哪个。这样打包出来的 jar 里已经包含了对应环境的默认配置,再配合外置配置覆盖,灵活性就很高。
4.3 IDEA 里直接构建 Docker 镜像
“idea 打包 docker镜像”这种需求,本质上是在构建阶段生成镜像,免去手动把 jar 包拷到服务器再写 Dockerfile 的操作。前提是你本机装了 Docker Desktop,并且在 IDEA 里配置了 Docker 插件连接本地 Docker 守护进程。
操作路径是:在 IDEA 右侧的 Maven 面板里,找到spring-boot:build-image这个 goal(Spring Boot 2.3+ 提供),双击执行,插件会自动根据项目配置生成一个可运行镜像。这套方案用的是 Cloud Native Buildpacks 技术,不需要编写 Dockerfile,但生成的镜像名字和标签需要你提前在 pom.xml 里通过image配置指定。如果你的项目对基础镜像、启动命令有特殊要求,还是建议自己写 Dockerfile 然后用命令行docker build -t 镜像名:标签 .构建。
用 IDEA 构建镜像确实方便,但我个人在实际项目中还是更倾向于在服务器上用 Dockerfile 构建,原因有两个:一是服务器构建出的镜像与运行时完全一致,避免本机与服务器架构差异(比如 Apple Silicon 的 arm64 镜像在 x86 服务器上有些基础镜像会有兼容性问题);二是 CI/CD 流水线拉代码到服务器后直接构建,流程更统一。
5. Nginx 配置与前后台联调
5.1 静态资源托管与 History 路由回退
前端 dist 上传到服务器后,Nginx 需要把它作为静态资源目录暴露出去。最简单配置如下:
server { listen 80; server_name your-domain.com; root /var/www/frontend/dist; index index.html; location / { try_files $uri $uri/ /index.html; } }这里最关键的就是try_files $uri $uri/ /index.html。它的作用是:当用户访问/about这个路径时,Nginx 先看有没有对应的实体文件$uri,没有再找目录$uri/,都找不到就回退到/index.html。这样 Vue Router 的 History 模式刷新页面就不会 404。如果你的前端项目用了 Hash 路由,这个配置可以省略,但我还是建议配置上,因为不排除有的页面通过 URL 直接访问。
5.2 反向代理解决跨域与接口转发
前端页面能正常打开了,接下来要让页面里的接口请求打到后端服务。假设前端请求统一带/api前缀,经过 Nginx 转发到 Spring Boot 服务的http://127.0.0.1:8080,配置如下:
location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $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_pass末尾的斜杠很关键。http://127.0.0.1:8080/带斜杠时,Nginx 会把请求 URI 中匹配到的/api/前缀去掉再转发,所以前端请求/api/user/list会变成后端/user/list。如果你不想去掉前缀,就把proxy_pass配成http://127.0.0.1:8080(不带斜杠),后端接收到的还是/api/user/list。这个规则容易混淆,每次配完最好用 curl 实测一下转发效果。
5.3 静态资源缓存策略与 gzip 压缩
前端静态资源带 hash 的文件名意味着内容变了文件名就变,非常适合长期缓存。可以在 Nginx 里针对这类文件做强缓存:
location /assets/ { expires 30d; add_header Cache-Control "public, immutable"; }而 index.html 不能被缓存,否则前端发版后用户浏览器还停留在旧页面。可以单独设置location = /index.html { add_header Cache-Control "no-cache"; }。
gzip 压缩同样在 Nginx 层开启即可:
gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss image/svg+xml; gzip_min_length 1k;如果前端构建时已经用插件预生成了.gz文件,可以配合gzip_static on直接返回预压缩文件,减少服务器端压缩的 CPU 开销。
5.4 多个不同结构的项目共用 Nginx 的问题
热词里有“nginx部署多个web项目”,这在实际工作中也很常见。如果一台服务器上要跑多个前后台分离项目,每个项目分配不同的 server_name 或不同的 location 前缀即可。用不同域名区分时,写多个 server 块;用同一域名不同路径区分时,在 location 层级做隔离。
后一种情况要注意:多个前端项目共用一个域名时,之前说的 History 路由回退try_files ... /index.html可能互相覆盖。比如项目 A 挂在/a/下,项目 B 挂在/b/下,配置必须写成location ^~ /a/ { alias /var/www/project-a/; try_files $uri $uri/ /a/index.html; }。这里的 alias 和 try_files 回退路径都要带上前缀,非常容易出错。我建议能用子域名区分就用子域名区分,配置简单,项目之间彻底隔离,排查问题也方便。
6. Docker 容器化部署与生产环境落地
6.1 后端 Dockerfile 编写与镜像构建
后端容器化的核心是写一个干净的 Dockerfile。我用得比较多的是多阶段构建,虽然对单模块项目稍显冗余,但能控制最终镜像体积。下面是一个简化的两阶段后端 Dockerfile:
# 构建阶段 FROM maven:3.8-openjdk-11 AS builder WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests # 运行阶段 FROM openjdk:11-jre-slim WORKDIR /app COPY --from=builder /build/target/demo.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod", "app.jar"]第一段负责构建,第二段只拷贝构建产物,镜像里不会残留 Maven 和源码,体积小很多。如果你的项目是纯单模块且构建时间不长,也可以把 Dockerfile 简化成只包含运行阶段,手动把 jar 拷进去:
FROM openjdk:11-jre-slim WORKDIR /app COPY demo.jar app.jar ENTRYPOINT ["java", "-jar", "app.jar"]这里有一个生产上必须注意的细节:容器内进程要以非 root 用户运行。可以在 Dockerfile 里创建专用用户,避免容器权限过大。Jenkins 或 GitLab CI 构建时,镜像标签建议用构建时间和 Git commit 组合,比如demo:20250518-3f2a9c1,这样回滚时可以精确定位到代码版本。
6.2 前端 Dockerfile 与 Nginx 基础镜像组合
前端容器化有两种思路:一种是把打包出来的 dist 目录挂载到宿主机 Nginx 上,Nginx 装在宿主机;另一种是前端也容器化,用 nginx 镜像承载静态资源。第二种更干净,迁移和扩展都方便。
前端容器化的典型做法是两个阶段:第一阶段用 node 镜像执行构建,第二阶段把 dist 目录复制到 nginx 镜像中,覆盖默认的/usr/share/nginx/html。同时把配置好的 nginx.conf 也复制进去:
# 构建阶段 FROM node:18-alpine AS builder WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci COPY . . RUN npm run build # 运行阶段 FROM nginx:1.24-alpine COPY --from=builder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]这样构建出的前端镜像自带 Nginx 配置,部署时只需要把后端地址通过环境变量传入即可。为了让后端地址不写死在 nginx.conf 里,可以在启动脚本里用 envsubst 替换模板变量,或者通过 docker-compose 的 environment 传递,再用自定义 entrypoint 渲染配置。这个技巧在微服务场景下非常有用,一个镜像可以部署到多个环境,配置由运行时注入。
6.3 docker-compose 一键编排前后台服务
当前台、后台都要容器化之后,用 docker-compose 编排是最有效率的方式。一个简单的docker-compose.yml示例如下:
version: '3.8' services: frontend: image: my-frontend:latest ports: - "80:80" depends_on: - backend restart: always backend: image: my-backend:latest ports: - "8080:8080" environment: SPRING_PROFILES_ACTIVE: prod DB_HOST: mysql REDIS_HOST: redis depends_on: - mysql - redis restart: always mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: demo volumes: - mysql-data:/var/lib/mysql restart: always redis: image: redis:7-alpine volumes: - redis-data:/data restart: always volumes: mysql-data: redis-data:这个编排文件里,数据库和 Redis 也给容器化了,数据通过 volume 持久化。生产环境如果数据库有备份需求、主从复制需求,建议单独管理数据库服务,不一定要放进 compose 里。compose 部署时先docker compose build构建镜像,再docker compose up -d启动服务,最后用docker compose ps查看所有服务状态。如果服务起不来,用docker compose logs -f查看日志排查。
关于 hot restart 的问题:compose 文件里的restart: always保证了服务器重启后容器自动拉起来,这是生产环境必须具备的。另外,当你更新了某张后端镜像后,执行docker compose up -d --build时,compose 会检测到镜像变化后重建容器,这个流程可以用来做简单版本的滚动发布。
6.4 Redis、MySQL 等依赖服务的容器化部署要点
Redis 用 Docker 部署非常简单,一个命令就能跑起来。但生产环境用 Docker 起 Redis 要注意:一定要挂载数据卷,否则容器删除后数据全部丢失;Redis 默认没有密码认证,如果端口暴露在外网,一定会被扫描爆破。建议通过--requirepass设置强密码,或者在 compose 文件里通过command: redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes配置。
MySQL 用 Docker 部署要特别注意字符集和时区问题。MySQL 8.0 默认字符集是 utf8mb4,一般够用,但如果业务上有特殊字符需求,需要在启动参数或配置文件里显式指定。时区问题建议在连接 URL 里显式指定serverTimezone=Asia/Shanghai,避免后端和数据库时区不一致导致时间数据错乱。数据卷挂载是必须的,同时定期做逻辑备份,不能只依赖 Docker volume,防止存储节点故障导致数据彻底丢失。
6.5 容器化部署的监控与日志
部署上去不代表就完事了。生产环境一定要有基本的监控和日志收集。最简单的方式是看 Docker 日志:docker compose logs -f --tail=100 backend。但这只适合登录服务器排查问题。如果项目规模再大一点,可以引入 Prometheus 监控和 Grafana 展示,把 Node Exporter、cAdvisor 等指标采集起来。这个方向我在另一个项目里专门整理过完整的监控方案,核心是用 Prometheus 抓取指标、Grafana 做可视化看板、Alertmanager 做告警通知。
日志方面,容器日志默认输出到 stdout,然后由 Docker 接管。如果要更精细地做日志分析,可以把日志挂载出来或用日志采集组件。对中小项目,我建议先把 Nginx access log 和 Spring Boot 日志目录挂载到宿主机固定目录,方便后续对接采集工具。
7. 常见问题与排查技巧实录
7.1 高频问题速查表
我把实战中遇到的高频问题整理成一张表,方便大家对照排查:
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 前端页面白屏,控制台 JS/CSS 404 | publicPath或base配置错误 | 打开 Network 看失败资源 URL,根据部署路径修正构建配置 |
| 页面刷新后 404 | Vue Router History 模式没有配置回退 | Nginx 加try_files $uri $uri/ /index.html; |
| 接口请求失败,报跨域错误 | 前端请求了不同源的后端地址 | 统一改用相对路径/api,由 Nginx 反向代理 |
| 后端启动报端口被占用 | 宿主机端口或容器端口冲突 | 用lsof -i:8080排查占用进程,修改端口映射 |
| 容器内连不上宿主机数据库 | 数据库地址写成了 localhost | 容器内 localhost 是容器自己,宿主机数据库要用宿主机局域网 IP 或 host 网络模式 |
| jar 包启动报 class 版本错误 | 构建 JDK 与运行 JDK 版本不一致 | 统一 JDK 大版本,重新打包 |
| Docker 构建时 Maven 下载依赖太慢 | 国内网络访问 Maven 中央仓库慢 | 配置阿里云等镜像加速,或在构建阶段挂载本机 maven 仓库缓存 |
| 前后端都启动后,前端仍显示 502 | Nginx 代理的后端地址不通 | 在服务器上 curl 后端接口地址,确认后端进程和端口正常 |
7.2 一套好用的排查思路
排查前后台分离部署问题,我习惯按照“链路排查法”从浏览器一步步往后端推进。第一步看浏览器 Network 面板,确认页面资源和接口请求的状态码。如果 HTML 都能加载、CSS/JS 200,但页面空白,优先怀疑 JS 运行时错误,看 Console 面板。如果接口 404,说明 Nginx 代理路径或后端接口路径不匹配;如果 502,说明 Nginx 无法连接到后端服务;如果 504,说明后端服务响应超时。确定问题出在 Nginx 或后端之后,直接上服务器 curl 验证。
比如后端接口 502,我先执行curl -v http://127.0.0.1:8080/api/test看后端是否正常返回。如果不通,看后端日志;如果通,再看 Nginx 转发配置。这套方法看起来基础,但在紧张的上线时间窗里,能最快把问题定位到具体环节。
还有个很容易被忽略的地方:前后台分离项目部署后,前端请求的接口必须与 Nginx 反向代理规则一一对应。如果前后端约定接口前缀是/prod-api,但前端代码里的 baseURL 写的是/api,即使 Nginx 只代理了/prod-api,请求也会全部 404。前端接口前缀、后端 context-path、Nginx location 三者必须构成同一条链路,这是部署前的前置检查项。
7.3 部署时环境变量与敏感信息的管理
这一节专门聊聊部署里的环境变量与敏感信息管理。很多人在生产环境踩的坑,都源于配置写死。比如数据库密码写死在application.yml里,项目开源或代码仓库被多人访问时,密码直接泄露。比较实用的做法是:本地开发用本地配置,生产环境通过环境变量注入敏感信息。Spring Boot 原生支持环境变量覆盖配置项,比如SPRING_DATASOURCE_PASSWORD这种环境变量名会自动映射到spring.datasource.password配置项。
Docker Compose 部署时,可以使用.env文件配合${VARIABLE}语法:
environment: SPRING_DATASOURCE_URL: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}?useUnicode=true&characterEncoding=utf8 SPRING_DATASOURCE_USERNAME: ${DB_USER} SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD}.env文件不要提交到版本控制,并在服务器上严格控制权限。如果项目对安全的敏感度高,也可以集成更专业的机密管理工具,但对绝大多数前后台分离项目而言,环境变量方案已经够用且容易落地。
再补充一个线上环境常见的细节:后端服务健康检查。Spring Boot 项目可以引入 Actuator,暴露/actuator/health端点,配合 docker-compose 的 healthcheck 配置。这样编排工具可以感知到后端是否就绪,避免前端服务已经起来了,后端还在启动中,导致依赖它的请求全部失败。前端容器也没有必要因为后端还没有就绪就反复重启,配好 depends_on 的条件即可按需等待。
7.4 常用的网络连通性排查工具
服务器上排查问题时,curl 和 telnet 这两个工具要熟练。curl可以测试接口是否通、返回什么状态码、响应头是否正确。telnet则用来检测端口连通性,比如telnet 192.168.1.10 3306检查 MySQL 端口是否可达。如果端口不通,检查防火墙、安全组规则、容器端口映射。
还有一个很实用的小技巧:Nginx 配置修改后,一定先执行nginx -t验证语法,再执行nginx -s reload热加载。千万不要直接重启 Nginx,否则配置里有语法错误,服务直接挂掉,影响所有正在运行的站点。热加载相比重启的优势在于平滑,不会中断现有连接。
8. 前后台分离部署的一条龙实战复盘
看完前面的分环节拆解,我再用一个完整小项目的流程做个串联复盘,这样大家对整体节奏会更有体感。假设项目是 Vue 3 + Spring Boot 2.7,代码托管在 Git 仓库,服务器是 Linux,域名暂用 IP。
第一步,在本地把前后端代码分别 build 一遍。前端执行npm run build,确认 dist 目录生成;后端执行mvn clean package -DskipTests,确认 jar 包生成。这一步能提前暴露绝大多数构建问题。
第二步,把前端 dist 目录上传到服务器指定目录,比如/opt/www/frontend/dist。后端 jar 包上传到/opt/app/backend。同时把生产环境外置配置文件也放好,比如/opt/app/backend/config/application-prod.yml。
第三步,配置 Nginx。把前面提到的静态托管、History 回退、/api反代三块配置写到/etc/nginx/conf.d/default.conf。配置好后执行nginx -t验证,然后nginx -s reload。
第四步,启动后端。可以用 nohup 直接启动,也可以用 systemd 管理,生产环境建议 systemd。命令大概是这样:
cd /opt/app/backend nohup java -jar -Dspring.profiles.active=prod demo.jar > app.log 2>&1 &启动后curl http://127.0.0.1:8080/api/health确认接口正常。
第五步,打开浏览器访问服务器公网 IP,如果一切正常,页面能打开、接口能通,部署即完成。如果异常,按前面第七节的链路排查法逐步定位。
整个过程如果熟练,半小时内就能完成一次手动部署。但这里面有一个漫长的沟:第一次部署时,前端 publicPath、Nginx 代理路径、后端外置配置这三者可能来回调好几次才能对上。所以我的建议是,先在测试环境完整走一遍这套流程,记录每一步的配置项,形成部署文档。以后每次发版,照着文档操作,效率和稳定性都会好很多。
9. 关于部署方式的几条真实建议
最后聊点我个人的体会。前后台分离项目的部署,方式没有绝对标准,关键看团队规模和发布频率。如果你是自己维护一个小项目,手动打包 + Nginx + nohup 完全够用,不需要为了“上 Docker”而上 Docker。但一旦有两个人以上协作、或者需要频繁发版,容器化几乎是必须的,因为它能保证“本地能跑,服务器就能跑”。
热词里有一句“无法将此项目用于本地聊天”,这让我联想到另一个容易踩的坑:很多看似能把项目快速跑起来的工具或框架,在本地 Demo 阶段表现完美,但要做成真正可部署、可交付的项目时,需要补齐的工程化细节远比想象多。前后台分离项目的部署也是这样:开发环境顺风顺水,真正落到打包部署这步,才是检验项目完整度的时刻。
再说说 CI/CD。前面讲的所有步骤,其实都可以自动化。前端提交代码后自动 build、自动上传服务器,后端提交后自动打包镜像、推送镜像仓库,服务器端通过 webhook 拉取最新版本再重启。这套流程做下来,发版从“手忙脚乱”变成“一键完成”。即使暂时不打算上完整的 CI/CD,我也建议把部署脚本写在仓库里,比如deploy.sh,把构建、上传、重启这些命令固化下来。这样新人接手也能照着跑,降低部署环节的知识门槛。
前后台分离打包部署这件事,本质上没有高深技术,但细节密度非常高。配置项多、链路长、层级多,任何一个环节的疏漏都可能让整个服务跑不起来。但只要把架构理清楚、把每一步的配置逻辑搞明白、把常见坑提前规避掉,这就是一项稳定可靠的日常操作。希望这篇文章能帮你在部署这条路上少踩几个坑,把宝贵的时间留给真正需要解决的问题。