news 2026/10/11 17:47:48

Docker部署Spring Boot+Vue全流程:镜像构建、编排与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Docker部署Spring Boot+Vue全流程:镜像构建、编排与避坑指南

开头我就直接说结论:用Docker把这套Spring Boot + Vue项目容器化跑起来,说难不难,但踩过的坑绝对不少。尤其当你从“开发环境能跑”跨越到“一台干净服务器上一条命令拉起来”,中间涉及的镜像构建、网络打通、配置注入、优雅停机这些问题,每一步都有细节。我这边某个模拟项目X就是这么一步步从单机手动部署切到Docker Compose编排的,整个过程的经验沉淀下来,写成这篇东西分享给你。本文适合正在做前后端分离项目、准备引入容器化部署的同学,也适合Docker刚入门、想找一份完整可落地参考的人。

1. 先把部署架构想清楚:三个容器如何协同

很多人一上来就写Dockerfile,结果文件写好了、镜像build出来了,docker run却起不来,或者起来了但前端访问不到后端接口。根子在于架构没先理清。Docker部署Spring Boot + Vue,本质上要解决三个问题:后端服务怎么打包成镜像,前端静态资源怎么伺候,两个服务之间以及它们与数据库之间怎么通消息。

1.1 项目拆分与容器规划

我这边的模拟项目X是典型的前后端分离结构:后端Spring Boot提供RESTful接口,端口8080;前端Vue开发完打包后是一堆静态文件,需要Nginx托管,同时Nginx承担反向代理,把/api开头的请求转发给后端。数据库用的MySQL 8.0,数据目录必须持久化。

基于这个结构,容器规划就很清晰了:

容器角色镜像来源内部端口与外部的关系
springboot-app自建,多阶段构建8080仅内网,由Nginx代理访问
nginx-web自建,多阶段构建80映射到宿主机的8081,对外唯一入口
mysql-dbDocker Hub官方mysql:8.03306映射到宿主机3306,供本地运维用

关键设计思路是:对外只暴露Nginx一个口子,后端不直接暴露给外部。这样安全策略收敛了,外部请求全部走Nginx的80端口,再由它决定是返回前端静态页面还是代理给后端接口。很多初学者会把Spring Boot的8080也直接映射出去,图省事,但其实会增加暴露面和配置复杂度,不建议这么做。

1.2 网络模式选择:桥接网络比links好用

我当时规划网络时也纠结过,Docker Compose项目会自动创建默认网络,服务之间通过服务名互相访问。后来发现直接用Compose默认的桥接网络就足够了,服务名就是天然的主机名,Spring Boot容器里配置数据库地址直接写mysql-db而不是localhost,Nginx配置里代理上游直接写springboot-app:8080。

这里要刻意提醒一下:Docker容器里没有localhost共享的概念。每个容器有自己独立的网络命名空间,localhost指的是容器自己,你如果在Spring Boot容器里访问localhost:3306,那是在找容器自己,必然失败。跨容器访问必须走网络别名,也就是服务名。刚开始用Docker的人经常栽在这里,查了半天代码也查不出问题。

2. 后端镜像构建:Dockerfile里的瘦身思路与启动细节

Spring Boot后端镜像可能是整个环节里最容易出问题的地方,但也是最容易优化出彩的地方。我从最初的单阶段构建到后来改成多阶段构建,镜像体积从500多MB直接降到180MB左右,部署和传输速度明显改善。

2.1 多阶段构建:编译容器与运行容器分离

后端的构建流程很简单:先拉一个带Maven和JDK的镜像把源码编译成jar包,再拉一个只带JRE的镜像把jar包塞进去运行。两个阶段分开,最终镜像里没有编译工具链,体积自然小。下面是我这边的Dockerfile,踩过几个坑之后稳定使用的版本:

