实战用例(多容器Docker化设计)
本文将通过一个具体的电商项目用例,详细展示如何将 Spring Boot 后端、Vue 管理端、Uniapp 用户端(H5)以及 MySQL 数据库 进行容器化,并使用 Docker Compose 进行统一管理。
1. 项目背景与需求
将四个部分分别容器化,并通过一个命令启动整个系统,同时保证开发与生产环境的一致性。
- Java 后端:提供 RESTful API,使用 Spring Boot 框架,连接 MySQL 数据库
- Vue 管理端:基于 Vue 3 的后台管理系统,供管理员操作
- Uniapp 用户端:基于 uni-app 开发的跨端应用(H5 + 小程序),用户通过浏览器访问 H5 版本
- MySql 数据库:MySQL 8.0,存储业务数据。
2. 服务拆分与容器化设计
根据项目需求,将系统拆分为四个独立服务,每个服务运行在独立的容器中,通过 Docker Compose 统一编排。设计如下:

2.1 数据库独立(有状态服务)
- 为什么独立?
数据库是有状态服务,需要持久化存储,不能随着容器的重启而丢失数据。
- 实现方式
使用官方 MySQL 8.0 镜像,通过 Docker 卷(volume)挂载数据库文件到宿主机,保证数据持久化。
- 健康检查
设置健康检查,确保后端服务在数据库真正就绪后再启动,避免因数据库未准备好导致后端启动失败。
2.2 Java 后端(无状态服务)
- 为什么无状态?
后端应用不保存用户会话等状态数据,可以横向扩展,容器重启不影响业务。
- 实现方式
- 将 Spring Boot 应用打包为 JAR 包,运行在 JRE 环境中。
- 采用多阶段构建 :
第一阶段 使用 Maven 镜像编译打包,
第二阶段 使用轻量级 JRE 镜像运行,显著减小最终镜像体积。- 通过环境变量注入数据库连接信息(如数据库地址、用户名、密码),实现配置与镜像分离,镜像可在不同环境(开发、测试、生产)复用。
- 注意事项
Spring Boot 的配置文件(如application.properties )中应使用占位符 ${} 引用环境变量,这样容器运行时可以通过 -e 或 Compose 的 environment 注入实际值。
2.3 前端静态服务(Vue 管理端 & Uniapp 用户端)
- 为什么用 Nginx 托管静态文件?
前端项目经过构建后生成的是纯静态文件 (HTML、CSS、JS),使用高性能的 Nginx 作为 Web 服务器,既能高效地处理静态资源请求,又能方便地配置反向代理解决跨域问题。
- 实现方式
- 同样采用多阶段构建 :
第一阶段 使用 Node 镜像安装依赖并执行构建命令,生成 dist 目录(或类似输出目录,根据实际项目配置调整);
第二阶段 将构建产物复制到 Nginx 镜像的静态文件目录(如 /usr/share/nginx/html)- 自定义 Nginx 配置,添加反向代理规则,将前端发往 /api 的请求转发到后端服务,从而绕过浏览器的同源策略限制。
2.4 网络与通信
- 容器网络
所有服务置于同一个 自定义 Docker 网络(如 ecommerce-net)中,服务名可直接作为域名相互访问
例如:
后端可以通过 mysql:3306 访问数据库,前端 Nginx 可以通过 http://backend:8080 代理到后端。
- 跨域解决方案
前端 Nginx 配置反向代理,将 /api 路径的请求转发到后端,对浏览器来说请求的是同源地址,自然不存在跨域问题。
2.5 编排管理
- 使用 Docker Compose 定义服务、网络和卷,通过一个 docker-compose.yml 文件实现一键启动/停止。
- 开发环境 vs 生产环境 :
开发时可以挂载源码目录实现热更新(如 Vue 项目可使用 npm run serve 配合端口映射);
生产环境则使用构建好的镜像,移除挂载,保证部署的稳定性。
3. 项目目录结构
合理的目录结构有助于维护和扩展。以下是一个推荐的项目组织方式:

