Docker 学习篇(七)| 实战 --- 用 Docker 构建 SpringBoot + Vue 全栈项目
-
- [1. 前置准备](#1. 前置准备)
-
- [1.1 确认 Docker 装好了](#1.1 确认 Docker 装好了)
- [1.2 配置镜像加速器](#1.2 配置镜像加速器)
- [2. 拉取中间件镜像](#2. 拉取中间件镜像)
- [3. 后端:blog-server 的 Dockerfile](#3. 后端:blog-server 的 Dockerfile)
-
- [3.1 分析项目](#3.1 分析项目)
- [3.2 在项目根目录创建文件](#3.2 在项目根目录创建文件)
- [3.3 构建镜像](#3.3 构建镜像)
- [4. 前端:blog-ui 的 Dockerfile](#4. 前端:blog-ui 的 Dockerfile)
-
- [4.1 分析项目](#4.1 分析项目)
- [4.2 创建文件](#4.2 创建文件)
- [4.3 构建镜像](#4.3 构建镜像)
- [5. docker-compose.yml(全家桶一键启动)](#5. docker-compose.yml(全家桶一键启动))
-
- [5.1 环境变量的几种管理方式](#5.1 环境变量的几种管理方式)
-
- [方式一:全部写在 yml 里(上面就是)](#方式一:全部写在 yml 里(上面就是))
- [方式二:抽离到 `.env` 文件(推荐日常开发)](#方式二:抽离到
.env文件(推荐日常开发)) - [方式三:CI/CD 变量注入(上服务器推荐)](#方式三:CI/CD 变量注入(上服务器推荐))
- [方式四:Secrets Manager(大厂生产环境)](#方式四:Secrets Manager(大厂生产环境))
- 总结:你该用哪种
- [6. 启动与验证](#6. 启动与验证)
-
- [6.1 如果你本机已装了 MySQL / Redis](#6.1 如果你本机已装了 MySQL / Redis)
- [6.2 构建并启动](#6.2 构建并启动)
- [6.3 验证](#6.3 验证)
- [6.4 最终效果:四个容器各司其职](#6.4 最终效果:四个容器各司其职)
- [7. 部署到服务器](#7. 部署到服务器)
-
- [7.1 导出镜像](#7.1 导出镜像)
- [7.2 上传到服务器](#7.2 上传到服务器)
- [7.3 服务器上导入并启动](#7.3 服务器上导入并启动)
- [8. 命令速查](#8. 命令速查)
- [9. 常见问题](#9. 常见问题)
1. 前置准备
1.1 确认 Docker 装好了
bash
docker version
输出中有 Client 和 Server 两段,Server 有版本号就是 OK。
1.2 配置镜像加速器
国内直连 Docker Hub 几乎不可用,必须配镜像加速。
Docker Desktop → 设置 → Docker Engine → 修改 registry-mirrors:
json
{
"registry-mirrors": [
"https://docker.1ms.run",
"https://docker.m.daocloud.io"
]
}
点击 Apply & Restart 重启 Docker 后生效。
验证:
bash
docker pull 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)。
bash
docker pull mysql:8.0
docker pull redis:7-alpine
确认:
bash
docker images
看到 mysql:8.0 和 redis: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_HOST |
localhost |
mysql(容器名) |
| 数据库端口 | DB_PORT |
3307 |
3306(容器内端口) |
| 数据库名 | --- | blog |
--- |
| 用户名 | DB_USERNAME |
root |
root |
| 密码 | DB_PASSWORD |
root |
root |
| Redis 地址 | REDIS_HOST |
localhost |
redis(容器名) |
| Redis 端口 | REDIS_PORT |
6380 |
6379(容器内端口) |
| Redis 密码 | --- | 123456 |
--- |
| 服务端口 | SERVER_PORT |
8080 |
8080 |
| 上传目录 | 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:
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"]
多阶段构建原理见第四篇第 3 节和第 17 节。简单说:该处 maven:3.9-eclipse-temurin-21(包含Maven + JDK) 编译完就丢弃,不进入最终镜像。
blog-server 最终镜像里只有: ✅ JRE 21(运行 Java) ✅ app.jar(我们的代码) ✅ /app/upload 目录 ❌ Maven / JDK / 源码(编译完就不要了)
3.3 构建镜像
bash
cd blog-server
docker build -t blog-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:
nginx
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:
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 构建镜像
bash
cd blog-ui
docker build -t blog-ui:latest .
5. docker-compose.yml(全家桶一键启动)
在项目根目录创建 docker-compose.yml:
yaml
services:
mysql:
image: mysql:8.0
container_name: blog-mysql
ports:
- "3307:3306"
volumes:
- D:/Develop/DockerData/Personal/docker-mysql:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: blog
restart: unless-stopped
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
networks:
- blog-net
redis:
image: redis:7-alpine
container_name: blog-redis
ports:
- "6380:6379"
volumes:
- D:/Develop/DockerData/Personal/docker-redis:/data
command: redis-server --requirepass 123456
restart: unless-stopped
networks:
- blog-net
blog-server:
image: blog-server:latest # 本地 docker build -t blog-server .,服务器 docker load,同一份 compose 两边通用
container_name: blog-server
ports:
- "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/upload
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_started
restart: unless-stopped
networks:
- blog-net
blog-ui:
image: blog-ui:latest
container_name: blog-ui
ports:
- "80:80"
depends_on:
- blog-server
restart: unless-stopped
networks:
- blog-net
volumes:
blog-upload: # 命名卷:Docker 管理,不用关心路径
networks:
blog-net:
driver: bridge # 自定义桥接网络:容器间用容器名互访
5.1 环境变量的几种管理方式
上面这个 docker-compose.yml 把密码直接写在文件里------对入门来说最直观,但有两个问题:一是提交到 Git 会泄露密码,二是项目多了改密码要到处翻。实际开发中,根据场景有不同的做法。
方式一:全部写在 yml 里(上面就是)
适合本地快速验证,复制粘贴就能跑。缺点也明显------密码明文,不能提交 Git。
方式二:抽离到 .env 文件(推荐日常开发)
把敏感变量抽到 .env 文件里,docker-compose.yml 用 ${变量名} 引用。
项目根目录创建 .env:
bash
# 数据库
MYSQL_ROOT_PASSWORD=root
MYSQL_DATABASE=blog
# Redis
REDIS_PASSWORD=123456
# blog-server
DB_HOST=mysql
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=root
REDIS_HOST=redis
REDIS_PORT=6379
UPLOAD_PATH=/app/upload
JWT_SECRET=LuckyBlogSecretKeyForJWTTokenGenerationMustBe256BitsLong
docker-compose.yml 改为引用变量:
yaml
services:
mysql:
image: mysql:8.0
container_name: blog-mysql
ports: ["3307:3306"]
volumes: [D:/Develop/DockerData/Personal/docker-mysql:/var/lib/mysql]
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
restart: unless-stopped
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
networks: [blog-net]
redis:
image: redis:7-alpine
container_name: blog-redis
ports: ["6380:6379"]
volumes: [D:/Develop/DockerData/Personal/docker-redis:/data]
command: redis-server --requirepass ${REDIS_PASSWORD}
restart: unless-stopped
networks: [blog-net]
blog-server:
image: blog-server:latest
container_name: blog-server
ports: ["8081:8080"]
volumes: [blog-upload:/app/upload]
environment:
SPRING_PROFILES_ACTIVE: docker
DB_HOST: ${DB_HOST}
DB_PORT: ${DB_PORT}
DB_USERNAME: ${DB_USERNAME}
DB_PASSWORD: ${DB_PASSWORD}
REDIS_HOST: ${REDIS_HOST}
REDIS_PORT: ${REDIS_PORT}
REDIS_PASSWORD: ${REDIS_PASSWORD}
UPLOAD_PATH: ${UPLOAD_PATH}
JWT_SECRET: ${JWT_SECRET}
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_started
restart: unless-stopped
networks: [blog-net]
blog-ui:
image: blog-ui:latest
container_name: blog-ui
ports: ["80:80"]
depends_on: [blog-server]
restart: unless-stopped
networks: [blog-net]
volumes:
blog-upload:
networks:
blog-net:
driver: bridge
一定要把 .env 加到 .gitignore 里,避免提交到 Git:
gitignore
# .gitignore
.env
同时提交一个 .env.example 给其他人参考(里面填假值或不填):
bash
# .env.example
MYSQL_ROOT_PASSWORD=你的密码
MYSQL_DATABASE=blog
REDIS_PASSWORD=你的密码
# ... 其他变量
docker compose 启动时自动读取 同目录下的
.env文件,不需要额外配置。环境变量只在容器首次启动时生效,启动后改.env需要docker compose down && docker compose up -d重建容器。
方式三:CI/CD 变量注入(上服务器推荐)
.env 文件虽然解决了提交 Git 的问题,但服务器上仍然存了一个明文文件。更好的做法是:
yaml
# docker-compose.yml 里依然用 ${变量}
# 但不依赖 .env 文件,而是由 CI/CD 系统在部署时注入
yaml
# GitHub Actions 示例
- name: Deploy
run: docker compose up -d
env:
MYSQL_ROOT_PASSWORD: ${{ secrets.MYSQL_ROOT_PASSWORD }}
REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD }}
密码存在 GitHub Secrets / GitLab CI Variables 里,加密存储,部署时注入到容器的环境变量。服务器上没有任何文件包含明文密码。
方式四:Secrets Manager(大厂生产环境)
大厂会用专门的密钥管理服务(AWS Secrets Manager、HashiCorp Vault、Azure Key Vault),密码加密存储 + 访问审计 + 自动轮换。部署工具在启动容器时调用 API 拉取密码,直接注入环境变量,密码不落在任何文件里。
总结:你该用哪种
| 方式 | 适用场景 | 安全等级 |
|---|---|---|
| 方式一:全部写 yml 里 | 本地快速验证 | ⭐ |
方式二:.env 文件抽离 |
日常开发、个人项目 | ⭐⭐ |
| 方式三:CI/CD 注入 | 上服务器的项目 | ⭐⭐⭐⭐ |
| 方式四:Secrets Manager | 大厂生产、合规要求 | ⭐⭐⭐⭐⭐ |
当前项目推荐方式二 (
.env+.gitignore),平衡了安全性和操作复杂度。等真正部署到服务器时再升级到方式三。
关键知识点:
1. 为什么统一用
image:---build:vsimage:的详细对比、docker 与 docker compose 在各阶段的完整分析,见第五篇:docker 与 docker compose 对比2. 容器间通信用容器名 ---
DB_HOST=mysql用的是 compose 服务名。原理见第四篇第 12 节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:停掉本机服务(简单省事)
powershell
# 管理员 PowerShell
net 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 构建并启动
bash
# 1. 构建镜像(docker build → 读 Dockerfile,打出镜像)
docker build -t blog-server:latest ./blog-server
docker build -t blog-ui:latest ./blog-ui
# 2. 启动(docker compose → 读 docker-compose.yml,镜像跑成容器)
docker compose up -d
# 之后改代码,只需重建镜像并重启对应服务
docker build -t blog-server:latest ./blog-server && docker compose up -d
6.3 验证
bash
# 看四个容器是否都在跑
docker compose ps
# 看后端日志
docker logs -f blog-server
# 测试 API(通过 nginx 代理)
curl http://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 容器能正常读写:
java
@RestController
public class RedisTestController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@GetMapping("/redis/set")
public String setRedisData() {
stringRedisTemplate.opsForValue().set("docker-test", "Docker Redis 连接成功!");
return "已存入 Redis";
}
@GetMapping("/redis/get")
public String getRedisData() {
return stringRedisTemplate.opsForValue().get("docker-test");
}
}
访问 /redis/set 存数据,/redis/get 取数据,能取到说明 Redis 通了。
7. 部署到服务器
7.1 导出镜像
bash
docker save -o blog-server.tar blog-server:latest
docker save -o blog-ui.tar blog-ui:latest
7.2 上传到服务器
通过 FinalShell 或宝塔面板,把 blog-server.tar、blog-ui.tar、docker-compose.yml 传到服务器上。
7.3 服务器上导入并启动
服务器上需要拉取中间件镜像(MySQL、Redis):
bash
# 拉取中间件镜像(服务器也要配镜像加速器)
docker pull mysql:8.0
docker pull redis:7-alpine
# 导入你自己的镜像
docker load -i blog-server.tar
docker load -i blog-ui.tar
# 修改 docker-compose.yml 中卷路径为 Linux 路径(如 /data/docker-mysql)
# docker-compose.yml 用的是 image: 不是 build:,本地服务器同一份,不用改
docker compose up -d
服务器部署时,建议删掉 docker-compose.yml 中 MySQL 和 Redis 的
ports映射------生产环境只暴露前端 80/443 就够了。
注意 .tar vs .tar.gz: 如果镜像文件后缀是 .tar.gz,两种方式导入:
bash
# 方式一:管道解压导入(最可靠)
gunzip -c blog-server.tar.gz | docker load
# 方式二:直接用 load(部分新版 Docker 支持)
docker load -i blog-server.tar.gz
.tar被改后缀成.tar.gz会导致gunzip -c失败,用file 文件名可查看真实类型。
docker cp:容器和宿主机互传文件
bash
# 从宿主机复制到容器
docker cp app.jar blog-server:/app/app.jar
# 从容器复制到宿主机(比如导出日志)
docker cp blog-server:/app/logs ./logs
# 容器内目录结构参考:
# 后端应用:取决于 Dockerfile 的 WORKDIR(本例为 /app)
# 前端静态文件:/usr/share/nginx/html(Nginx 默认)
8. 命令速查
完整命令手册见 第六篇:常用命令速查。
9. 常见问题
| 问题 | 解决 |
|---|---|
| 端口被占用 | 停掉 Windows 本机的 MySQL/Redis 服务,或岔开端口映射(如 3307) |
| 容器启动后立即退出 | docker logs 容器名 看错误日志,通常缺环境变量或端口冲突 |
| 后端连不上数据库 | 检查 DB_HOST=mysql(容器名)且 DB_PORT=3306(容器内端口),不是宿主机端口 3307 |
| 构建慢 | 检查 .dockerignore 有没有排除 node_modules / target |
| 镜像拉不下来 | 检查镜像加速器配了没有,重启 Docker |
| 改代码不生效 | 先 docker build -t 镜像名 . 重建镜像,再 docker compose up -d 重启 |
| Redis 客户端连不上 | 填 localhost,端口填宿主机映射端口(如 6380),不是容器 IP |
| 容器名重复 | 容器名同一机器唯一,先 docker rm 容器名 再重建 |
容器卡死,stop/restart/kill 都不响应: 详见第四篇第 11 节方法。