资讯动态

Docker部署Spring Boot + Vue前后端分离项目全攻略

发布时间:2026/10/21 7:58:53 来源:尧图企业网站定制
本地跑得好好的一部署就翻车这应该是很多人接触前后端分离项目后最头疼的事。尤其当项目涉及Spring Boot后端、Vue前端、外加MySQL数据库的时候环境问题简直能把人逼疯JDK版本不一致、Node版本对不上、数据库初始化脚本没跑、端口被占用……每一条都能让你在“部署”这一步卡上半天。后来我把这套组合整体迁到Docker上用容器把后端、前端、数据库各自的运行环境固化下来才算真正解决了“我这能跑、你那跑不起来”的老大难。这篇文章就围绕“Docker部署Spring Boot Vue项目”这套组合把我实际操作的完整过程、镜像构建思路、编排细节、以及几个坑的排查链路全部记录下来适合那些已经把项目在本地跑通、正准备往服务器上部署的开发者参考。我默认你已经了解Docker的基本命令build、run、ps、logs这些也知道Spring Boot和Vue项目的基本结构。如果你只是刚把环境装好建议先花半小时把官方快速入门过一遍再回来看这篇会顺畅很多。下面我直接从整套方案的价值判断开始讲然后是镜像构建、编排启动、生产细节最后附上我真实踩过的一个排查案例。1. 为什么推荐用Docker部署前后端分离项目不只是“省事”这么简单先说点实际的。以前部署Spring Boot Vue项目最典型的流程是这样的服务器上手动装JDK还得纠结是8还是11还是17、装Maven或Gradle、装Node.js版本还得和本地开发时一致、装Nginx、装MySQL然后手动打包、上传、改配置、重启。听起来每一步都不难但合在一起就是灾难——尤其是当你需要维护多台服务器或者隔了几个月要重新部署一次的时候你甚至记不清当初到底装的哪个版本。用Docker之后整个思路就变了你把运行环境直接“写”进镜像里服务器上只需要有一个Docker引擎剩下的全是复制粘贴。我最初从传统部署切到Docker部署核心动机有三个都不是为了赶时髦环境一致性问题从根本上消除。本地开发用的JDK 17、Node 18、MySQL 8镜像里是什么样服务器上就是什么样。不会再出现“本地好好的服务器上报ClassNotFoundException”这种问题。部署动作变得可重复、可脚本化。传统部署里你可能维护一份手写的部署文档但实际操作时总有人为偏差——漏了一步、路径写错。Docker把整个部署过程固化成了Dockerfile和compose文件版本管理进Git任何人拉下来执行一遍就得到完全一样的结果。迁移和扩容的成本极低。换服务器把镜像导出或推到镜像仓库新机器上拉下来启动就行。要横向扩展后端容器多起几个副本前面用负载均衡接着即可。当然这不代表Docker是万能的。如果你只有一个极简单的单体项目、服务器也就一台那手写脚本部署也许更直接。但只要项目包含两个以上组件或者你有“换机器重来”的预期容器化的收益就非常明显了。我这套方案最终的结构是这样拆的组件容器化方式对外暴露后端Spring Boot独立镜像多阶段构建仅内网端口8080不直接暴露给外部前端Vue构建产物打入Nginx镜像宿主机80端口由Nginx统一接管数据库MySQL官方镜像数据目录挂载到宿主机宿主机3306端口供后端连接这个拆分逻辑是外部流量只经过Nginx前端容器后端API通过Docker内部网络访问不直接暴露公网。这样在安全性和请求转发层面都更清晰。如果你没有独立的Nginx容器也可以理解成前端镜像里的那个Nginx同时承担了静态文件服务和API反向代理两个角色。2. 后端Spring Boot镜像构建多阶段构建才是关键2.1 基础镜像选型不要盲目追求“小而美”后端镜像的核心内容是JDK运行环境 打好的Jar包。最简单粗暴的写法是直接用openjdk:17-jdk这种完整镜像把Jar包COPY进去然后java -jar启动。但这样做出来的镜像体积很大而且运行时根本用不到编译器、调试工具这些内容。更科学的做法是多阶段构建multi-stage build第一阶段用带Maven和JDK的镜像完成编译打包第二阶段只拷贝Jar包到一个精简的运行时镜像里。我当前用的Dockerfile大致长这样# 第一阶段编译打包 FROM maven:3.8-openjdk-17 AS builder WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests # 第二阶段运行 FROM eclipse-temurin:17-jre WORKDIR /app COPY --frombuilder /build/target/demo-backend.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]这里有几个值得展开讲一下的点第一mvn dependency:go-offline这一步很重要。它的作用是先把所有依赖下载到本地Maven仓库后续真正的打包阶段就不会因为网络波动反复拉依赖了。配合Docker的层缓存机制只要pom.xml没变这一层缓存就一直有效重新构建镜像时速度会快非常多。第二运行时基础镜像我选的是eclipse-temurin:17-jre而不是某一个固定的厂商镜像。Temurin是目前比较活跃的OpenJDK发行版镜像长期维护安全性有保障。选JRE版本而不是JDK版本是因为我们运行阶段只要执行Java代码不需要再编译任何东西体积能省一部分。第三注意我用的是maven:3.8-openjdk-17作为构建阶段的镜像。如果你本地用的是Gradle就换成对应的Gradle镜像思路完全一致。2.2 实际构建时最容易踩的坑Jar包名字写死这个坑我至少见过三个人踩过。Spring Boot的Maven插件默认生成的Jar包名通常是项目名-版本号.jar比如demo-backend-0.0.1-SNAPSHOT.jar。如果你在Dockerfile里写死了这个文件名等哪天版本号升级了重新构建镜像时就会报“文件不存在”。我个人的习惯是在pom.xml里显式指定finalName让Jar包名固定build finalNamedemo-backend/finalName /build这样Dockerfile里始终写COPY --frombuilder /build/target/demo-backend.jar app.jar不管版本号怎么变构建都不会断。这个小习惯能让你的Dockerfile更健壮强烈建议养成本能反应。2.3 后端镜像要不要集成配置文件很多人会把application-prod.yml打进镜像但我更推荐的做法是**配置文件通过环境变量或挂载目录注入而不是打进镜像。**原因很简单镜像应该是“一份代码打包后的产物”而配置是“部署环境相关的内容”。你把配置打进镜像就失去了在不同环境间复用同一个镜像的能力。我在后面的编排章节里会详细讲怎么通过环境变量动态切换数据库地址、账号密码这些内容这里先记住一个原则尽量不要把任何和环境相关的东西写死进镜像。3. 前端Vue项目的镜像打包思路从构建产物到Nginx托管3.1 为什么要用“构建阶段 Nginx运行阶段”的结构Vue项目在开发时是源码形态有node_modules、有各种.vue文件但它并不能直接被浏览器执行。你需要先经过构建npm run build生成dist目录里面是一堆静态资源文件HTML、JS、CSS、图片等。这些静态文件需要一个Web服务器来托管所以前端镜像的典型做法就是第一阶段用Node镜像执行npm install和npm run build产出dist第二阶段用Nginx镜像把dist目录拷贝进去配置Nginx来处理静态资源和API代理对应的Dockerfile也贴在下面# 第一阶段构建Vue应用 FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm install --registryhttps://registry.npmjs.org COPY . . RUN npm run build # 第二阶段Nginx托管 FROM nginx:1.25-alpine COPY --frombuilder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD [nginx, -g, daemon off;]3.2 前端Nginx配置不只是托管静态文件这里有一个新手特别容易忽略的点Vue项目如果用了Vue Router的历史模式history模式那么用户直接访问/detail/123这种地址时Nginx会拿着这个路径去找对应的文件结果当然是404。解决方式是用Nginx的try_files指令把所有请求先尝试匹配静态文件匹配不到就回退到入口页面index.html交给前端路由去处理。我的nginx.conf大概长这样server { listen 80; server_name _; root /usr/share/nginx/html; index index.html; client_max_body_size 20M; # 前端路由回退 location / { try_files $uri $uri/ /index.html; } # API请求反向代理到后端容器 location /api/ { proxy_pass http://demo-backend: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; } }这里面的重点有两个。第一个是try_files $uri $uri/ /index.html;这行。它的逻辑是先按当前路径找真实文件找不到就按目录找再找不到就全部丢给index.html。这样前端路由在刷新页面时就不会白屏。第二个是location /api/这一段反向代理。注意我代理的目标地址写的是http://demo-backend:8080这里的demo-backend是后端容器在Docker内部网络里的主机名不是localhost。如果你写成localhostNginx容器会尝试连接它自己结果必然是连接失败。这个点在后续排查章节我会再带你走一遍完整的认知链路。顺带说一下client_max_body_size这个配置。如果你的项目里有文件上传功能默认的Nginx上传大小限制是1M超过就会报413。我习惯在一开始就调大到20M左右免得后面上线了被产品经理追着问“为什么文件传不上去”。3.3 前端镜像构建的缓存策略Vue项目构建慢最大的瓶颈往往在npm install这一步因为依赖包数量动辄几百个而且网络不稳定的话会特别折磨人。Docker的层缓存在这里能帮上大忙只要package.json和package-lock.json没有变化npm install那一层就不会重新执行。这也是为什么我强烈建议你把package*.json单独COPY进来先执行安装再COPY源码。一开始偷懒把所有文件一次性COPY进去等于每次改一行代码都要重新npm install构建一次十分钟脾气再好也会被磨没。如果项目用的是pnpm或yarn思路完全一致把包管理命令替换成对应的就行。4. 编排启动docker-compose如何让三套组件协同工作4.1 网络和依赖关系是编排的核心前后端和数据三个容器不能各跑各的它们需要彼此通信而且要保证启动顺序合理——后端启动时需要连接数据库如果数据库还没就绪后端会反复重试或直接退出。这些需求用单纯的docker run命令去管理会很痛苦所以使用docker-compose.yml做统一编排是我推荐的方案。我的compose配置大体如下version: 3.8 services: db: image: mysql:8.0 container_name: demo-db restart: always environment: MYSQL_ROOT_PASSWORD: demo_root_pwd MYSQL_DATABASE: demo_db MYSQL_USER: demo_user MYSQL_PASSWORD: demo_pwd ports: - 3306:3306 volumes: - ./mysql-data:/var/lib/mysql - ./init:/docker-entrypoint-initdb.d command: - --character-set-serverutf8mb4 - --collation-serverutf8mb4_unicode_ci backend: build: ./backend container_name: demo-backend restart: always depends_on: db: condition: service_healthy environment: SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/demo_db?useSSLfalseserverTimezoneAsia/Shanghai SPRING_DATASOURCE_USERNAME: demo_user SPRING_DATASOURCE_PASSWORD: demo_pwd ports: - 8080:8080 frontend: build: ./frontend container_name: demo-frontend restart: always depends_on: - backend ports: - 80:804.2 数据库的初始化脚本放在哪里MySQL官方镜像有个很贴心的机制首次启动时它会自动执行/docker-entrypoint-initdb.d目录下的所有.sql脚本按文件名顺序执行。这意味着你不需要手动进入容器执行初始化SQL把建表语句、初始数据脚本放到宿主机目录./init下并挂载进去数据库容器第一次启动时就会自动完成初始化。但有一个必须强调的点这个机制只在数据目录为空也就是首次初始化时生效。如果你的mysql-data目录已经有数据了再怎么改init里的脚本都不会重新执行。你需要手动进入容器执行或者把数据目录清空后再重新初始化。我在实际项目里就吃过这个亏改了初始化脚本重启无数次都没效果排查了半天才发现是数据卷里的老数据在“作祟”。4.3depends_on和健康检查别让顺序变成竞态depends_on控制的是容器的启动顺序但它不保证依赖的服务已经“可用”。比如depends_on: [db]只能保证db容器先启动不代表MySQL已经完成初始化、可以接受连接了。如果你的后端一旦连不上数据库就退出那么在db还没就绪时后端会直接启动失败。这里有两个解决思路。第一个思路是给后端容器加restart策略比如restart: always配合后端侧代码里的重连机制MySQL没起来就反复重启后端直到能连上。这个方案简单粗暴但说实话体验不好日志里会刷一堆连接失败的错误。第二个思路更优雅给db服务配置健康检查然后让后端依赖“健康状态”。上面配置里的condition: service_healthy就是这个意思。配合在db里加一段健康检查healthcheck: test: [CMD, mysqladmin, ping, -h, localhost, -udemo_user, -pdemo_pwd] interval: 5s timeout: 3s retries: 12这样compose会先启动db然后持续执行健康检查直到MySQL真正能接受连接后才启动backend。顺序问题从“猜”变成了“确定”明显更可靠。4.4 宿主机端口冲突一个很隐蔽的坑你还记得我上面把数据库的3306端口映射到了宿主机对吧如果你的服务器上本身也装了MySQL这个映射会直接失败报端口占用。所以上线前一定要确认宿主机上有没有其他进程占用了你要映射的端口。另外还有一个更微妙的问题你映射了3306到宿主机如果服务器有公网IP且安全组放行了3306相当于你的数据库直接暴露在公网上了。这非常危险。我的建议是如果不需要在宿主机上直接连数据库比如你只在容器内部连就把ports那一段去掉或者只映射到127.0.0.1写法是ports: - 127.0.0.1:3306:3306这样只有本机可以访问外部网络完全碰不到安全性提升一个档次。5. 生产环境不能忽略的几件事数据卷、日志和健康检查5.1 数据卷必须挂载否则容器一删数据就没了容器是“有状态”的克星。MySQL里的数据如果不挂载到宿主机一旦容器被删除比如升级镜像、重启Docker服务数据就跟着容器一起消失了。这是Docker部署里最闻名的“删库跑路”事故源泉但很多人第一次部署时根本不会意识到。挂载的方式我上面已经写在compose里了./mysql-data:/var/lib/mysql。这里我建议用相对路径相对于compose文件所在目录来挂载因为用绝对路径的话换一台服务器就要改配置可迁移性差。如果用的是云服务器最好同时考虑把数据目录放到独立的数据盘上避免系统盘出现问题导致数据丢失。5.2 容器日志的增长问题别再让日志吃满磁盘Docker默认会把容器的标准输出和标准错误都收进日志文件如果你不管它日志文件会一直增长。一个流量稍大的后端几天时间日志就能吃掉几个GB的磁盘空间。而磁盘满了之后最直接的表现就是容器写文件失败、服务无响应。我在生产环境里一般会在compose里对日志做滚动限制logging: driver: json-file options: max-size: 20m max-file: 5意思就是单个日志文件最多20MB最多保留5个文件超过就自动轮转删除最旧的。如果项目需要做集中式日志分析比如扩容到多副本的场景可以引入EFK这类方案但那是后话了单机部署阶段先把磁盘安全保住。5.3 给每个服务加健康检查监控的最后一公里编排文件里给db配置了健康检查但生产环境下我更建议给后端和前端也加上。后端的健康检查可以直接利用Spring Boot Actuator的/actuator/health端点healthcheck: test: [CMD, curl, -f, http://localhost:8080/actuator/health] interval: 10s timeout: 5s retries: 3注意一个问题eclipse-temurin:17-jre这个镜像里默认没有curl。你需要在Dockerfile里先安装它RUN apt-get update apt-get install -y curl \ rm -rf /var/lib/apt/lists/*或者用wget替代取决于你已经装了什么工具。健康检查的意义在于它不仅能让docker ps里显示容器是否健康更重要的是能被其它依赖它的服务感知状态变化也能被外部的监控系统定期探活。6. 一次真实踩坑容器能启动但前端“死活”调不通后端完整排查链路6.1 现象描述看似正常实则全断有次我把这套环境搭好后用docker compose up -d一次性启动所有容器三个容器都显示Up状态。本地访问前端页面也能打开但点击登录按钮后接口请求全部超时。打开浏览器F12Network面板里所有/api/...请求都是net::ERR_CONNECTION_REFUSED。6.2 排查第一步先确认后端容器内的服务是否正常我首先执行docker exec -it demo-backend bash curl http://localhost:8080/actuator/health结果返回了{status:UP}说明后端进程本身是健康的。这一下就排除了后端自身故障问题大概率出在“前端请求后端”这条链路上。6.3 排查第二步确认Nginx配置是否生效我进入前端容器检查配置docker exec -it demo-frontend bash cat /etc/nginx/conf.d/default.conf发现try_files和/api/代理配置都在。这时我开始怀疑是不是Nginx配置压根没被加载于是重新执行了nginx -t提示配置语法正常。问题依然存在说明问题不在这里。6.4 排查第三步验证容器间网络通信问题浮出水面我改用curl从前端容器内部直接请求后端docker exec -it demo-frontend curl http://demo-backend:8080/actuator/health结果也是一样超时。到这一步基本可以确定两个容器之间根本不通。接着我查看网络情况发现一个被我忽略的事实——两个容器不在同一个自定义网络里。原因是我最开始是分两次用docker run启动的后来才迁移到compose但旧的容器还在使用默认网络或者网络配置有冲突。compose默认会为项目创建一个独立的网络所有在compose里定义的服务都会加入这个网络但如果你手动创建过容器或者有其它历史容器网络隔离问题就会很隐蔽地出现。解决办法是在compose里显式声明自定义网络并让所有服务使用同一个网络networks: app-network: driver: bridge services: db: ... networks: - app-network backend: ... networks: - app-network frontend: ... networks: - app-network重新执行docker compose down docker compose up -d再从前端容器内部curl后端通了。6.5 排查第四步问题重新出现这次指向Nginx配置解析网络修好后我再次刷新页面发现请求不再报连接拒绝但所有API返回502 Bad Gateway。这说明Nginx确实把请求转发到了后端但后端没有正确响应。我又开始怀疑是不是demo-backend这个主机名解析不到于是从前端容器里ping了一下docker exec -it demo-frontend ping demo-backend结果能ping通。那问题就只剩一个Nginx的proxy_pass写法。我当时的配置是location /api/ { proxy_pass http://demo-backend:8080/; }注意这个坑在于proxy_passURL末尾的斜杠。当时我写的代理地址带上了/Nginx在转发时会去掉匹配前缀/api/然后把剩下的路径拼到代理地址后面。也就是说请求/api/user/login会变成http://demo-backend:8080/user/login而后端Controller的路由一般还是挂在/api下自然就匹配不到路由了。两种写法的区别我整理成了一张表方便你对照proxy_pass写法代理结果请求 /api/user/login是否保留/api前缀http://demo-backend:8080/api/user/login保留http://demo-backend:8080//user/login去掉我改回不带斜杠的写法重新加载Nginx配置请求立刻恢复正常。6.6 这次踩坑留下的三条经验第一条容器状态Up不等于服务已经通了。排查前后端联调问题永远先走“容器内curl”这条链路从后端自身健康、容器间连通性、Nginx转发规则三个层面逐个验证比直接刷新页面看浏览器报错要高效得多。第二条Docker网络永远用自定义网络不要靠默认行为。如果你有过手动docker run和compose混用的历史网络配置很容易变得难以追踪。显式声明网络不仅让容器间通信可预期也让整个配置表意更清晰。第三条Nginx的proxy_pass写法差异是高频坑。转发时带不带结尾斜杠行为截然不同。如果你发现前端页面能打开但接口全是502或404优先检查这里。7. 让部署过程更顺滑的几条实战建议到这一步一套能跑的Docker化前后端分离项目已经成型了。我再补几个实际操作中总结出来的习惯未必所有人都会提到但确实让我的部署体验提升了不少。第一条给镜像加版本标签不要永远用latest。正式部署时我习惯打上日期或构建号比如demo-backend:20250601这样镜像有变更时可以随时回滚到上一个版本。latest在开发方便但在生产环境里它会让“回滚”变得不可控。第二条把.env文件纳入.gitignore但保留.env.example。compose里的数据库密码、环境变量这些敏感信息不要直接写死在docker-compose.yml里提交到代码仓库。Docker Compose会默认读取同目录下的.env文件并自动替换变量所以可以把配置改成${MYSQL_ROOT_PASSWORD}这种形式然后维护一个不含真实密码的.env.example模板供团队成员参考真实环境各自填写各自的.env。第三条每次变更部署前先本地用同样的镜像跑一遍。我见过很多人在服务器上直接改配置、直接重新build一旦失败排错成本极高。先在本地或测试环境用同一套compose文件拉起来验证一遍确认没问题再推到服务器能省掉一大半故障时间。第四条尽量保持Dockerfile和compose文件的简洁。有些项目喜欢在Dockerfile里堆很多层操作每多一个RUN就会多出一层镜像、多一分缓存失效的风险。能用一条命令完成的事情尽量合并成一条。同时把容易变化的指令比如COPY源码放在后面把不容易变化的指令比如安装依赖放在前面充分发挥层缓存。根据我个人的实践体验Docker部署这套Spring Boot Vue项目最大的价值不在于“不用装环境”这一个点而在于它把整个部署过程从“一门手艺”变成了“一份可复现的配置”。团队新成员拿到仓库只要装了Docker一条命令就能把整个项目跑起来不再需要“求教老员工部署步骤”这种环节。不同的项目结构可能还需要你在Nginx配置、容器网络、数据备份上做更多调整但基本框架和排查思路是通用的。希望这篇能帮你少走几步弯路把更多精力留到业务本身。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