Docker Compose 部署方案设计方法详解(容器编排实战)

部署方案最终定为四个容器的 Docker Compose 编排:前端 Nginx 容器、后端 Spring Boot 容器、MySQL 容器、Redis 容器,外加一个自定义 bridge 网络和一个独立数据卷。后端和前端镜像都用多阶段构建,运行镜像里不含 Maven、Node 和源码,后端镜像从 800MB 级压到 200MB 出头,前端镜像只有 25MB 左右。SSE 长连接在容器网络里天然可用,不需要额外开端口,唯一要处理的是心跳与健康检查的配合。下面把镜像构建、编排文件、网络与数据卷设计逐块展开。

一、整体架构

容器 镜像来源 暴露端口 职责
frontend 多阶段构建(node → nginx) 80 托管 dist + 反代 /api 与 /sse
backend 多阶段构建(maven → jre) 仅内网 Spring Boot 多阶段生成 + SSE
mysql mysql:8.0 官方镜像 仅内网 文章与任务数据
redis redis:7-alpine 官方镜像 仅内网 缓存、任务进度、限流计数

对外只暴露 Nginx 的 80 端口,MySQL、Redis、后端全部收在容器网络内部。这样安全组只需开 80,公网扫不到 3306 和 6379。

二、后端镜像:多阶段构建

后端是 Spring Boot + JDK 17,用 Maven 构建。第一阶段用 maven:3.9-eclipse-temurin-17 编译,第二阶段切到 eclipse-temurin:17-jre-alpine 只放运行环境:

#build stage
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn package -DskipTests -B

#runtime stage
FROM eclipse-temurin:17-jre-alpine AS runtimeWORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
EXPOSE 8080
ENV TZ=Asia/Shanghai \
    JAVA_OPTS="-Xms512m -Xmx1024m -XX:MaxMetaspaceSize=256m"
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

两个细节:COPY pom.xml 和 mvn dependency:go-offline 先跑,依赖层被 Docker 缓存,改源码后重新构建只重编译业务代码,构建时间从 5 分钟降到 30 秒;JVM 参数走环境变量 JAVA_OPTS,不同机器内存不同,改 compose 里的 environment 即可,不用重打镜像。

三、前端镜像:多阶段构建

前端是 Vue 3 + Vite。第一阶段用 node 构建出 dist,第二阶段用 nginx 托管:

#build stage
FROM node:20-alpine AS builder
WORKDIR /app
RUN npm config set registry https://registry.npmmirror.com
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

#runtime stage
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

npm ci 按 lock 文件精确安装,比 npm install 快且可复现。nginx.conf 里最关键的两行:try_files 兜 SPA 路由,location /sse/ 关缓冲开长连接。

server {
    listen 80;
    server_name _;
    root /usr/share/nginx/html;

    location / {
        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;
    }

    location /sse/ {
        proxy_pass http://backend:8080/sse/;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_buffering off;
        proxy_cache off;
        proxy_read_timeout 3600s;
        add_header X-Accel-Buffering no;
        gzip off;
    }
}

backend 是 compose 里的服务名,Docker 内置 DNS 会自动解析成容器 IP,容器重建 IP 变化也不影响。

四、Docker Compose 编排

services:
  mysql:
    image: mysql:8.0
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ai_writer
      TZ: Asia/Shanghai
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci
    volumes:
      - mysql_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p${MYSQL_ROOT_PASSWORD}"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    restart: always
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  backend:
    build: ./backend
    restart: always
    environment:
      SPRING_PROFILES_ACTIVE: prod
      DB_USER: ${DB_USER}
      DB_PASSWORD: ${DB_PASSWORD}
      REDIS_PASSWORD: ${REDIS_PASSWORD}
      LLM_API_KEY: ${LLM_API_KEY}
      JAVA_OPTS: "-Xms512m -Xmx1024m"
      TZ: Asia/Shanghai
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy

  frontend:
    build: ./frontend
    restart: always
    ports:
      - "80:80"
    depends_on:
      - backend

