news 2026/10/10 9:38:43

Docker部署Spring Boot+Vue前后端分离项目的完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Docker部署Spring Boot+Vue前后端分离项目的完整指南

上两周刚把一个前后端分离的项目完整搬到 Docker 上,Spring Boot 做后端接口,Vue 做管理端页面,从本地开发环境到服务器一键部署,整个过程折腾了差不多三天。这中间踩了不少坑,有的坑网上资料说得含糊,有的坑是版本更新之后才出现的,趁着记忆还热乎,我把整套思路和操作细节整理出来,需要的可以直接照着抄。

1. 部署前需要理清的设计思路

1.1 为什么把项目拆成三个容器而不是直接扔进一台机器

很多人第一次用 Docker 部署前后端项目,容易陷入一个误区:把所有东西塞进一个容器里,或者干脆只把 Jar 包放进去,前端文件还是手动拷贝到 Nginx 里。这样做短期能跑,但后续维护会发现很难受——前端改一行代码要重新登服务器替换静态文件,后端升级版本还要担心影响前端环境。

我用的方案是拆成三个容器:后端容器跑 Java 服务,前端容器用 Nginx 承载 Vue 打包后的静态文件,再加上 MySQL 数据库容器。三者的关系是:前端通过 HTTP 请求访问后端接口,后端连接数据库读写数据,对外只暴露前端和数据库的端口。

这样做的好处有几个。第一是环境隔离,Java 的 JDK 版本、Nginx 的配置、MySQL 的数据存储互不干扰,不会出现“升级 Node 导致 Nginx 配置不见了”这种破事。第二是迁移方便,整套环境写在 docker-compose 文件里,换一台服务器只需要装好 Docker,一条命令就能拉起全部服务。第三是回滚容易,镜像就是存档版本,出问题直接把镜像 tag 切回上一个,验证过的历史版本随时能用。

1.2 项目目录结构与镜像规划

在写 Dockerfile 之前,我先规划了目录结构,建议你也先做这一步,别急着写配置。

project-root/ ├── backend/ # Spring Boot 项目 │ ├── Dockerfile │ └── target/xxx.jar ├── frontend/ # Vue 项目 │ ├── Dockerfile │ ├── nginx.conf │ ├── dist/ # npm run build 产物 │ └── src/ └── docker-compose.yml

后端和前端各自维护一套 Dockerfile,最外层根据数据库配置额外添加 MySQL 服务。需要注意一点:Jar 文件是通过 Maven 构建出来的,前端 dist 是通过 npm 构建出来的,但我在写镜像的时候用了多阶段构建,也就是直接在 Dockerfile 里完成编译打包,不用先在宿主机上装一遍 Maven 和 Node。后面的 Dockerfile 部分我会展开细说。

1.3 部署前的自检清单

动手写配置之前,先在本地或者测试环境确认这几件事:

  • Spring Boot 项目是否用了 Maven 或 Gradle 构建,能在命令行里完整打包生成可执行 Jar。
  • Vue 项目执行npm run build能正常产出 dist 目录,并且开发环境下的 API 请求地址是相对路径或者可以通过环境变量覆盖。
  • 后端数据库连接信息没有硬编码在代码里,而是通过环境变量读取,方便容器启动时传入。
  • 确认服务器的端口规划:前端端口、后端端口、数据库端口不能和已有服务冲突。

前两项是打包的基础,后两项是容器化改造的重头戏。如果你的项目目前是直接写死数据库地址的,建议先改成配置项,容器里再通过 Environment 传入。

2. 后端容器化:Spring Boot 的 Dockerfile 编写

2.1 基础镜像选择:别只用 openjdk 不带版本

Spring Boot 的 Dockerfile 第一行就是选择基础镜像,这一步很容易翻车。很多人习惯写FROM openjdk:latest,图省事。但在生产环境里latest是个坑——它指向的 JDK 版本可能跟你本地开发环境不一致,导致编译出的 class 文件无法运行。

