news 2026/5/6 22:52:36

Docker 学习篇(六)| 实战 — 用 Docker 构建 SpringBoot + Vue 全栈项目

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Docker 学习篇(六)| 实战 — 用 Docker 构建 SpringBoot + Vue 全栈项目

Docker 学习篇(六)| 实战 — 用 Docker 构建 SpringBoot + Vue 全栈项目

    • 1. 前置准备
      • 1.1 确认 Docker 装好了
      • 1.2 配置镜像加速器
    • 2. 拉取中间件镜像
    • 3. 后端:blog-server 的 Dockerfile
      • 3.1 分析项目
      • 3.2 在项目根目录创建文件
      • 3.3 构建镜像
    • 4. 前端:blog-ui 的 Dockerfile
      • 4.1 分析项目
      • 4.2 创建文件
      • 4.3 构建镜像
    • 5. docker-compose.yml(全家桶一键启动)
    • 6. 启动与验证
      • 6.1 如果你本机已装了 MySQL / Redis
      • 6.2 构建并启动
      • 6.3 验证
      • 6.4 最终效果:四个容器各司其职
    • 7. 部署到服务器
      • 7.1 导出镜像
      • 7.2 上传到服务器
      • 7.3 服务器上导入并启动
    • 8. 命令速查
    • 9. 常见问题

1. 前置准备

1.1 确认 Docker 装好了

dockerversion

输出中有 Client 和 Server 两段,Server 有版本号就是 OK。

1.2 配置镜像加速器

国内直连 Docker Hub 几乎不可用,必须配镜像加速。

Docker Desktop → 设置 → Docker Engine → 修改registry-mirrors

{"registry-mirrors":["https://docker.1ms.run","https://docker.m.daocloud.io"]}

点击Apply & Restart重启 Docker 后生效。

验证:

dockerpull hello-world

能拉下来就说明配好了。拉完删掉:docker rmi hello-world

⚠️docker.xuanyuan.me在 Docker Desktop 29.x 上不兼容(报content size of zero),不要加。


2. 拉取中间件镜像

项目需要的中间件只有 MySQL 和 Redis。Nginx 不需要单独拉——它会在构建前端镜像时从 Docker Hub 自动拉取(FROM nginx:alpine)。

dockerpull mysql:8.0dockerpull redis:7-alpine

确认:

dockerimages

看到mysql:8.0redis:7-alpine即可。


3. 后端:blog-server 的 Dockerfile

3.1 分析项目

blog-server 是一个 Spring Boot 3.2.5 + Java 21 + Maven 多模块项目:

blog-server/ ├── pom.xml ← 父 POM ├── blog-bootstrap/ ← 启动模块(有 main 方法) ├── blog-module-common/ ├── blog-module-article/ ├── blog-module-comment/ ├── blog-module-media/ ├── blog-module-auth/ ├── blog-module-site/ └── blog-module-message/

关键配置(application.yml):

配置项环境变量application.yml 默认值docker-compose 中覆盖为
数据库地址DB_HOSTlocalhostmysql(容器名)
数据库端口DB_PORT33073306(容器内端口)
数据库名blog
用户名DB_USERNAMErootroot
密码DB_PASSWORDrootroot
Redis 地址REDIS_HOSTlocalhostredis(容器名)
Redis 端口REDIS_PORT63806379(容器内端口)
Redis 密码123456
服务端口SERVER_PORT80808080
上传目录UPLOAD_PATH本地 Windows 路径/app/upload(容器内路径)

application.yml 默认值已设为连 Docker 容器(宿主机端口 3307/6380),方便 IDEA 直接跑。docker-compose 会用自己的环境变量覆盖为容器内端口(3306/6379)。

3.2 在项目根目录创建文件

blog-server/下创建.dockerignore

target/ .git/ .idea/ *.md *.log upload/ logs/

blog-server/下创建Dockerfile

