【Docker】实战用例:前后端分离项目多容器Docker化设计

实战用例(多容器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 数据库独立(有状态服务)

  1. 为什么独立?

数据库是有状态服务,需要持久化存储,不能随着容器的重启而丢失数据。

  1. 实现方式

使用官方 MySQL 8.0 镜像,通过 Docker 卷(volume)挂载数据库文件到宿主机,保证数据持久化。

  1. 健康检查

设置健康检查,确保后端服务在数据库真正就绪后再启动,避免因数据库未准备好导致后端启动失败。

2.2 Java 后端(无状态服务)

  1. 为什么无状态?

后端应用不保存用户会话等状态数据,可以横向扩展,容器重启不影响业务。

  1. 实现方式
  • 将 Spring Boot 应用打包为 JAR 包,运行在 JRE 环境中。
  • 采用多阶段构建
    第一阶段 使用 Maven 镜像编译打包,
    第二阶段 使用轻量级 JRE 镜像运行,显著减小最终镜像体积。
  • 通过环境变量注入数据库连接信息(如数据库地址、用户名、密码),实现配置与镜像分离,镜像可在不同环境(开发、测试、生产)复用。
  1. 注意事项

Spring Boot 的配置文件(如application.properties )中应使用占位符 ${} 引用环境变量,这样容器运行时可以通过 -e 或 Compose 的 environment 注入实际值。

2.3 前端静态服务(Vue 管理端 & Uniapp 用户端)

  1. 为什么用 Nginx 托管静态文件?

前端项目经过构建后生成的是纯静态文件 (HTML、CSS、JS),使用高性能的 Nginx 作为 Web 服务器,既能高效地处理静态资源请求,又能方便地配置反向代理解决跨域问题。

  1. 实现方式
  • 同样采用多阶段构建
    第一阶段 使用 Node 镜像安装依赖并执行构建命令,生成 dist 目录(或类似输出目录,根据实际项目配置调整);
    第二阶段 将构建产物复制到 Nginx 镜像的静态文件目录(如 /usr/share/nginx/html)
  • 自定义 Nginx 配置,添加反向代理规则,将前端发往 /api 的请求转发到后端服务,从而绕过浏览器的同源策略限制。

2.4 网络与通信

  1. 容器网络

所有服务置于同一个 自定义 Docker 网络(如 ecommerce-net)中,服务名可直接作为域名相互访问

例如:

后端可以通过 mysql:3306 访问数据库,前端 Nginx 可以通过 http://backend:8080 代理到后端。

  1. 跨域解决方案

前端 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)

此方案利用 Nginxenvsubst 命令,在容器启动时动态替换 前端静态文件中的占位符(如 {{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_oncondition特性,确保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检测服务真正就绪;intervaltimeoutretries控制检查行为
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 实战注意事项

  1. 版本兼容性

确保 Docker Engine 版本支持 Compose file 3.8(Docker 19.03.0+)。

  1. 环境变量覆盖

Compose 中的 environment 会覆盖镜像中 ENV 设置的默认值,也覆盖 .env 文件中同名变量。

  1. 构建参数 vs 环境变量

args 仅在构建时有效,不会传递到运行时的容器中;environment 在容器运行时生效。

前端两种方案的选择需根据实际需求。

  1. 网络模式

自定义桥接网络提供了服务发现和隔离,生产环境如需跨主机通信,可改用 overlay 网络(需 Swarm 模式)。

  1. 健康检查的重要性

特别是数据库,直接依赖启动顺序可能不够,因为容器启动后服务可能还在初始化。使用健康检查可确保服务真正就绪。

  1. 日志管理

生产环境建议配置日志驱动限制大小,如:

javascript 复制代码
logging:
  driver: "json-file"
  options:
    max-size: "10m"
    max-file: "3"
  1. 资源限制
  • 可为每个服务设置 CPU、内存限制,防止单个容器耗尽主机资源。
  • 注意:deploy 仅用于 Swarm 模式,Compose 单独使用时可用 mem_limit 等旧版字段。
javascript 复制代码
deploy:
  resources:
    limits:
      cpus: '0.5'
      memory: 512M
相关推荐
小江的记录本2 小时前
【HashMap】HashMap 系统性知识体系全解(附《HashMap 面试八股文精简版》)
java·前端·后端·容器·面试·hash·哈希
邓草2 小时前
Ubuntu修改docker数据目录的方法
ubuntu·docker·eureka
熠速2 小时前
CI/CD功能介绍
运维·ci/cd
李长渊哦2 小时前
Nginx 反向代理实战:解决 IPv6 报错与跨网段访问指南
运维·nginx
Anthony_CH2 小时前
window系统无虚拟化安装Docker的方式
docker·容器·eureka
信创工程师-小杨2 小时前
银河麒麟SP3如何离线部署二进制docker
运维·docker·容器
小疙瘩2 小时前
本文记录Windows11安装Docker(Docker Desktop)的详细步骤
运维·docker·容器
沐伊~2 小时前
LINUX基础篇(Ubuntu):
linux·运维·服务器
艾莉丝努力练剑2 小时前
System V IPC底层原理详解
linux·运维·服务器·网络·c++·人工智能·学习