# 第一阶段:编译 FROM maven:3.8-openjdk-11 AS builder WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline -B COPY src ./src RUN mvn clean package -DskipTests -B # 第二阶段:运行 FROM openjdk:11-jdk-slim WORKDIR /app ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone COPY --from=builder /build/target/app.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "/app/app.jar"]

这里有个看起来不起眼但很重要的操作:用RUN mvn dependency:go-offline -B先把依赖下载好,生成镜像构建缓存。这样之后改代码重新构建,只要pom没变,Maven依赖层就会命中缓存,省掉每次重复下载依赖的时间。实测下来,单纯改一个Java类再build镜像,整个过程能控制在20秒左右,不卡在Maven下载上。

JRE镜像我选jdk-slim而不是alpine版本,原因在于alpine上的glibc和某些Java原生库会有兼容问题,尤其项目里用了JNA或者需要访问系统时间库时,容易出现诡异报错。slim系列基于Debian,包管理成熟、兼容性好,虽然体积比alpine大一点,但换来的是省心,值。

2.2 JVM参数与容器资源限制的适配

容器里跑Java有个特殊问题:JVM默认的堆内存大小是根据宿主机物理内存计算的。如果不加限制,一台64G内存的机器上,容器内的JVM可能把堆开到物理内存的1/4甚至更多,而Docker容器本身可能只配额了1G。结果就是容器OOM被杀,或者机器上多个容器互相抢占内存。

我这边给JVM显式设置了堆参数,同时开启容器感知特性:

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

在实际服务器的docker-compose里,我还加了mem_limit资源限制,让Docker和JVM两层设置匹配。经验是:-XX:MaxRAMPercentage适合JVM版本较新、想自动适应容器内存的场景;-Xmx512m则适合明确知道业务峰值需要多少堆的场景。我最终两者都用了,以-Xmx为硬顶,MaxRAMPercentage兜底。如果你的服务是定时任务类、内存访问不密集,堆给256m就能跑;如果是高并发接口型,建议先压测再定。

另外在JVM参数里我加了-Djava.security.egd=file:/dev/./urandom。很多人忽略这个参数,Spring Boot启动时如果/dev/random熵源不足,会出现启动慢到几十秒甚至卡住的现象。换成urandom后启动时间明显稳定。

2.3 时区问题:两行命令搞定

容器默认时区是UTC,而业务日志、定时任务都需要北京时间。第一次我没处理时区,结果凌晨的定时任务差8小时执行,排查了很久才意识到是容器时区问题。解决办法就在Dockerfile里加两行:

ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

第一行设置环境变量,第二行把系统时区文件链接过去。注意Debian系镜像可能没装tzdata,如果执行报错,可以在前面加一句RUN apt-get update && apt-get install -y tzdata。这是我实际遇到的情况,openjdk:11-jdk-slim里tzdata默认就有,但如果换更激进的基础镜像,就要留意。

3. 前端镜像构建:Nginx托管与API反向代理的细节

前端这一侧,难点不再是构建,而是怎么让Nginx既能托管SPA的静态资源,又能把API请求转发到后端容器,同时处理好路由的history模式。

3.1 多阶段构建:从Node到Nginx

前端的Dockerfile思路和后端一致,分两个阶段:

# 第一阶段:构建前端静态文件 FROM node:16-alpine AS build WORKDIR /web COPY package*.json ./ RUN npm ci --registry=https://registry.npmmirror.com COPY . . RUN npm run build # 第二阶段:Nginx托管 FROM nginx:1.25-alpine WORKDIR /usr/share/nginx/html RUN rm -rf ./* COPY --from=build /web/dist . COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]

npm ci和npm install的区别值得说一句。npm ci是严格按package-lock.json安装依赖,速度更快,且不会改动lock文件,在CI/Docker构建场景下是更正确的选择。以前我用npm install,时不时出现依赖版本漂移导致的构建结果不一致,换成npm ci后就没再出现。

前端构建阶段我在网络源上走了点弯路,npm默认源在某些网络环境下特别慢,后来在Dockerfile里临时指定了npmmirror源,构建速度提升显著。但要注意,这只适用于构建阶段,不会污染项目的npm配置。

