Docker Compose 部署 Vue+SpringBoot 前后端分离项目
2026/9/18 1:57:32 网站建设 项目流程

1. 一套 Vue+SpringBoot 前后端分离项目,为什么非得用 Docker 跑

先说结论:如果你手上有 Vue 做前端、SpringBoot 做后端的前后端分离项目,并且你已经受够了"在我电脑上明明是好的"这句话,那 Docker 基本是你绕不过去的一站。我前后用 Docker 部署过七八套这类项目,从个人小工具到公司内部中台,踩过的坑能装满一个回收站文件夹。这篇就把我从零到一键启停的完整过程摊开讲——镜像怎么切、Compose 怎么写、Nginx 怎么配、接口 404 和跨域怎么排,全部给你落到能直接抄的粒度。

这篇文章适合三类人:刚学完 Vue 和 SpringBoot、准备第一次把项目部署到服务器的朋友;被环境依赖折磨过、想用容器统一运行环境的开发者;以及手里有一堆微服务、想用编排工具把启动流程压成一条命令的工程师。我不打算讲太多 Docker 的理论八股,重点放在"这套前后端分离项目落到容器里,每一步为什么这么选"。

先交代一下背景。前端 Vue 一般用 Vite 或 Vue CLI 构建,产物是一堆静态文件;后端 SpringBoot 打包出来是一个可执行 JAR,内部塞了 Tomcat。传统部署方式无非是:服务器上装个 Nginx 托管前端静态文件,再把 JAR 丢到服务器上用nohup java -jar跑起来。听起来简单,但真做起来,JDK 版本、Nginx 路径、字符集、时区、端口占用、开机自启,每一项都能耗掉你半天。而容器化的价值就在于:把这些环境变量和依赖,全部锁进镜像里,让"能跑"这件事变得可复制。

2. 动手前的整体设计:镜像怎么切、服务怎么连

在敲第一行 Dockerfile 之前,我建议你先花二十分钟把架构想清楚。很多新手上来就docker run一个 JAR,跑通了就以为万事大吉,结果前端往哪放、接口怎么代理、数据库怎么连,全都打成一团。这一步的规划质量,直接决定后面你要返工几次。

2.1 前后端分离项目在传统部署下的三个老大难

第一个难题是环境漂移。你本地是 JDK 17,服务器上是 JDK 8,SpringBoot 3.x 直接起不来,报的错还特别隐晦,往往是一串 class 版本号不匹配。前端也类似,Node 版本不同,npm install出来的依赖树能差出一整个 node_modules 地狱。

第二个难题是代理与静态资源的耦合。前后端分离项目里,前端页面是静态的,但发请求时要打到后端 API。开发阶段我们靠vue.config.js或 Vite 的server.proxy做代理,可一旦打包上线,这个代理就失效了,必须靠 Nginx 的location反向代理来接管。这一步配错,页面能打开但接口全红,非常常见。

第三个难题是启停与依赖顺序。后端要连数据库,数据库还没起来,SpringBoot 就疯狂重连失败然后退出。以前靠写一堆 shell 脚本sleep 30硬等,容器编排里如果不好好处理依赖,你会看到后端容器反复重启,日志刷屏。

这三个问题,Docker 加 Compose 组合起来恰好都能治:镜像锁环境,Nginx 容器管代理,Compose 管顺序和网络。这就是我坚持容器化的原因。

2.2 镜像拆分的三种思路与取舍对比

怎么把项目装进镜像,业内其实有几种流派,我列个表对比一下,你再决定用哪种。

方案做法优点缺点
单镜像全塞一个镜像里同时放 Nginx、静态文件、JAR、JDK只有一个容器,部署简单镜像巨大(500MB+),前后端耦合,改前端要重新构建整个镜像
双镜像分离前端一个 Nginx 镜像,后端一个 JRE 镜像职责清晰,各自独立构建,镜像小需要额外配 Nginx 反向代理和网络
后端镜像内托管前端把 Vue 产物拷进 SpringBoot 的 static 目录只需要一个容器前后端构建耦合,前端改一点就要重打后端包,热更新全无

