Docker 学习篇(六)| 实战 — 用 Docker 构建 SpringBoot + Vue 全栈项目
Docker 学习篇六| 实战 — 用 Docker 构建 SpringBoot Vue 全栈项目1. 前置准备1.1 确认 Docker 装好了1.2 配置镜像加速器2. 拉取中间件镜像3. 后端blog-server 的 Dockerfile3.1 分析项目3.2 在项目根目录创建文件3.3 构建镜像4. 前端blog-ui 的 Dockerfile4.1 分析项目4.2 创建文件4.3 构建镜像5. docker-compose.yml全家桶一键启动6. 启动与验证6.1 如果你本机已装了 MySQL / Redis6.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.0和redis:7-alpine即可。3. 后端blog-server 的 Dockerfile3.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_PASSWORDrootrootRedis 地址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/下创建.dockerignoretarget/ .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 --frombuilder /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 的 Dockerfile4.1 分析项目blog-ui 是 Vue 3 Vite Element Plus构建后生成dist/静态文件。4.2 创建文件在blog-ui/下创建.dockerignorenode_modules/ dist/ .git/ *.md在blog-ui/下创建nginx.confserver { 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_URLhttp://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 --frombuilder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80VITE_API_BASE_URL 为什么设空Vite 在构建时会把import.meta.env.VITE_API_BASE_URL替换为实际值写进 JS 文件里。设为空字符串后Axios 的baseURL为空所有请求变成相对路径如/api/v1/articles浏览器自动发到当前页面的域名。然后 nginx 根据/api/前缀转发给后端。blog-ui 最终镜像里的东西 ✅ NginxWeb 服务器 ✅ dist/ 静态文件HTML JS CSS ✅ nginx.conf代理规则 ❌ Node.js不需要了 ❌ node_modules不需要了 ❌ 源代码不需要了 ❌ npm不需要了4.3 构建镜像cdblog-uidockerbuild-tblog-ui:latest.5. docker-compose.yml全家桶一键启动在项目根目录创建docker-compose.ymlservices:mysql:image:mysql:8.0container_name:blog-mysqlports:-3307:3306volumes:-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:6379volumes:-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_ACTIVEdocker-DB_HOSTmysql# 用容器名当域名-DB_PORT3306# 容器内端口不是映射端口-DB_USERNAMEroot-DB_PASSWORDroot-REDIS_HOSTredis-REDIS_PORT6379-REDIS_PASSWORD123456-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:80depends_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_HOSTmysql用的是compose 服务名原理同一个自定义网络blog-net里的容器Docker 内置 DNS 会把服务名解析为容器 IP注意端口用容器内端口3306不是宿主机映射端口33073. 四个镜像来源远程拉取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/6380IDEA 连接也不用变。方案 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:3306root / root连 Docker MySQL →localhost:3307root / root6.2 构建并启动# 1. 先构建镜像把代码打成镜像dockerbuild-tblog-server:latest ./blog-serverdockerbuild-tblog-ui:latest ./blog-ui# 2. 一键启动dockercompose up-d# 之后改代码只需重建镜像并重启对应服务dockerbuild-tblog-server:latest ./blog-serverdockercompose up-d6.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:latest7.2 上传到服务器通过 FinalShell 或宝塔面板把blog-server.tar、blog-ui.tar、docker-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/htmlNginx 默认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_HOSTmysql容器名且DB_PORT3306容器内端口不是宿主机端口 3307构建慢检查.dockerignore有没有排除node_modules/target镜像拉不下来检查镜像加速器配了没有重启 Docker改代码不生效docker compose up -d --build重建镜像并重启Redis 客户端连不上填localhost端口填宿主机映射端口如 6380不是容器 IP容器名重复容器名同一机器唯一先docker rm 容器名再重建容器卡死stop/restart/kill 都不响应这种情况通常是 runc 进程异常传统docker stop或docker 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-dkill $PID后容器状态从 running 变成 stopped再docker rm -f清理干净然后 compose 重部署。