volumes:
  mysql_data:
  redis_data:

4.1 启动顺序:depends_on 的坑

depends_on 默认只等容器”启动”,不等”就绪”。MySQL 容器起来了,但 mysqld 可能还在初始化,后端去连直接 Connection refused,然后容器崩溃、compose 拉起、再失败,循环崩溃。解法是给 MySQL 和 Redis 加 healthcheck,后端依赖写 condition: service_healthy,等健康检查通过再启动后端。后端侧再加一层保险:数据库连接配置 fail-fast: false,短时不可用先启动再重连。

4.2 数据持久化

MySQL 的 /var/lib/mysql 和 Redis 的 /data 用命名卷挂载。容器随时可以删了重建,数据留在卷里不丢。环境变量统一走 .env 文件,MYSQL_ROOT_PASSWORD 这类敏感值不写进 compose 和镜像。

五、SSE 长连接在容器网络里的注意点

SSE 走 HTTP,容器间通信没有额外限制,proxy_read_timeout 3600s 在容器网络里依然生效。三个注意点:后端容器里必须设 TZ=Asia/Shanghai,否则定时心跳与日志时区漂移;ulimit 与文件描述符按并发连接数调,SSE 长连接吃文件描述符,/etc/security/limits.conf 和 Docker 默认的 1024 连接数对高并发不够;日志挂载到宿主机目录,docker compose logs -f 之外,容器删了日志不丢。

六、构建与发布流程

  1. 改代码后,后端 docker compose build backend,前端 docker compose build frontend;
  2. docker compose up -d 增量拉起变化的服务;
  3. docker compose ps 看健康状态,docker compose logs -f backend 盯启动日志;
  4. 验证 SSE:curl -N http://服务器IP/sse/generate?topic=docker,确认流式输出;
  5. 回滚:镜像打 tag(backend:1.2.0),出问题 docker compose up -d --no-deps backend:1.1.0 秒级切换。

这套方案在「企业级 AI 网关」项目上原样复用,网关只是后端镜像换了构建源,compose 的网络、卷、健康检查模板直接拷贝,新项目从零到上线一套 compose 搞定。

七、安全与资源限制

容器化不代表放任不管,三处加固是上线前必须做的。资源限制用 deploy.resources.limits 给每个容器划上限,防止单个服务把整台机器吃满——多阶段生成是内存敏感型任务,后端镜像的堆上限和容器内存上限要留出缓冲区,容器限 2G、JVM 堆 1G,避免 OOM 被杀后无限重启。端口最小化前面已说过,MySQL 和 Redis 不映射到宿主机。镜像层面,运行镜像只保留 JRE 和运行时文件,不包含 Maven、Node、源码,安全扫描的攻击面小得多。

  backend:
    build: ./backend
    restart: always
    deploy:
      resources:
        limits:
          cpus: "2.0"
          memory: 2g

.env 文件不进版本库,敏感配置在 CI 里通过密钥注入。compose 里引用的 LLM_API_KEY 只存在于容器环境变量,镜像和源码里搜不到明文。每天定时 docker compose down 前先确认数据卷正常,容器重建流程已经跑过多次,数据无一丢失。

常见问题(FAQ)

Q1:compose 里后端连不上 MySQL 怎么办?

确认 MySQL 健康检查通过后后端才启动,且连接串主机名用服务名 mysql 而不是 IP。

Q2:前端镜像为什么这么小?

多阶段构建只把 dist 静态文件和 nginx 配置文件拷进运行镜像,没有 Node 和源码。

Q3:容器里 SSE 频繁断连是什么原因?

多半是后端时区或心跳问题导致超时误判,检查 TZ 环境变量并确认 proxy_read_timeout 配置生效。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
赵其鑫的头像赵其鑫管理团队

相关推荐

返回顶部