4. 各服务的 Dockerfile 详解
4.1 Java 后端 ( backend / Dockerfile )
Spring Boot 应用默认支持从环境变量覆盖配置文件中的属性,因此只需在 Dockerfile 中声明默认值(可选),并在运行时通过 -e 或 Compose 注入实际值。
java
# Dockerfile 文件
# 构建阶段:使用 Maven 镜像编译打包(给这个阶段起名叫 builder,后面可以引用)
FROM maven:3.8-openjdk-11 AS builder
# 设置工作目录为 /app
WORKDIR /app
# 复制 pom.xml
COPY pom.xml .
# 预下载所有依赖到本地缓存(利用 Docker 缓存:如果 pom.xml 没变,这层缓存可以复用,加快构建)
RUN mvn dependency:go-offline
# 复制源码目录
COPY src ./src
# 编译打包,生成 JAR 文件(-DskipTests 跳过测试,加快构建)
RUN mvn clean package -DskipTests
# 运行阶段:使用轻量级 JRE 镜像(只包含运行时,不含 Maven 和编译器)
FROM openjdk:11-jre-slim
WORKDIR /app
# 设置默认环境变量(可被运行时覆盖)
ENV SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/ecommerce \
SPRING_DATASOURCE_USERNAME=root \
SPRING_DATASOURCE_PASSWORD=secret \
SPRING_PROFILES_ACTIVE=prod
# 从构建 builder 阶段复制 JAR 包,重命名为 app.jar
COPY --from=builder /app/target/*.jar app.jar
# 暴露应用端口
EXPOSE 8080
# 启动命令
ENTRYPOINT ["java", "-jar", "app.jar"]
为什么要这样做?
多阶段构建
第一阶段有 Maven 和 JDK ,体积较大,但编译完成后,我们只需要 JRE 和最终的 JAR 包,
第二阶段使用 jre-slim 镜像,最终镜像仅包含运行时所需,体积可减小到 100MB 左右。dependency:go-offline
提前下载所有依赖,后续如果只修改源码而不改变 pom.xml,构建时会直接使用缓存层,加快构建速度。
环境变量
通过环境变量动态配置数据库连接,镜像与配置分离,符合 12-Factor 原则。
环境变量说明:SPRING_DATASOURCE_URL:数据库 JDBC 连接串(如 jdbc:mysql://mysql:3306/ecommerce)
SPRING_DATASOURCE_USERNAME:数据库用户名
SPRING_DATASOURCE_PASSWORD:数据库密码
SPRING_PROFILES_ACTIVE:激活的 Spring Profile(如 dev、prod)
在 application.properties 中只需使用变量占位符
java
spring.datasource.url=${SPRING_DATASOURCE_URL}
spring.datasource.username=${SPRING_DATASOURCE_USERNAME}
spring.datasource.password=${SPRING_DATASOURCE_PASSWORD}
4.2 Vue 管理端 (web / Dockerfile)
Vue 项目通常需要根据不同环境(开发、测试、生产)配置不同的 API 地址、CDN 路径等。由于前端构建后生成的是静态文件,无法像后端那样在运行时直接读取操作系统环境变量,因此需要通过构建时注入或运行时替换的方式来实现环境配置。下面提供两种主流方案。
方案一:构建时注入(通过 Docker ARG)
这种方法在 Docker 构建阶段 将环境变量传递给前端构建工具(如 Vite、Webpack),在编译时直接替换代码中的变量。最终生成的镜像已包含特定环境的配置,不同环境需要构建不同的镜像。
javascript
# Dockerfile 文件
# 构建阶段
FROM node:16 AS builder
WORKDIR /app
# 定义构建参数(可设置默认值)
ARG VUE_APP_API_BASE_URL=/api
ARG VUE_APP_CDN_DOMAIN=
# 将构建参数写入环境变量,供构建工具使用
ENV VUE_APP_API_BASE_URL=${VUE_APP_API_BASE_URL} \
VUE_APP_CDN_DOMAIN=${VUE_APP_CDN_DOMAIN}
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# 运行阶段
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
前端项目中的使用(以 Vue 3 + Vite 为例):
在 .env 文件中定义变量名,或直接在代码中通过 import.meta.env.VITE_XXX (Vite)或 process.env.VUE_APP_XXX (Vue CLI)引用。构建时会自动替换为传入的值。
在 docker-compose.yml 文件 中传递 构建参数
javascript
web:
build:
context: ./web
args:
VUE_APP_API_BASE_URL: /api
VUE_APP_CDN_DOMAIN: https://cdn.example.com
- 优点:简单直接,镜像构建完成后无需额外处理。
- 缺点:不同环境需要构建不同镜像,难以实现"一次构建,多处部署"。
方案二:运行时注入(通过 envsubst + Nginx)
此方案利用 Nginx 的 envsubst 命令,在容器启动时动态替换 前端静态文件中的占位符(如 {{API_BASE_URL}} ),从而实现同一个镜像在不同环境中通过环境变量配置。这种方法更适合生产环境的多环境部署 (如测试、预发、生产共用同一个镜像)。
javascript
# Dockerfile 文件
# 构建阶段
FROM node:16 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
# 构建时使用占位符,例如将 API 基础地址替换为 {{API_BASE_URL}}
RUN npm run build
# 运行阶段
FROM nginx:alpine
# 安装 envsubst(Alpine 镜像极简,默认没有 envsubst 命令,需要安装)
RUN apk add --no-cache gettext
# 复制构建产物到临时目录,启动时通过脚本替换
COPY --from=builder /app/dist /tmp/dist
# 复制启动脚本
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
# 复制 Nginx 配置(无需特殊处理)
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
ENTRYPOINT ["/docker-entrypoint.sh"]
配置 docker-entrypoint.sh 启动脚本文件
javascript
#!/bin/sh
set -e
# 将环境变量中的值替换到 JS 文件中
# 假设构建时我们将 API 基础地址写成了 {{API_BASE_URL}} 占位符
if [ -n "$API_BASE_URL" ]; then
find /tmp/dist -type f -name "*.js" -exec sed -i "s|{{API_BASE_URL}}|$API_BASE_URL|g" {} \;
fi
# 复制替换后的文件到 Nginx 目录
cp -r /tmp/dist/* /usr/share/nginx/html/
# 启动 Nginx
exec nginx -g 'daemon off;'
前端项目中需将动态配置的位置写成占位符,例如:
javascript
// config.js
const API_BASE_URL = '{{API_BASE_URL}}';
在 docker-compose.yml 文件 中传递 运行时环境变量
javascript
web:
build: ./web
environment:
API_BASE_URL: /api # 或 http://backend:8080
- 优点:镜像与环境无关,只需构建一次,可在不同环境通过环境变量差异化配置。
- 缺点:需要额外编写启动脚本,且替换逻辑需谨慎(避免误替换)。
两种方案的选择建议
- 在开发环境 ,可使用方案一(构建时注入),配合 Docker Compose 的构建参数快速切换。
- 在生产环境 ,推荐方案二(运行时注入),实现 CI/CD 流水线中"构建一次,到处运行",避免因环境差异导致的构建错误。
无论采用哪种方案,Nginx 的反向代理配置保持不变,依然通过 location /api/ 将请求转发到后端。
配套的 nginx.conf(位于 web/ 目录下):
javascript
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://backend: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;
}
}
}
关键点解释
- try_files uri uri/ /index.html :解决 Vue 单页应用的路由问题,当请求的资源不存在时,返回 index.html,由前端路由处理
- /api/ 反向代理:将前端对 /api 的请求转发到后端容器,后端容器监听 8080 端口,且服务名为 backend,在 Docker 网络中可解析。
4.3 Uniapp 用户端 (mobile/Dockerfile)
Uniapp 构建 H5 的方式与 Vue 项目类似,同样需要处理环境变量。可完全参考 Vue 管理端的两种方案,只需根据实际项目调整构建命令和输出目录。
常见差异点
- 构建命令可能为 npm run build:h5
- 输出目录可能为 dist/build/h5 或 dist/dev/h5,需在 Dockerfile 中正确指定。
5. Docker Compose 统一编排
javascript
version: '3.8' # 注意:推荐使用 3.8 版本,支持较新的特性,如 depends_on 的 condition
services:
mysql:
image: mysql:8.0
container_name: ecommerce-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ecommerce
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
volumes:
- mysql-data:/var/lib/mysql # 持久化数据卷
- ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql # 初始化脚本(可选)
ports:
- "3306:3306" # 开发时映射端口便于客户端工具连接,生产环境建议移除或仅绑定内网
networks:
- ecommerce-net
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
backend:
build: ./backend
container_name: ecommerce-backend
restart: unless-stopped
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/ecommerce?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
SPRING_DATASOURCE_USERNAME: ${MYSQL_USER}
SPRING_DATASOURCE_PASSWORD: ${MYSQL_PASSWORD}
SPRING_PROFILES_ACTIVE: prod
depends_on:
mysql:
condition: service_healthy # 等待 MySQL 健康检查通过后再启动,避免因数据库未就绪导致后端启动失败
networks:
- ecommerce-net
# 开发环境可挂载源码实现热部署(需配合 spring-boot-devtools)
# volumes:
# - ./backend/target:/app
web:
build:
context: ./web
# 方案一:构建时注入(使用 args)
args:
VUE_APP_API_BASE_URL: /api
VUE_APP_CDN_DOMAIN: https://cdn.example.com
# 方案二:运行时注入(使用 environment)
environment:
API_BASE_URL: /api # 启动脚本会读取此变量替换占位符
container_name: ecommerce-web
restart: unless-stopped
ports:
- "8081:80" # 将宿主机 8081 端口映射到容器 80 端口,供外部访问管理端
networks:
- ecommerce-net
depends_on:
- backend # 仅控制启动顺序,不等待后端就绪(Nginx 可独立提供静态文件)
mobile:
build: ./mobile
container_name: ecommerce-mobile
restart: unless-stopped
ports:
- "8082:80" # 用户端 H5 映射到宿主机 8082 端口
networks:
- ecommerce-net
depends_on:
- backend
volumes:
mysql-data:
driver: local # 使用本地驱动创建数据卷,数据存储在宿主机 /var/lib/docker/volumes/ 下
networks:
ecommerce-net:
driver: bridge # 创建自定义桥接网络,实现容器间通过服务名通信
5.1 关键配置解释
| 配置项 | 作用/说明 | 关键点/注意事项 |
|---|---|---|
| version: '3.8' | 指定Compose文件格式版本 | 推荐3.8以上,支持depends_on的condition特性,确保Docker Engine版本兼容(19.03.0+) |
| services.mysql.image | 使用官方MySQL 8.0镜像 | 无需构建,直接拉取;版本可固定如mysql:8.0.30避免意外升级 |
| services.mysql.container_name | 固定容器名称 | 便于管理,但注意同一宿主机下不能重名 |
| services.mysql.restart: unless-stopped | 容器退出时自动重启(除非手动停止) | 保证服务高可用,适合生产环境 |
| services.mysql.environment | 通过环境变量初始化数据库 | 使用${VAR}从.env读取敏感信息,避免硬编码;MYSQL_DATABASE自动创建库 |
| services.mysql.volumes | 数据持久化挂载 | mysql-data:/var/lib/mysql保证数据不丢失;init.sql挂载到初始化目录,首次启动自动执行 |
| services.mysql.ports | 映射端口到宿主机 | 开发时方便客户端工具连接;生产应移除或绑定内网IP(如127.0.0.1:3306:3306) |
| services.mysql.networks | 加入自定义网络 | 服务名mysql可供同一网络其他容器访问 |
| services.mysql.healthcheck | 健康检查命令 | 通过mysqladmin ping检测服务真正就绪;interval、timeout、retries控制检查行为 |
| services.backend.build | 指定构建上下文 | 使用./backend目录下的Dockerfile构建镜像 |
| services.backend.environment | 传入后端环境变量 | 覆盖application.properties中的占位符;数据库连接串使用服务名mysql |
| services.backend.depends_on | 控制启动顺序 | condition: service_healthy确保MySQL健康后才启动后端,避免连接失败 |
| services.backend.networks | 加入同一网络 | 通过服务名mysql访问数据库 |
| services.backend.volumes (注释) | 开发时挂载源码 | 需配合spring-boot-devtools实现热部署;生产环境必须移除 |
| services.web.build | Vue管理端构建配置 | context指定路径;args用于构建时注入(方案一),environment用于运行时注入(方案二) |
| services.web.args | 构建参数传递 | 对应Dockerfile中的ARG,构建时替换前端变量,不同环境需构建不同镜像 |
| services.web.environment | 运行时环境变量 | 容器启动时由docker-entrypoint.sh读取,替换静态文件中的占位符,实现镜像复用 |
| services.web.ports | 端口映射 | 将宿主机8081端口映射到容器80端口,外部访问管理端 |
| services.web.depends_on | 依赖后端 | 仅控制启动顺序,不等待后端就绪;Nginx可独立提供静态文件 |
| services.web.networks | 加入同一网络 | 使Nginx能通过http://backend:8080代理后端API |
| services.mobile | Uniapp用户端 | 配置与web类似,映射8082端口;构建命令和输出目录需根据实际项目调整 |
| volumes.mysql-data | 定义数据卷 | driver: local使用本地存储,数据保存在/var/lib/docker/volumes/下 |
| networks.ecommerce-net | 创建自定义网络 | driver: bridge实现单机容器通信;服务名作为DNS解析,解耦IP依赖 |
| .env文件 | 集中管理环境变量 | 存放敏感信息(密码)和环境差异配置;必须加入.gitignore避免泄露 |
5.2 配套的 .env 文件
- 作用:集中管理敏感信息和环境差异配置,避免硬编码在 Compose 文件中。
- 注意:.env 文件不应提交到版本控制(应加入 .gitignore),生产环境需单独维护。
javascript
MYSQL_ROOT_PASSWORD=root123
MYSQL_USER=ecommerce_user
MYSQL_PASSWORD=ecommerce_pass
- 如果同时保留 args 和 environment,则优先使用 environment 运行时注入(前提是镜像支持)。
- 实际项目中可根据需要选择一种方式,或通过 Compose 文件分离(如 docker-compose.override.yml 用于开发,使用 args;生产使用 environment)。
5.3 实战注意事项
- 版本兼容性
确保 Docker Engine 版本支持 Compose file 3.8(Docker 19.03.0+)。
- 环境变量覆盖
Compose 中的 environment 会覆盖镜像中 ENV 设置的默认值,也覆盖 .env 文件中同名变量。
- 构建参数 vs 环境变量
args 仅在构建时有效,不会传递到运行时的容器中;environment 在容器运行时生效。
前端两种方案的选择需根据实际需求。
- 网络模式
自定义桥接网络提供了服务发现和隔离,生产环境如需跨主机通信,可改用 overlay 网络(需 Swarm 模式)。
- 健康检查的重要性
特别是数据库,直接依赖启动顺序可能不够,因为容器启动后服务可能还在初始化。使用健康检查可确保服务真正就绪。
- 日志管理
生产环境建议配置日志驱动限制大小,如:
javascript
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
- 资源限制
- 可为每个服务设置 CPU、内存限制,防止单个容器耗尽主机资源。
- 注意:deploy 仅用于 Swarm 模式,Compose 单独使用时可用 mem_limit 等旧版字段。
javascript
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M