3.2 SPA路由history模式与try_files

Vue默认的hash路由不涉及服务端配置,但大多数正规项目都会改成history模式,去掉URL里的#。问题来了:history模式下,用户直接访问/login、/dashboard这类具体路径时,Nginx找不到对应的物理文件,会返回404。

解决办法是Nginx配置里加try_files指令:

location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }

这段配置的含义是:先尝试按请求的路径找文件,找不到就尝试目录,还找不到就统一返回index.html,由前端路由接管。初学者容易漏掉这行,前端自己访问时都从根路径进没问题,一部署上线,刷新子页面就白屏或404。这个坑我帮同事排过好几次。

3.3 API反向代理的几个避坑点

Nginx里把/api前缀的请求转发给后端Spring Boot,常规写法是:

location /api/ { proxy_pass http://springboot-app: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://springboot-app:8080/会把/api前缀剥掉再转发,也就是前端请求/api/user/list,后端收到的是/user/list;不带斜杠则保持原路径,后端收到/api/user/list。两种方式都能用,关键是前后端约定要一致。我这边是后端接口统一加/api前缀,前端代理走proxy_pass http://springboot-app:8080;(不带斜杠),保持全路径传递,后端Controller也能正常匹配。

另外,proxy_set_header这几行别删。Spring Boot里如果用了request.getRemoteAddr()做IP白名单或访问日志,拿不到真实客户端IP会全是Nginx容器IP。X-Forwarded-For和X-Real-IP就是防止这个问题。

3.4 单页应用缓存策略

静态资源部署后,最尴尬的场景是用户浏览器缓存了旧版本的JS文件,界面还是老的。解决办法是在构建时给JS/CSS文件名加内容哈希。Vue CLI和Vite默认都会做这件事,但Nginx的缓存响应头也得配合:

location /assets/ { expires 30d; add_header Cache-Control "public, immutable"; } location / { expires -1; add_header Cache-Control "no-cache"; }

前者告诉浏览器带哈希的静态资源可以长期缓存,后者保证index.html每次重新校验。这套配置能让发版后用户无需强刷就能看到新界面,是我在部署迭代中被逼出来的方案。

4. docker-compose编排:数据库、依赖顺序与数据持久化

镜像准备好之后,容器编排是关键。我用的是Docker Compose,它比一个个docker run命令强太多了:一条docker-compose up -d重启整套环境,依赖顺序自动处理,网络和卷统一管理。

4.1 compose文件的核心配置

我这边生产环境的compose文件结构大致如下:

version: "3.8" services: mysql-db: image: mysql:8.0 container_name: projectx-mysql restart: always environment: MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD}" MYSQL_DATABASE: projectx MYSQL_USER: projectx_user MYSQL_PASSWORD: "${MYSQL_USER_PASSWORD}" TZ: Asia/Shanghai command: - --character-set-server=utf8mb4 - --collation-server=utf8mb4_unicode_ci - --default-time-zone=+08:00 volumes: - mysql-data:/var/lib/mysql - ./sql/init:/docker-entrypoint-initdb.d healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"] interval: 10s timeout: 5s retries: 5 ports: - "3306:3306" springboot-app: build: context: ./backend container_name: projectx-api restart: always depends_on: mysql-db: condition: service_healthy environment: SPRING_PROFILES_ACTIVE: prod DB_HOST: mysql-db DB_PORT: 3306 DB_NAME: projectx DB_USER: projectx_user DB_PASSWORD: "${MYSQL_USER_PASSWORD}" TZ: Asia/Shanghai ports: - "8080:8080" nginx-web: build: context: ./frontend container_name: projectx-web restart: always depends_on: - springboot-app ports: - "8081:80" volumes: - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro volumes: mysql-data:

MySQL的command里我显式指定了字符集和时区。utf8mb4比utf8强的点是能存四字节的Emoji字符,现代应用最好直接选utf8mb4,不然用户昵称里带个表情符号就会写入报错。这个坑我当年踩过,后来所有新建库都默认这个配置。

4.2 depends_on只解决启动顺序,不解决可用性

Compose的depends_on看起来控制了服务启动顺序,但它的默认行为只是想:先启动MySQL容器,再启动Spring Boot容器。问题在于MySQL容器“启动成功”不等于“可接受连接”,MySQL容器起来了,但初始化还在进行,Spring Boot这时候去连数据库,连接被拒,启动失败。

解决方式是健康检查配合depends_on新语法:

depends_on: mysql-db: condition: service_healthy

这样Compose会等MySQL通过健康检查(mysqladmin ping可以成功返回)才启动Spring Boot。这里注意condition: service_healthy是Compose较新版本才支持的写法。如果你的Compose版本不支持这种格式,另一个替代方案是在Spring Boot启动命令里加--spring.datasource.initialization-mode=never,或者在代码里配置数据库连接重试,但都不如健康检查这种方式干净。我在服务器上升级了Docker Compose插件后,这个功能稳定可用。

4.3 环境变量管理:不要硬编码敏感信息

看compose文件你会发现,密码都是${变量名}的形式,而不是写死。我是用一个.env文件在同级目录存放真实值,.env不提交到代码仓库,加入.gitignore。这算是最朴素的密钥管理方案,但好在简单有效。如果环境要求更高,可以接入专门的密钥管理服务,但在大部分项目场景下,.env配合好权限控制就够用了。

Spring Boot端的数据源配置我用环境变量注入:

SPRING_DATASOURCE_URL: jdbc:mysql://mysql-db:3306/projectx?useUnicode=true&characterEncoding=utf8 SPRING_DATASOURCE_USERNAME: projectx_user SPRING_DATASOURCE_PASSWORD: "${MYSQL_USER_PASSWORD}"

application.yml里写:

spring: datasource: url: ${SPRING_DATASOURCE_URL} username: ${SPRING_DATASOURCE_USERNAME} password: ${SPRING_DATASOURCE_PASSWORD}

这样开发和生产的数据库配置彻底分离,代码仓库里永远不会出现真实密码。

4.4 数据卷持久化:最容易被忽略的致命细节

MySQL容器一旦删除重建,如果不做数据持久化,库里的数据就全没了。这个后果在早期阶段不明显,等上线有真实用户数据后就是灾难级事故了。Compose的volumes段里我声明了mysql-data这个命名卷,挂载到MySQL的数据目录:

volumes: mysql-data:

匿名卷或命名卷之间我优先选命名卷,因为升级镜像时不会意外丢失数据,备份和迁移也方便,docker volume inspect就能看到挂载点位置。后端生成的日志文件、上传的图片文件如果也是写在容器本地,同样要做数据卷映射,不然容器重建后数据归零。

5. 部署上线流程:构建、迁移、更新与回滚

镜像和编排文件都准备完毕后,实际部署这条链路里还有几个经常翻车的环节:服务器上有没有正确的Docker环境、构建产物怎么传过去、出问题如何快速回滚。我把这几件事按顺序梳理一遍。

5.1 服务器环境检查与准备

在一台干净的Linux服务器上,第一步不是拉代码,而是确认Docker环境:

docker version docker-compose version

然后我把项目的后端目录、前端目录、compose文件等结构统一放到一个固定目录,比如/opt/projectx/,所有操作都在这个目录下进行:

/opt/projectx/ ├── docker-compose.yml ├── .env ├── backend/ │ ├── Dockerfile │ └── src/ └── frontend/ ├── Dockerfile ├── nginx.conf └── src/

目录结构调整好之后,第一次启动的完整命令序列是:

cd /opt/projectx docker-compose up -d --build mysql-db docker-compose up -d --build docker-compose ps