# ===== 第一阶段:编译 ===== FROM maven:3.9-eclipse-temurin-21 AS builder WORKDIR /app # 直接复制所有源码,单步编译 # 注意:多模块 Maven 项目不建议用 mvn dependency:go-offline 分层—— # 内部模块间依赖无法从本地仓库解析,会导致构建失败 COPY . . RUN mvn clean package -DskipTests -pl blog-bootstrap -am # ===== 第二阶段:运行 ===== FROM eclipse-temurin:21-jre-alpine WORKDIR /app # 从编译阶段只拿 jar 包 COPY --from=builder /app/blog-bootstrap/target/*.jar app.jar # 创建上传目录 RUN mkdir -p /app/upload EXPOSE 8080 CMD ["java", "-jar", "app.jar"]

多阶段构建的关键认知:第一阶段用maven:3.9-eclipse-temurin-21(含 JDK + Maven + 源码 → ~700MB),编译完后整个第一阶段丢弃。最终镜像基于eclipse-temurin:21-jre-alpine(只含 JRE → ~180MB),加上我们的 jar 约 250MB。如果不用多阶段,直接把 JDK 打进去,镜像至少 500MB+。

blog-server 最终镜像里的东西: ✅ JRE 21(运行 Java 需要) ✅ app.jar(我们的代码) ✅ /app/upload 目录 ❌ Maven(不需要了) ❌ JDK 编译器(不需要了) ❌ 源代码(不需要了)

三种 Docker 构建方式的对比(基础/多阶段/Buildpacks)详见第四篇第 3 节。这里直接用最推荐的多阶段构建。

3.3 构建镜像

cdblog-serverdockerbuild-tblog-server:latest.

4. 前端:blog-ui 的 Dockerfile

4.1 分析项目

blog-ui 是 Vue 3 + Vite + Element Plus,构建后生成dist/静态文件。

4.2 创建文件

blog-ui/下创建.dockerignore

node_modules/ dist/ .git/ *.md

blog-ui/下创建nginx.conf