我试过第一种和第三种。第一种镜像动辄六七百兆,推送镜像到仓库时等到怀疑人生;第三种确实是"一个 JAR 解决所有",但前端每次改动都要重新走一遍 Maven 打包,团队协作时非常难受。最后我固定用第二种:双镜像分离。前端交给 Nginx,后端只跑 JAR,两边通过 Docker 内部网络通信,Nginx 把/api前缀的请求转发到后端容器。

2.3 我最终落地的方案:双镜像 + Compose 编排

具体长这样:整台机器上跑两个容器加一个可选的数据库容器。web容器基于nginx:alpine,里面只有编译好的前端静态文件和一份 Nginx 配置;api容器基于eclipse-temurinopenjdk的 JRE 镜像,里面只有 JAR 和 JVM 参数。两个容器加入同一个自定义 bridge 网络,web通过服务名api就能访问到后端,不需要暴露后端的端口到宿主机。

这个设计的妙处在于网络隔离。后端端口完全不对外暴露,外部只能通过 Nginx 的 80 端口进来,安全性提升一截,也避免了端口冲突。你本地开发时后端跑 8080,服务器上根本不用管,容器内部自己通。下面几章我就按"先做后端镜像、再做前端镜像、最后 Compose 串起来"的顺序,逐块拆给你看。

3. 后端 SpringBoot 镜像:从 Maven 打包到容器启动

后端这块相对单纯,核心就是"怎么把 JAR 用最小体积跑起来"。但里面有几个细节,做不做,直接影响到镜像大小和启动速度。

3.1 打包方式与 JAR 分层

SpringBoot 项目用 Maven 或 Gradle 打出来的可执行 JAR,内部结构其实是"应用代码 + 依赖库 + 启动器"三层。SpringBoot 2.3 之后支持了layered JAR,也就是把依赖按变动频率拆成dependenciesspring-boot-loadersnapshot-dependenciesapplication四层。这么做的好处是:当你只改了业务代码,Docker 构建时可以复用前面几层缓存,镜像重建从几分钟压到几十秒。

如果你嫌配置分层麻烦,还有一个更省事的做法——多阶段构建,在容器里直接跑 Maven。我早期图省事就这么干,缺点是每次构建都要重新下载依赖,网速慢的时候能等到睡着。所以我现在推荐本地或 CI 先打 JAR,再进 Dockerfile 只负责运行,构建速度快得多。命令很普通:

mvn clean package -DskipTests

打完之后在target目录下会生成类似demo-0.0.1-SNAPSHOT.jar的文件。注意-DskipTests不是让你永远不跑测试,而是构建镜像时跳过,测试应该放在 CI 的独立阶段跑,别混在一起。

3.2 Dockerfile 逐行拆解

后端的 Dockerfile 我习惯放在项目根目录,内容就这么几行:

FROM eclipse-temurin:17-jre-jammy WORKDIR /app # 时区设置为东八区,避免日志时间差八小时 ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone COPY target/demo-0.0.1-SNAPSHOT.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "/app/app.jar"]

这里有几个点值得说。基础镜像我选eclipse-temurin的 JRE 版本而不是 JDK,因为运行阶段根本不需要编译器,JRE 能省下两百多兆。如果你项目用的是 SpringBoot 2.x 配 JDK 8,换成eclipse-temurin:8-jre就行;用 3.x 就上 17。时区那两行是血的教训,不加的话容器里默认 UTC,日志时间比北京时间早八小时,排查问题时能把人绕晕。

ENTRYPOINT用 exec 格式(中括号数组写法)而不是 shell 格式,是因为 shell 格式会把启动命令包一层/bin/sh -c,导致 JVM 收不到 SIGTERM 信号,docker stop时无法优雅停机,只能等超时被强杀,容易丢数据。这个细节很多教程不讲,但生产环境里很关键。

3.3 JVM、时区与健康检查的实战参数