我现在用的是结合了 Maven 和 Java 运行时的多阶段方案。第一阶段用带 Maven 的镜像编译打包,第二阶段用精简的 JRE 镜像运行。这样最终镜像体积小,里面也不含编译工具,安全性和启动速度都好得多。

一个实际例子,我的后端项目基于 Java 17 和 Spring Boot 3.x,Dockerfile 长这样:

# 第一阶段:编译打包 FROM maven:3.9.4-eclipse-temurin-17 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests # 第二阶段:运行 FROM eclipse-temurin:17-jre WORKDIR /app COPY --from=builder /app/target/*.jar app.jar EXPOSE 8080 ENV TZ=Asia/Shanghai ENTRYPOINT ["java", "-jar", "app.jar"]

2.2 多阶段构建解决了什么问题

多阶段构建最直接的价值是控制镜像大小。如果只用单个 Maven 镜像跑完所有步骤,最终镜像里会残留 maven 仓库、源码、编译工具链,体积轻则几百 MB,重则一个多 G。而上面的写法里,最终运行的镜像只包含 JRE 和打好的 Jar,体积通常在 200 MB 左右。

还有一个隐藏好处是依赖下载缓存。mvn dependency:go-offline会在构建阶段先拉取所有依赖,存放到镜像层里。只要 pom.xml 没变,这一层的缓存就不会失效,后续构建几分钟内就能完成。

2.3 构建与运行细节:时区、参数、健康检查

Spring Boot 项目打包好之后,运行阶段有几个经常被忽略的细节。

时区问题。如果不设置环境变量,容器的默认时区是 UTC,和国内的时间差八个小时。数据库里存的时间和日志时间都会错乱。我在 Dockerfile 里加了ENV TZ=Asia/Shanghai,但更稳妥的办法是在启动时挂载宿主机时区文件,或者直接依赖基础镜像的 tzdata 包。上面的写法在常用基础镜像里实测没问题,如果你的镜像里没有 tzdata,需要先apt-get install -y tzdata。

JVM 参数。容器环境下 JVM 的默认堆内存行为可能会让进程吃满宿主机内存。建议在启动命令里显式限制内存:

ENTRYPOINT ["java", "-Xms256m", "-Xmx512m", "-jar", "app.jar"]

如果服务器内存紧张,这个配置能避免容器耗尽宿主机资源导致 Docker 守护进程被拉死。当然具体的堆内存大小要根据你项目的实际负载来调。

还有一个容易被忽略的点是健康检查。Spring Boot Actuator 自带health端点,可以在 Dockerfile 里配置 HEALTHCHECK:

HEALTHCHECK --interval=30s --timeout=3s --retries=3 \ CMD curl -fs http://localhost:8080/actuator/health || exit 1

但要注意基础镜像里不一定带 curl,有些精简 JRE 镜像里没有。这时候需要在构建阶段把 curl 装进去,或者直接用 Java 写一个简单检查。我一般选择在基础镜像里补装 curl,代价是体积略增,但可排查性和可运维性都上来了。

2.4 构建镜像的命令与测试

写好后端 Dockerfile,执行构建:

docker build -t backend:1.0.0 ./backend

构建完成后先不急着编排,用单独试跑的模式验证容器能不能正常启动:

docker run -d --name backend-test -p 8080:8080 backend:1.0.0

然后检查容器日志是否出现了 Spring Boot 的启动成功标志,再访问一下http://localhost:8080/actuator/health确认状态 UP。这一步在开发机上验证完,后面编排时心里就踏实了。

3. 前端容器化:Vue 项目如何构建镜像

3.1 前端构建的两种思路对比

前端和纯后端服务不太一样,Vue 项目本身没有运行时的服务进程,它是一堆静态文件,通过 Nginx 对外提供 HTTP 服务。构建思路无非两种。

一种是先在宿主机上执行npm run build,把 dist 目录拷进镜像里。这种方式适合本机已经装了 Node,但换到其他环境时不够通用。