server { listen 80; server_name _; root /usr/share/nginx/html; index index.html; # ========== 关键:API 请求代理到后端 ========== location /api/ { proxy_pass http://blog-server: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; } # Swagger / Knife4j API 文档 location /swagger-ui.html { proxy_pass http://blog-server:8080; } location /v3/api-docs { proxy_pass http://blog-server:8080; } location /webjars/ { proxy_pass http://blog-server:8080; } # 上传文件 location /upload/ { proxy_pass http://blog-server:8080; } # Vue Router history 模式:找不到文件就回退到 index.html location / { try_files $uri $uri/ /index.html; } }

为什么需要 nginx 代理/api/

Vue 项目里的 Axios 发请求是从用户浏览器发出的,不是从 Docker 容器里发出的。如果你在.env里写VITE_API_BASE_URL=http://localhost:8080,那浏览器就真的去访问用户自己电脑的localhost:8080——这在部署到服务器上时完全不对。

正确做法:前端请求全部发到同源(同一个域名/端口),由 nginx 根据路径前缀把/api/转发给后端容器:

浏览器 → http://服务器/api/v1/articles ↓ nginx (blog-ui 容器) ↓ proxy_pass http://blog-server:8080 ↓ blog-server 容器处理请求

blog-ui/下创建Dockerfile

# ===== 第一阶段:构建 ===== FROM node:20-alpine AS builder WORKDIR /app # 先复制依赖描述文件(package.json + package-lock.json) # 这样改源码不改依赖时,npm install 这层走缓存 COPY package*.json . RUN npm config set registry https://registry.npmmirror.com # 国内加速,海外可删 RUN npm install # VITE_API_BASE_URL 设为空:浏览器发请求到同源,由 nginx 代理到后端 ENV VITE_API_BASE_URL="" COPY . . # build-only 是该项目跳过了类型检查(vue-tsc),普通项目用 npm run build 即可 RUN npm run build-only # ===== 第二阶段:托管 ===== FROM nginx:alpine COPY --from=builder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80

VITE_API_BASE_URL 为什么设空?Vite 在构建时会把import.meta.env.VITE_API_BASE_URL替换为实际值,写进 JS 文件里。设为空字符串后,Axios 的baseURL为空,所有请求变成相对路径(如/api/v1/articles),浏览器自动发到当前页面的域名。然后 nginx 根据/api/前缀转发给后端。

blog-ui 最终镜像里的东西: ✅ Nginx(Web 服务器) ✅ dist/ 静态文件(HTML + JS + CSS) ✅ nginx.conf(代理规则) ❌ Node.js(不需要了) ❌ node_modules(不需要了) ❌ 源代码(不需要了) ❌ npm(不需要了)

4.3 构建镜像

cdblog-uidockerbuild-tblog-ui:latest.

5. docker-compose.yml(全家桶一键启动)

在项目根目录创建docker-compose.yml

services:mysql:image:mysql:8.0container_name:blog-mysqlports:-"3307:3306"volumes:-D:/Develop/DockerData/Personal/docker-mysql:/var/lib/mysqlenvironment:MYSQL_ROOT_PASSWORD:rootMYSQL_DATABASE:blogrestart:unless-stoppedhealthcheck:test:["CMD","mysqladmin","ping","-h","localhost"]interval:10stimeout:5sretries:5networks:-blog-netredis:image:redis:7-alpinecontainer_name:blog-redisports:-"6380:6379"volumes:-D:/Develop/DockerData/Personal/docker-redis:/datacommand:redis-server--requirepass 123456restart:unless-stoppednetworks:-blog-netblog-server:image:blog-server:latest# 本地 docker build,服务器 docker load,同一份 compose 两边通用container_name:blog-serverports:-"8081:8080"# 8081 避免和 IDEA 里跑的冲突volumes:-blog-upload:/app/upload# 上传的文件持久化environment:-SPRING_PROFILES_ACTIVE=docker-DB_HOST=mysql# 用容器名当域名!-DB_PORT=3306# 容器内端口,不是映射端口-DB_USERNAME=root-DB_PASSWORD=root-REDIS_HOST=redis-REDIS_PORT=6379-REDIS_PASSWORD=123456-UPLOAD_PATH=/app/uploaddepends_on:mysql:condition:service_healthyredis:condition:service_startedrestart:unless-stoppednetworks:-blog-netblog-ui:image:blog-ui:latestcontainer_name:blog-uiports:-"80:80"depends_on:-blog-serverrestart:unless-stoppednetworks:-blog-netvolumes:blog-upload:# 命名卷:Docker 管理,不用关心路径networks:blog-net:driver:bridge# 自定义桥接网络:容器间用容器名互访

关键知识点:

1. 为什么统一用image:不用build:

  • docker-compose.yml 没有build:,不管本地还是服务器,同一份文件直接用
  • 本地改代码后:docker build -t blog-server:latest . && docker compose up -d
  • 服务器部署时:docker load -i blog-server.tar && docker compose up -d
  • 不用记"本地用 build 服务器用 image"这种容易忘的规则

2. 容器间通信用容器名

  • blog-server 的环境变量里DB_HOST=mysql,用的是compose 服务名
  • 原理:同一个自定义网络(blog-net)里的容器,Docker 内置 DNS 会把服务名解析为容器 IP
  • 注意端口用容器内端口(3306),不是宿主机映射端口(3307

3. 四个镜像来源

远程拉取(Docker Hub) 本地构建(我们的 Dockerfile) ┌─────────────────┐ ┌──────────────────┐ │ mysql:8.0 │ │ blog-server │ │ redis:7-alpine │ │ blog-ui │ └─────────────────┘ └──────────────────┘ ↓ ↓ 4 个镜像 → docker compose up → 4 个容器

6. 启动与验证

6.1 如果你本机已装了 MySQL / Redis

两种方案,挑一个:

方案 A:停掉本机服务(简单省事)

# 管理员 PowerShellnet stop MySQL80# 服务名可能不同,去 services.msc 确认net stop Redis# 服务名也可能是 RedisService 或其他,去 services.msc 确认

此时 3306 和 6379 空闲,但 compose 仍然走 3307/6380,IDEA 连接也不用变。

方案 B:本机服务留着,Docker 换端口(推荐,已配好)

本文的docker-compose.yml已经用了岔开端口的方案:

Windows 本机 Docker 容器 ├── MySQL → localhost:3306 ├── MySQL → localhost:3307 ← compose 默认 ├── Redis → localhost:6379 ├── Redis → localhost:6380 ← compose 默认

不需要改任何配置,直接docker compose up -d就能两边同时跑。

⚠️数据目录绝对不能共用!两个 MySQL 实例指向同一份数据文件会锁表甚至损坏数据。Docker 的数据目录D:/Develop/DockerData/Personal/docker-mysql必须是空的独立目录。

IDEA 里切着连:

  • 连 Windows MySQL →localhost:3306,root / root
  • 连 Docker MySQL →localhost:3307,root / root

6.2 构建并启动

# 1. 先构建镜像(把代码打成镜像)dockerbuild-tblog-server:latest ./blog-serverdockerbuild-tblog-ui:latest ./blog-ui# 2. 一键启动dockercompose up-d# 之后改代码,只需重建镜像并重启对应服务dockerbuild-tblog-server:latest ./blog-server&&dockercompose up-d

6.3 验证

# 看四个容器是否都在跑dockercomposeps# 看后端日志dockerlogs-fblog-server# 测试 API(通过 nginx 代理)curlhttp://localhost/api/v1/site/config# 浏览器访问# 前端: http://localhost# API 文档: http://localhost/swagger-ui.html(通过 nginx 代理)# 后端直连: http://localhost:8081(绕过 nginx,调试用)

所有访问都走前端 80 端口,nginx 根据路径自动转发/api/到后端。后端 8081 只给开发者本地调试用。

6.4 最终效果:四个容器各司其职

浏览器访问 http://localhost │ ▼ ┌──────────────┐ │ blog-ui │ Nginx 容器 │ :80 │ / → 静态文件 (Vue) └──────┬───────┘ /api/* → 代理给 blog-server:8080 │ /upload/* → 代理给 blog-server:8080 ▼ ┌──────────────┐ │ blog-server │ JRE 容器 │ :8080 │ Spring Boot 应用 └──┬─────┬─────┘ │ │ ▼ ▼ ┌────┐ ┌────┐ │mysql│ │redis│ 中间件容器 │:3306│ │:6379│ └────┘ └────┘

用代码验证 Redis 连接(可选):

在 blog-server 里加一个测试接口,确认 Redis 容器能正常读写:

@RestControllerpublicclassRedisTestController{@AutowiredprivateStringRedisTemplatestringRedisTemplate;@GetMapping("/redis/set")publicStringsetRedisData(){stringRedisTemplate.opsForValue().set("docker-test","Docker Redis 连接成功!");return"已存入 Redis";}@GetMapping("/redis/get")publicStringgetRedisData(){returnstringRedisTemplate.opsForValue().get("docker-test");}}

访问/redis/set存数据,/redis/get取数据,能取到说明 Redis 通了。


7. 部署到服务器

7.1 导出镜像

dockersave-oblog-server.tar blog-server:latestdockersave-oblog-ui.tar blog-ui:latest

7.2 上传到服务器

通过 FinalShell 或宝塔面板,把blog-server.tarblog-ui.tardocker-compose.yml传到服务器上。

7.3 服务器上导入并启动

服务器上需要拉取中间件镜像(MySQL、Redis):

# 拉取中间件镜像(服务器也要配镜像加速器)dockerpull mysql:8.0dockerpull redis:7-alpine# 导入你自己的镜像dockerload-iblog-server.tardockerload-iblog-ui.tar# 修改 docker-compose.yml 中卷路径为 Linux 路径(如 /data/docker-mysql)# docker-compose.yml 用的是 image: 不是 build:,本地服务器同一份,不用改dockercompose up-d

服务器部署时,建议删掉 docker-compose.yml 中 MySQL 和 Redis 的ports映射——生产环境只暴露前端 80/443 就够了。

注意.tarvs.tar.gz如果镜像文件后缀是.tar.gz,两种方式导入:

# 方式一:管道解压导入(最可靠)gunzip-cblog-server.tar.gz|dockerload# 方式二:直接用 load(部分新版 Docker 支持)dockerload-iblog-server.tar.gz

有人会把.tar文件直接改后缀成.tar.gz,导致gunzip -c失败。用file 文件名命令可以查看真实类型——显示POSIX tar archive就是.tar,显示gzip compressed data才是真正的.tar.gz

docker cp:容器和宿主机互传文件

# 从宿主机复制到容器dockercpapp.jar blog-server:/app/app.jar# 从容器复制到宿主机(比如导出日志)dockercpblog-server:/app/logs ./logs# 容器内目录结构参考:# 后端应用:取决于 Dockerfile 的 WORKDIR(本例为 /app)# 前端静态文件:/usr/share/nginx/html(Nginx 默认)

8. 命令速查

完整命令手册见 第五篇:常用命令速查。这里只列本篇用到的关键命令:

# 构建dockerbuild-tblog-server:latest ./blog-server# 构建后端镜像dockerbuild-tblog-ui:latest ./blog-ui# 构建前端镜像# 启动dockercompose up-d# 启动所有服务dockercompose up-d--force-recreate# 重建容器(镜像更新后用这个)dockercompose down# 停止并清理dockercomposeps# 看所有容器状态dockercompose logs-fblog-server# 看后端日志

9. 常见问题

问题解决
端口被占用停掉 Windows 本机的 MySQL/Redis 服务,或岔开端口映射(如 3307)
容器启动后立即退出docker logs 容器名看错误日志,通常缺环境变量或端口冲突
后端连不上数据库检查DB_HOST=mysql(容器名)且DB_PORT=3306(容器内端口),不是宿主机端口 3307
构建慢检查.dockerignore有没有排除node_modules/target
镜像拉不下来检查镜像加速器配了没有,重启 Docker
改代码不生效docker compose up -d --build(重建镜像并重启)
Redis 客户端连不上localhost,端口填宿主机映射端口(如 6380),不是容器 IP
容器名重复容器名同一机器唯一,先docker rm 容器名再重建

容器卡死,stop/restart/kill 都不响应:

这种情况通常是 runc 进程异常,传统docker stopdocker kill都无效。

# 1. 获取容器主进程 PIDPID=$(dockerinspect-f'{{.State.Pid}}'容器名或ID)# 2. 强杀该 PID(在同一 shell 会话执行,变量才保留)kill$PID# 如果 kill 无响应,升级为强杀:kill -9 $PID# 3. 确认容器已停dockerps-a# 4. 强制删除并重建dockerrm-f容器名dockercompose up-d

kill $PID后容器状态从 running 变成 stopped,再docker rm -f清理干净,然后 compose 重部署。

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

Museeks功能深度解析:从基础播放到高级队列管理

Museeks功能深度解析:从基础播放到高级队列管理 【免费下载链接】museeks 🎵 A simple, clean and cross-platform music player 项目地址: https://gitcode.com/gh_mirrors/mu/museeks Museeks是一款简洁、跨平台的音乐播放器,它以优…

作者头像 李华
网站建设 2026/5/6 22:48:27

在 Node.js 后端服务中接入 Taotoken 实现异步 AI 补全

在 Node.js 后端服务中接入 Taotoken 实现异步 AI 补全 1. 环境准备与依赖安装 在开始集成 Taotoken 之前,请确保您的 Node.js 开发环境满足以下条件:Node.js 版本不低于 16.x,并已初始化 npm 项目。我们将使用官方 openai npm 包进行对接&…

作者头像 李华