镜像能跑之后,我通常会再补三样东西。第一是JVM 参数,容器内存有限,JVM 默认会按宿主机内存去分配堆,容易被 OOMKiller 干掉。现在推荐用容器感知的写法:

ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75.0", "-XX:+UseG1GC", "-jar", "/app/app.jar"]

MaxRAMPercentage=75.0表示堆最大用到容器内存的 75%,留 25% 给元空间、线程栈和直接内存。比写死-Xmx512m灵活,换台机器不用改。

第二是健康检查,让容器自己汇报状态。SpringBoot 接了 Actuator 之后有/actuator/health端点,可以在 Compose 里配healthcheck。第三是环境变量注入,数据库地址、密码这些东西绝不能写死在镜像里,一律通过 Compose 的environment.env文件传进去,SpringBoot 用${DB_HOST:localhost}这种占位符读取。这样同一份镜像,测试环境和生产环境可以跑出不同配置。

4. 前端 Vue 镜像:构建产物与 Nginx 托管

前端这一步比后端费脑子,因为坑大多藏在"打包配置"和"Nginx 路由"之间的接缝处。我见过太多人本地npm run dev一切正常,打成镜像后页面白屏或者刷新就 404。

4.1 Vue 打包阶段最容易踩的 base 与路由

第一个坑是publicPath / base。Vue CLI 里叫publicPath,Vite 里叫base。如果你部署在域名根路径下,保持默认/就行;但如果你挂在子路径比如/admin/下,就必须显式配成/admin/,否则打包出来的资源引用路径会错,页面直接白屏,控制台一堆 404。这是新手最高频的白屏原因。

第二个坑是路由模式。Vue Router 有 hash 和 history 两种模式。hash 模式(URL 带#)刷新不会 404,因为#后面的内容不发给服务器;history 模式好看,但刷新时浏览器会把整条路径发给 Nginx,Nginx 找不到这个文件就报 404。解决办法是在 Nginx 配置里加try_files回退到index.html,后面会写。

第三个坑是环境变量注入的时机。Vite 的import.meta.env.VITE_API_BASE是在构建时就写死进产物的,不是运行时。也就是说,你编译镜像时如果 API 地址写的是http://localhost:8080,那这个地址就永远固化在里面了,换个环境就得重新构建。想要运行时可变,得改成用window变量或者干脆让前端只发相对路径/api,由 Nginx 去代理。我强烈推荐后者,前端只认/api,所有环境差异交给 Nginx,一劳永逸。

4.2 多阶段 Dockerfile 实测写法

前端的 Dockerfile 我固定用多阶段:第一阶段用 Node 编译,第二阶段只把产物拷进 Nginx。这样最终镜像里没有 Node、没有源码、没有 node_modules,干净又小。

# 阶段一:构建 FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build # 阶段二:运行 FROM nginx: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;"]

注意几个细节。npm cinpm install更适合 CI 和构建环境,它会严格按package-lock.json装,保证每次依赖版本一致。把COPY package*.jsonCOPY . .分开写,是为了利用缓存——只要依赖没变,npm ci这层就命中缓存,改业务代码不会重新装依赖,构建时间能砍掉一大半。

COPY --from=builder是从上一个阶段把dist目录搬过来,最终镜像里看不到任何源码。如果你用的是 Vue CLI,产物目录是dist一般没错;Vite 也是dist;个别老项目可能是build,改一下路径即可。

4.3 Nginx 配置:history 回退、反向代理、缓存与 gzip

这份nginx.conf是整套方案里我认为最值得反复打磨的文件,它同时承担静态托管和接口代理两个职责:

server { listen 80; server_name _; root /usr/share/nginx/html; index index.html; # history 模式回退,解决刷新 404 location / { try_files $uri $uri/ /index.html; } # 静态资源长缓存 location ~* \.(js|css|png|jpg|svg|woff2)$ { expires 30d; add_header Cache-Control "public, immutable"; } # 接口代理到后端容器 location /api/ { proxy_pass http://api: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; } }

逐条说。try_files $uri $uri/ /index.html是 history 模式的救命配置,请求的文件不存在就交给index.html,由前端路由自己处理。静态资源那段按扩展名匹配,给 JS、CSS 加 30 天强缓存,但前提是你的构建工具会给文件加 hash 指纹(Vite 和 Vue CLI 默认都会),否则缓存会导致用户看不到更新。

proxy_pass http://api:8080/这里有个极其容易配错的斜杠问题proxy_pass结尾带斜杠,意思是把/api/这个前缀剥掉再转发;不带斜杠则原样转发。也就是说,如果前端请求/api/user/list,带斜杠时后端收到的是/user/list,不带斜杠后端收到的是/api/user/list。你的后端 Controller 映射路径是哪个,就得配哪个,配反了就是 404。这个斜杠我至少栽过三次。

5. Docker Compose 编排:一个命令把整套服务拉起来

有了两个镜像,最后一步是用 Compose 把它们串起来,让docker compose up -d一句话搞定全部。这一步的重点是网络、依赖和配置注入。

5.1 网络、依赖顺序与启动竞态

Compose 默认会创建一个 bridge 网络,同一个 compose 文件里的服务可以直接用服务名互访,这也是前面 Nginx 里能写http://api:8080的原因。你不用管容器 IP,Compose 帮你做了 DNS 解析。

依赖顺序靠depends_on,但要提醒一句:depends_on只保证容器"启动了",不保证里面的服务"就绪了"。比如你的后端要连 MySQL,depends_on只能保证 MySQL 容器先被拉起来,但 MySQL 初始化可能要十几秒,这期间后端连不上照样报错。正确的做法是配合healthcheckcondition: service_healthy,让 Compose 等数据库真正健康了再启动后端。

services: api: build: ./backend depends_on: mysql: condition: service_healthy environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/demo?useSSL=false&serverTimezone=Asia/Shanghai SPRING_DATASOURCE_USERNAME: root SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD} networks: - app_net web: build: ./frontend ports: - "80:80" depends_on: - api networks: - app_net mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} MYSQL_DATABASE: demo volumes: - mysql_data:/var/lib/mysql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] interval: 10s retries: 5 networks: - app_net volumes: mysql_data: networks: app_net: driver: bridge