另一种是我推荐的方式:用 Node 镜像在构建阶段执行打包,然后只把 dist 产物拷贝到 Nginx 镜像里。这样宿主机完全不需要安装 Node,任何一台装了 Docker 的机器都能完成构建。前端 Dockerfile 如下:

# 第一阶段:构建 Vue 静态资源 FROM node:18-alpine AS builder WORKDIR /app COPY package.json . RUN npm install COPY . . RUN npm run build # 第二阶段:运行 Nginx 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

3.2 前端路由 history 模式与 Nginx 配置

这地方的坑是我认为整个项目里最容易踩的,得单独说。Vue Router 如果用的是createWebHistory模式,页面地址长这样http://xxx.com/system/user,不带#。这种模式下,当你直接访问一个子路由,比如刷新页面或者直接输入 URL,浏览器会向服务器请求/system/user这个路径,但服务器上根本没有这个文件,Nginx 会返回 404。

解决方法是给 Nginx 配置try_files,让所有找不到的路径都回退到index.html,由前端路由接管接下来的路由解析。

server { listen 80; server_name localhost; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://backend: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; } }

这里location /api/是反向代理规则。前端代码里请求接口的地址写成/api/...,Nginx 接收到这个路径后,会转发给后端容器。proxy_pass http://backend:8080/api/;里的backend是 docker-compose 里的服务名,容器网络内可以直接通过服务名访问,后面编排部分会解释为什么这么写。

3.3 运行时环境变量处理:别把打包当唯一途径

如果你的前端代码里有VUE_APP_API_BASE_URL之类的环境变量,并且这个变量在开发环境和测试环境指向不同的接口地址,那你需要注意:npm run build的过程会把环境变量编译进 JavaScript 文件里。换句话说,一旦构建完成,镜像里的接口地址就固定了,无法通过容器运行时的环境变量覆盖。

这种“构建时注入”的方式在开发环境没问题,但到了部署阶段就很麻烦。比如同一个镜像要同时部署到测试环境和生产环境,测试环境的接口是http://test-api.xxx.com,生产环境是http://api.xxx.com,你就必须分别构建两个镜像。

我用的方案是运行时动态注入。在 Nginx 镜像的启动阶段执行一个脚本,把环境变量写到/usr/share/nginx/html/config.js里,然后在 HTML 里引入这个文件,前端代码从window.SITE_CONFIG.apiBaseUrl读取接口地址。这样同一个镜像通过传入不同的环境变量,就能适配不同环境。

具体做法是加一个 entrypoint.sh:

#!/bin/sh cat > /usr/share/nginx/html/config.js << EOF window.SITE_CONFIG = { apiBaseUrl: "${API_BASE_URL}" } EOF nginx -g "daemon off;"

在 Dockerfile 里替换 Nginx 默认启动命令:

COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"]

前端代码里这样读取:

const apiBaseUrl = window.SITE_CONFIG?.apiBaseUrl || '/api/';

这样构建出的镜像可以一套打天下,每次部署只是改环境变量,不用重新构建。对于需要频繁发布测试版和正式版的情况,这个方案能省不少时间。

4. 使用 docker-compose 一键编排整套服务

4.1 docker-compose 文件的基础结构

前端和后端镜像都构建好了,接下来要把数据库也拉进来,一起受 docker-compose 的编排管理。先放一个实际能用的 docker-compose.yml:

version: "3.8" services: mysql: image: mysql:8.0 container_name: project-mysql restart: always environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: project_db MYSQL_USER: appuser MYSQL_PASSWORD: app123456 volumes: - ./mysql-data:/var/lib/mysql ports: - "3306:3306" networks: - app-net backend: build: ./backend image: backend:1.0.0 container_name: project-backend restart: always depends_on: - mysql environment: SPRING_PROFILES_ACTIVE: docker DB_HOST: mysql DB_PORT: 3306 DB_NAME: project_db DB_USER: appuser DB_PASSWORD: app123456 ports: - "8080:8080" networks: - app-net frontend: build: ./frontend image: frontend:1.0.0 container_name: project-frontend restart: always depends_on: - backend environment: API_BASE_URL: /api/ ports: - "80:80" networks: - app-net networks: app-net: driver: bridge

4.2 服务名与容器网络的关键作用

docker-compose 会给每个服务创建一个可通过服务名访问的网络别名。比如 backend 容器在 app-net 网络里,它的主机名就是backend。所以前端 Nginx 配置里写proxy_pass http://backend:8080/api/;,就能直接访问到后端服务,不需要关心 IP 地址。

这一点在生产环境特别重要。容器每次启动分配到的 IP 是动态的,直接写死 IP 会在重启后失效。通过服务名通讯,容器怎么重启、IP 怎么变化,都不用修改配置。

depends_on控制启动顺序。需要注意它只保证先启动数据库容器,不代表数据库已经初始化完成。实际部署中我遇到过一个情况:MySQL 还在初始化时,Spring Boot 已经启动,连接数据库失败导致整个启动流程崩溃。这个问题有两种处理方式。一是给后端容器加restart: always,MySQL 初始化完成后后端会自动重启连接,简单粗暴但有效。二是写一个等待脚本,在容器启动前循环检查数据库端口是否就绪。两种我都试过,等待脚本更可控,但脚本本身的健壮性也要测试,所以我一般先用restart: always,等系统平稳运行后再考虑要不要加脚本。

4.3 数据库数据持久化:容器能删,数据不能丢

MySQL 容器有一个大坑:如果容器被删除,而数据没有挂载到宿主机,那么整个数据库就没了。docker-compose 里我用了一个数据卷挂载:

volumes: - ./mysql-data:/var/lib/mysql

这样 MySQL 的数据文件会存储到宿主机项目的mysql-data目录下。即使执行docker-compose down把容器都删掉,下次重新启动docker-compose up -d时,数据仍然在。对于生产环境,这个挂载目录最好放到独立的数据盘或者使用 Docker Volume 卷。

4.4 镜像管理:给镜像打 tag 并推送到仓库

本地构建的镜像只存在于当前机器,如果要把项目部署到远程服务器,有几种方式。最简单的做法是直接在服务器上放源码并执行docker-compose up -d --build,让服务器自己构建。这种方式适合测试环境,但每次发布都要把源码传上去,项目大时很耗时。

更规范的做法是构建完成后把镜像推送到镜像仓库,服务器只负责拉取。构建时打好标签:

docker tag backend:1.0.0 registry.example.com/project/backend:1.0.0 docker push registry.example.com/project/backend:1.0.0

在服务器上执行:

docker pull registry.example.com/project/backend:1.0.0 docker-compose up -d

这在多人协作或需要多台服务器部署时尤其重要。镜像仓库就是存档,每一个版本都是可回滚的中转点,比每次临时打 tar 包靠谱得多。

4.5 常用编排命令速查

这里整理几个我每次部署都会用到的命令:

# 构建镜像并后台启动全部服务 docker-compose up -d --build # 查看服务状态 docker-compose ps # 查看某个服务的日志 docker-compose logs -f backend # 重新启动某个服务 docker-compose restart frontend # 停止并移除容器(不会删数据卷) docker-compose down # 彻底停止并移除容器、网络和匿名卷 docker-compose down -v

down -v要谨慎使用,它会删除挂载到容器匿名卷的数据。我因为手误执行过一次,差点把数据库测试数据清空,好在那只是开发环境。生产环境务必确保所有数据都挂载到了宿主机路径或者命名卷,否则千万别加-v。

5. 踩坑记录与排查技巧实录

5.1 镜像拉取超时与构建慢的问题

使用默认源拉取基础镜像时常会遇到超时或速度极慢,这个问题在国内环境尤其明显。解决办法是配置镜像加速地址,在 Docker 守护进程的配置文件中加入 registry-mirrors 配置。

如果你所在的网络环境无法直接访问公共仓库,也可以在构建阶段配置镜像源。前端 Node 镜像的npm install速度慢时,可以把 npm 源切到国内镜像,在 Dockerfile 里加一行:

RUN npm config set registry https://registry.npmmirror.example.com

Maven 同理,在 settings.xml 或构建命令里指定镜像地址。这一步能有效缩短构建时间,尤其是首次构建需要拉取大量依赖时,差距非常明显。

5.2 容器里访问不到宿主机服务

有一种场景是后端需要连接数据库,但数据库不在容器里而在宿主机上。这时候 Spring Boot 配置里如果写localhost:3306,连接的一定是容器自身的 3306 端口,肯定连不上宿主机。

解决方法是把数据库主机地址写成宿主机在 Docker 网络里的 IP。Docker 的 bridge 网络模式下,宿主机地址通常是172.17.0.1。更推荐的做法是使用host.docker.internal这个特殊域名,它在 Linux 上需要额外参数支持,在 Docker Desktop for Mac/Windows 上是内置的。

我实际验证下来,最省心的方案还是把所有依赖服务都纳入 docker-compose 统一管理,这样直接用服务名即可完成互通,不需要关心 IP 和网络模式。

5.3 前端刷新页面返回 404

刚才在 Nginx 配置里提过这个问题,但在实际部署时还是会有人漏掉。如果你已经配了try_files $uri $uri/ /index.html;但仍然 404,排查几个点。

第一,确认 Nginx 的root路径是否正确。如果镜像里静态文件在/usr/share/nginx/html,而配置里写成/var/www/html,那所有文件都会 404。

第二,确认配置生效了。有的基础镜像已经在/etc/nginx/nginx.conf或/etc/nginx/conf.d/default.conf里有一套默认配置,你新拷贝的配置可能被覆盖或冲突。执行docker exec进入容器查看当前生效的配置内容,逐行确认。

第三,检查防火墙或安全组规则。如果是在云服务器上部署,80 端口需要放行,否则外部访问不了。

5.4 后端容器启动失败,反复重启

这类问题一般集中在依赖资源和资源耗尽两个方面。数据库连接不上的原因可能是数据库服务还没就绪,或者连接地址写错。JVM 内存设置过大但容器可用内存不足,则会导致启动崩溃。使用docker-compose logs查看日志是最直接的排查途径,日志里会明确显示启动失败的原因。

有一次我遇到 Spring Boot 不断重启循环,日志里没有特别明显的报错,最后发现是 healthcheck 配置的 curl 命令在容器里不存在,健康检查永远失败而触发重启机制。精简的基础镜像往往缺少工具,排查问题前先确认基础镜像包含哪些命令,避免把排查工具本身做成新的故障源。

5.5 常用排查命令速查表

目标命令
查看容器状态docker ps -a
跟踪日志docker logs -f 容器名
进入容器docker exec -it 容器名 /bin/sh
查看网络docker network inspect app-net
查看资源占用docker stats
查看挂载docker inspect 容器名查看Volumes和Mounts字段
清理悬空镜像docker image prune

5.6 镜像体积优化经验

部署完成后,我还会看一遍镜像体积。Spring Boot 的多阶段构建已经让后端镜像控制在 250 MB 以内,但 Nginx 镜像如果配置不当也可能变大。

最大的体积杀手是把源码 copy 到运行镜像里。有些人是直接把整个前端源码目录COPY . .到 Nginx 里,这样镜像里全是 .vue 文件和 node_modules,最后体积奔着 1 GB 去了。正确做法是只拷贝dist目录。

其次是一个隐藏体积杀手:如果你用单阶段构建,Jar 文件可能包含一堆冗余依赖。Spring Boot 3.x 自带 Spring Boot Maven Plugin 的分层工具,可以把 Jar 包里的依赖模块、应用模块拆开分层,配合 Docker 构建缓存,让依赖层只在 pom 变更时重新打包。

6. 我的自动化部署经验

整套方案稳定运行之后,我加了一点点自动化脚本,让发布流程更顺手。

先是在项目里写了一个简单的部署脚本deploy.sh:

#!/bin/bash set -e echo "==== 构建后端镜像 ====" docker build -t backend:1.0.0 ./backend echo "==== 构建前端镜像 ====" docker build -t frontend:1.0.0 ./frontend echo "==== 启动服务 ====" docker-compose up -d echo "==== 清理悬空镜像 ====" docker image prune -f

在服务器上执行这个脚本,它会自动完成从源码到服务启动的全过程。如果代码有内容更新,只需要在服务器上拉取最新代码(git pull),然后重新执行脚本,前端的 npm 构建、后端的 Maven 打包都会在容器内完成,宿主机完全不需要安装 Java 和 Node 环境。

这种方式对于中小型项目来说,性价比已经很高了。后续如果团队规模变大、发布频率变高,可以考虑接入更完善的自动化流水线,但基础原理都是一样的,仍然是构建镜像、推送仓库、服务器拉取启动这三个环节。

我这里分享的只是常规做法,不同项目细节会有些出入。比如如果你的前端没有用 Vue Router 的 history 模式,Nginx 的配置就不需要try_files回退;如果 Spring Boot 项目还在用 Java 8,基础镜像的标签要对应改成temurin-8。

部署本身不是难点,难的是把每个环节的依赖关系理清楚,然后选择合适的编排方式。按上面这一套流程走下来,前后端项目从一个无从下手的“一堆本地进程”,变成一个完整的、可迁移的容器集合,整个开发体验会顺畅很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 9:38:39

校园商铺管理系统完整开发指南:SpringBoot+Vue+MySQL从设计到部署

做毕设辅导这几年&#xff0c;我经手过最多的题目类型&#xff0c;就是“某某系统管理平台”。表面看&#xff0c;这类题目就是标准的增删改查&#xff0c;很多同学拿到题目的第一反应是“稳了”&#xff0c;结果真正动手才发现&#xff1a;功能好写&#xff0c;但数据库设计容…

作者头像 李华
网站建设 2026/10/10 9:38:06

Python单元测试最佳实践:unittest框架核心用法与工程管理指南

1. 为什么你的项目需要单元测试——先想清楚再动手1.1 单元测试到底在解决什么问题我在一线写了十来年代码&#xff0c;见过太多项目死在"改一处代码&#xff0c;崩三个功能"的泥潭里。最常见的场景是&#xff1a;产品经理说"帮我把价格计算里加个折扣"&am…

作者头像 李华
网站建设 2026/10/10 9:37:48

基于SpringBoot+Vue的酒店管理系统设计与实现全攻略

每年到三四月份&#xff0c;总有不少同学私信问我&#xff1a;毕设到底选什么题&#xff1f;系统做到什么程度答辩才稳&#xff1f;有没有一个项目是“功能够全、技术栈够主流、工作量看起来也够足”的&#xff1f;如果你正在为选题挠头&#xff0c;那我非常建议看看“基于Spri…

作者头像 李华
网站建设 2026/10/10 9:37:04

英语报警口语速成:5W框架与六大紧急场景应对

1. 报警电话的5W框架&#xff1a;先搞清楚接警员想听什么很多人学英语报了十多年培训班&#xff0c;雅思也考过&#xff0c;真到了国外碰上抢劫、车祸或者朋友突然倒地不起的那一刻&#xff0c;大脑直接一片空白&#xff0c;嘴里只剩下“Hello”和“Help”。这不是个别现象&…

作者头像 李华
网站建设 2026/10/10 9:35:46

银行排队系统:从数据结构到事件驱动仿真全解析

简介&#xff1a;一份面向数据结构课程期末作业的银行排队系统实现资源&#xff0c;重点演示队列先进先出结构以及VIP与普通用户的多队列优先级调度。压缩包共九个文件&#xff0c;大小约1.03MB&#xff0c;包含C源码、用户信息文本、可执行程序及Code::Blocks工程文件&#xf…

作者头像 李华