这里建议第一步先把MySQL单独启动起来,等它完成初始化并健康后再一次性拉起所有。特别是全新环境首次建库时,MySQL初始化需要跑初始化脚本,如果和其他服务同时启动,Spring Boot大概率会因为连不上库而启动失败,虽然restart: always会帮它重试,但日志里一堆红色报错,容易让人误判出问题。

5.2 镜像构建策略:用Compose的build还是预构建镜像

Compose的build直接指定context,每次up -d --build都会重新构建镜像。这种方式适合代码直接部署在服务器上的项目。我工作时遇到过场景是代码仓库和服务器分离,甚至在服务器上不拉源码,只拉镜像。这时建议把构建好的镜像推到镜像仓库,服务器上直接用image: myregistry.com/projectx-api:版本号,发布流程变成改版本号、拉镜像、重启容器。

对于小团队或者单机部署场景,直接用Compose的context构建更省心。但对于多环境部署,镜像仓库方案是迟早要走的路。我这边单机场景,直接代码目录构建,改动简单、链路短,目前够用。

5.3 更新与回滚操作

日常发布新版本,我的操作流程是:

cd /opt/projectx # 拉取新代码 git pull # 重新构建受影响的镜像并重启 docker-compose up -d --build springboot-app nginx-web # 查看滚动日志确认启动正常 docker-compose logs --tail=200 springboot-app

如果新版本启动后发现问题,需要回滚。Docker Compose原生没有一键回滚命令,我的做法很朴素:在关键的发布节点给当前镜像打标签,比如projectx-api:stable,回滚时把镜像标签指回去:

docker tag projectx-api:latest projectx-api:rollback-2025xxx docker-compose up -d --no-build springboot-app

如果改动涉及compose文件,可以用git revert先把配置文件回退,再重新up。

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

Ubuntu 从零搭建 EAP-TLS 双向认证测试环境实战

简介:这份文档面向需要在 Ubuntu 环境下搭建 802.1X 认证实验的网络运维与安全学习者,聚焦 Freeradius 与 EAP-TLS 双向认证的测试环境部署。内容围绕环境准备、Freeradius 安装、users 与 client.conf 配置、TLS 模块证书生成、eap 文件参数调整、路由器…

作者头像 李华
网站建设 2026/10/11 17:37:47

Reverse Engineer Anything:单日狂揽 1w+ Star 的开源逆向工程神器

1. 引言 最近,一个名为 Reverse Engineer Anything 的开源项目在 GitHub 上爆火,单日狂揽 1w Star,迅速冲上趋势榜前列。它之所以引发如此大的关注,是因为它把「逆向工程」这件事的门槛大幅拉低——让普通开发者也能轻松读懂、复现…

作者头像 李华
网站建设 2026/10/11 17:34:13

Stable Diffusion 本地安装与参数调优:从零跑出第一张图

简介:这份PDF资料面向希望入门AI绘画的开发者与爱好者,系统讲解Stable Diffusion的安装与使用流程,帮助零基础读者跨过环境配置门槛,快速跑通文本生成图像。资源包共1个PDF文件,约814KB,内容涵盖环境准备、…

作者头像 李华
网站建设 2026/10/11 17:31:34

CASIAwebFACE人脸识别数据集训练管线实战:从数据清洗到模型训练

简介:CASIA WebFace 是人脸识别领域最主流的大规模数据集之一,面向从事人脸检测、特征提取与模型训练的研究人员和算法工程师,尤其适合需要复现或对比经典人脸识别网络的中高级学习者。资源包内共1个docx文件,压缩包约11KB&#x…

作者头像 李华
网站建设 2026/10/11 17:29:56

Android Studio安装配置全攻略:SDK、Gradle与模拟器问题排查

想入坑 Android 开发,第一件绕不开的事就是安装 Android Studio。作为一个跟各种开发环境打过多年交道的人,我可以说:这个工具本身的安装门槛不高,但周围坑并不少——SDK 组件下载慢、Gradle 初始化卡住、模拟器黑屏,这…

作者头像 李华