5.2 数据卷、环境变量与配置注入

数据库这种有状态服务,一定要用 volume 持久化,否则docker compose down一执行,数据全没。上面mysql_data这个具名卷会把数据保存在宿主机上,删容器不删卷,重建后数据还在。要清库时才手动docker volume rm,危险操作,别手滑。

环境变量我放在同目录的.env文件里,比如DB_PASSWORD=xxxx,Compose 会自动读取。千万别把密码写进 docker-compose.yml 然后提交到 git,这属于经典事故。.env记得加进.gitignore。镜像里也一样,后端镜像本身不含任何密码,全靠运行时注入。

5.3 打包与启动的完整命令链

从零到跑起来,命令链大概是这样:

# 1. 打后端 JAR cd backend && mvn clean package -DskipTests && cd .. # 2. 前端不用手动构建,Dockerfile 里做了 # 3. 启动整套服务 docker compose up -d --build # 4. 看日志 docker compose logs -f api # 5. 下线 docker compose down

--build强制重新构建镜像,改了代码后加这个参数。日常只是重启就用docker compose restart。想看某个服务状态用docker compose ps。这一套命令下来,前端、后端、数据库就全起来了,浏览器访问宿主机 80 端口即可。

6. 联调排错实录:那些让你怀疑人生的坑

前面讲的是顺利路径,但真实世界里第一次跑通前的排错过程才是精华。这一章我把最常见的问题和排查思路整理出来,你可以当成一个速查手册。

6.1 跨域、404 与代理路径错位

页面能打开但接口全红,八成是代理没生效或者路径错位。排查顺序是这样的:先看浏览器 Network 里请求的实际 URL 是什么,是打到了http://localhost/api/...还是打到了别的域名。如果是后者,说明前端打包时 API 地址写死了,回到 4.1 节改成相对路径。如果 URL 对了但返回 404,就进web容器里看 Nginx 的错误日志:

docker compose exec web cat /var/log/nginx/error.log

如果日志里显示connect() failed (111: Connection refused),说明 Nginx 转发到api:8080连不上,重点查后端容器是否真的在监听 8080、是否在同一网络里。用docker compose exec web ping api能通就说明网络没问题。

跨域(CORS)这块,用 Nginx 反向代理之后其实是同源的,因为前端和接口都从同一个域名和端口进来,浏览器根本不会触发跨域检查。这也是我推荐代理方案而不是前端直连后端的原因之一。如果你还是遇到了跨域报错,多半是前端某个请求没走/api前缀,绕过了 Nginx 直连了后端,回头查那个请求的 URL。

6.2 Windows 上 Docker Desktop 起不来怎么办

我身边用 Windows 的朋友,十个有六个遇到过 Docker Desktop 启动失败,报错大意是"未检测到虚拟化支持"。这个问题跟项目本身无关,但会卡住你第一步。排查思路:先在 BIOS 里确认 CPU 虚拟化(VT-x / AMD-V)开着;然后在 Windows"启用或关闭功能"里确认 WSL2 或者 Hyper-V 已经启用;再检查是否装了和 Hyper-V 冲突的旧版虚拟机软件。用wsl --update更新一下 WSL 内核往往也能解决。顺带一提,如果你用的是新版 Docker Desktop,记得在设置里勾选"使用 WSL2 引擎",比老的 Hyper-V 模式快不少,文件挂载性能也好很多。

6.3 常见问题速查表

现象可能原因排查/解决
前端白屏,控制台资源 404base/publicPath 配错改成部署路径,重新构建
刷新页面 404history 模式无回退Nginx 加try_files ... /index.html
接口 404proxy_pass 斜杠问题对齐后端映射路径,注意尾部斜杠
接口 502后端容器未就绪或崩了docker compose logs api
日志时间差 8 小时容器时区为 UTC镜像设TZ=Asia/Shanghai
容器莫名被 kill内存超限被 OOM限制堆内存MaxRAMPercentage
docker stop很慢ENTRYPOINT 用了 shell 格式改成 exec 数组格式
数据库数据丢了没挂 volume加具名卷持久化

这张表我基本每次部署新项目都会对照一遍,能省掉大量重复排错的时间。

7. 上线之后还能怎么优化

跑通只是起点,真要长期维护,还有几件事值得做。镜像瘦身是第一件,后端用 JRE 不用 JDK,前端用 alpine 基础镜像,构建多阶段,一套下来能把镜像从 800MB 压到 200MB 以内,推拉速度天差地别。第二件是CI 集成,把docker builddocker compose up写进流水线,提交代码自动构建部署,人不用登服务器。

第三件是多架构镜像。如果你手里有 ARM 架构的服务器,或者在做国产化平台的适配,直接构建的 x86 镜像在上面跑不起来,需要用docker buildx构建linux/amd64linux/arm64双平台镜像。命令大概是这样:

docker buildx build --platform linux/amd64,linux/arm64 -t yourname/demo:latest --push .

第四件是日志与监控。容器日志默认存在宿主机上,越攒越多,记得在 Compose 里给每个服务配logging驱动限制单个日志文件大小和数量,不然磁盘迟早被撑爆。

最后分享一个我个人踩过最深的小坑:有次前端页面在容器里布局异常,本地却好好的,折腾半天才发现是 Nginx 把 CSS 当成了静态资源强缓存,而那次发版恰好没改文件名,用户拉到的还是旧样式。所以强缓存一定要配合文件 hash,构建工具开了 hash 之后再放心加 30 天过期。这类问题不看 Network 面板的响应头根本查不出来,多看一手原始响应,比瞎猜强太多。整套流程我用了两三年,从单机部署到多环境切换都很稳,你也照着搭一遍,基本就能把"环境不一致"这个老毛病彻底甩掉了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询