Docker Compose多容器编排与Nginx集群实战

作者:没有四次元口袋的蓝胖

日期:2026-09-16

标签:Docker, Docker Compose, Nginx, 容器化


写在前面

这是一篇 Docker Compose 多容器编排 + Nginx 集群的实战记录。从 Dockerfile 编写、数据持久化,到 Nginx 反向代理负载均衡,再到 docker-compose.yml 一键编排,完整记录了微服务容器化部署的全过程。


一、项目背景

痛点:团队开发、测试、生产三套环境,Java 应用在不同机器上运行结果不一致(JDK 版本不同、依赖缺失、配置混乱),排查问题效率极低。

目标:

  1. 统一环境 ------ "一次构建,到处运行"
  2. 简化部署 ------ 多服务(Web + MySQL + Redis + Nginx)一键编排
  3. 高可用 ------ Nginx 负载均衡 + 多应用实例

二、Dockerfile 编写与镜像构建

2.1 Spring Boot 应用镜像

dockerfile 复制代码
# 基础镜像:轻量级 JRE 环境
FROM openjdk:8-jre-alpine

# 设置工作目录
WORKDIR /app

# 指定非 root 用户运行(安全最佳实践)
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# 复制 jar 包
COPY target/app.jar /app/app.jar

# 通过环境变量实现多环境隔离(一次构建,多环境运行)
ENV SPRING_PROFILES_ACTIVE=prod

# 暴露端口
EXPOSE 8080

# 切换用户
USER appuser

# 启动命令
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

关键点解析:

指令 说明
FROM openjdk:8-jre-alpine 选择 alpine 版本,镜像体积更小(约 100MB vs 完整 JDK 的 500MB+)
SPRING_PROFILES_ACTIVE 通过环境变量切换配置,避免为不同环境打多个镜像
COPY --chown 指定文件属主为非 root 用户
USER appuser 容器不以 root 运行,遵循最小权限原则

💡 启动时指定环境:

bash 复制代码
docker run -e SPRING_PROFILES_ACTIVE=dev myapp:latest    # 开发环境
docker run -e SPRING_PROFILES_ACTIVE=prod myapp:latest   # 生产环境

2.2 MySQL 自定义镜像

dockerfile 复制代码
FROM mysql:5.7

# 复制自定义配置文件
COPY my.cnf /etc/mysql/conf.d/custom.cnf

# 复制初始化 SQL(首次启动时自动执行)
COPY init.sql /docker-entrypoint-initdb.d/

# 设置字符集环境变量
ENV MYSQL_CHARSET=utf8mb4

自定义 my.cnf 关键配置:

ini 复制代码
[mysqld]
# 字符集
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci

# 连接数
max_connections=500

# 慢查询日志
slow_query_log=1
slow_query_log_file=/var/log/mysql/slow.log
long_query_time=2

# InnoDB 缓冲池(根据服务器内存调整)
innodb_buffer_pool_size=512M

💡 通过自定义 my.cnf,把字符集、连接数、慢查询日志等配置固化到镜像中,避免每次部署后手动修改。

2.3 🎯 面试高频问题

Q:Dockerfile 中 COPY 和 ADD 有什么区别?

特性 COPY ADD
复制本地文件 ✅ ✅
自动解压 tar ❌ ✅
支持远程 URL ❌ ✅
推荐程度 推荐 非必要不用

最佳实践:优先用 COPY,语义更明确。ADD 的自动解压和远程下载功能可能带来不可预期的行为。

Q:为什么不用 root 用户运行容器?

安全。如果容器被攻破,root 用户意味着攻击者拥有容器内的最高权限,可能通过挂载卷等机制影响宿主机。非 root 用户限制了攻击面。


三、数据持久化方案

3.1 为什么需要数据持久化?

容器是"临时"的 ------ 容器删除后,内部数据全部丢失。数据库数据、应用日志、静态资源这些都不能丢。

3.2 Docker Volume 数据卷

bash 复制代码
# 创建命名数据卷
docker volume create mysql-data
docker volume create app-logs
docker volume create nginx-static

# 查看数据卷信息
docker volume inspect mysql-data

3.3 挂载方案

数据 容器内路径 宿主机挂载路径 说明
MySQL 数据 /var/lib/mysql /data/mysql/data 数据库文件,最重要
应用日志 /app/logs /data/app/logs 方便排查问题,不需要进容器看日志
Nginx 静态资源 /usr/share/nginx/html /data/nginx/static 前端文件,宿主机直接更新
Nginx 配置 /etc/nginx/conf.d /data/nginx/conf 方便修改配置,不用重建容器
bash 复制代码
docker run -d \
  -v mysql-data:/var/lib/mysql \
  -v /data/app/logs:/app/logs \
  myapp:latest

3.4 🎯 常见坑点

坑1:MySQL 容器挂载后启动失败

原因:宿主机挂载目录的权限与 MySQL 容器内 mysql 用户不匹配。

解决:

bash 复制代码
# 确保挂载目录的属主正确
chown -R 999:999 /data/mysql/data

坑2:容器重启数据丢了

原因:用了 bind mount(直接挂目录)但容器重建后目录变了,或者忘记挂载 -v 参数。

解决:使用命名 Volume(docker volume create),数据由 Docker 管理,不依赖具体容器。


四、Nginx 反向代理 + 负载均衡 + 动静分离

4.1 Nginx 配置文件

nginx 复制代码
# 定义后端应用集群
upstream app-cluster {
    # 轮询策略(默认)
    server 172.20.0.3:8080 weight=1;
    server 172.20.0.4:8080 weight=1;
    
    # 健康检查:连续 3 次失败标记为不可用
    # max_fails=3 fail_timeout=30s;
}

# 代理缓存配置
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=app_cache:10m max_size=100m inactive=60m;

server {
    listen 80;
    server_name example.com;

    # ===== 动静分离:静态资源直接由 Nginx 处理 =====
    location /static/ {
        alias /usr/share/nginx/html/static/;
        expires 30d;              # 浏览器缓存 30 天
        access_log off;           # 静态资源不记录日志
    }

    location ~* \.(css|js|png|jpg|gif|ico)$ {
        root /usr/share/nginx/html;
        expires 30d;
        access_log off;
    }

    # ===== 动态请求:反向代理到后端集群 =====
    location /api/ {
        proxy_pass http://app-cluster;
        
        # 传递真实客户端信息
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        
        # 代理缓存
        proxy_cache app_cache;
        proxy_cache_valid 200 10m;
        proxy_cache_valid 404 1m;
    }

    # ===== 默认页面 =====
    location / {
        root /usr/share/nginx/html;
        index index.html;
        try_files $uri $uri/ /index.html;
    }
}

4.2 核心配置解析

配置项 作用 面试常问
upstream 定义后端服务器集群 负载均衡策略有哪些?
weight 权重分配,数值越大分配越多请求 怎么实现不均匀负载?
proxy_pass 反向代理转发 proxy_pass 末尾加不加 / 的区别?
expires 30d 浏览器缓存静态资源 减少后端请求压力
proxy_cache Nginx 代理缓存 和浏览器缓存的区别?
try_files 前端路由支持(SPA 应用) Vue/React 路由刷新 404 怎么解决?

4.3 负载均衡策略

nginx 复制代码
upstream app-cluster {
    # 策略1:轮询(默认)
    server 172.20.0.3:8080;
    server 172.20.0.4:8080;

    # 策略2:权重
    server 172.20.0.3:8080 weight=3;  # 性能好的多分配
    server 172.20.0.4:8080 weight=1;

    # 策略3:ip_hash(同一用户始终访问同一节点,用于 Session 保持)
    # ip_hash;

    # 策略4:least_conn(最少连接数,空闲的节点多分配)
    # least_conn;
}

4.4 🎯 面试高频问题

Q:反向代理和正向代理有什么区别?

对比 正向代理 反向代理
代理对象 客户端 服务端
典型场景 VPN、科学上网 Nginx
客户端是否知道真实服务器 ❌ 不知道 ✅ 知道(但不直接访问)
一句话总结 代理客户端请求 代理服务端响应

Q:proxy_pass 末尾加不加 / 有什么区别?

nginx 复制代码
# 不加 /:/api/user → http://app-cluster/api/user(保留 /api 前缀)
location /api/ {
    proxy_pass http://app-cluster;
}

# 加 /:/api/user → http://app-cluster/user(去掉 /api 前缀)
location /api/ {
    proxy_pass http://app-cluster/;
}

五、Docker Compose 多服务编排

5.1 docker-compose.yml

yaml 复制代码
version: '3.8'

services:
  # ===== MySQL 数据库 =====
  mysql:
    image: mysql:5.7
    container_name: mysql
    environment:
      MYSQL_ROOT_PASSWORD: root123
      MYSQL_DATABASE: myapp
      MYSQL_CHARSET: utf8mb4
    volumes:
      - mysql-data:/var/lib/mysql
      - ./mysql/my.cnf:/etc/mysql/conf.d/custom.cnf
      - ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql
    ports:
      - "3306:3306"
    networks:
      - app-network
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: always

  # ===== Redis 缓存 =====
  redis:
    image: redis:6-alpine
    container_name: redis
    command: redis-server --appendonly yes
    volumes:
      - redis-data:/data
    ports:
      - "6379:6379"
    networks:
      - app-network
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: always

  # ===== Spring Boot 应用(实例1)=====
  app1:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: app1
    environment:
      SPRING_PROFILES_ACTIVE: prod
    volumes:
      - app-logs:/app/logs
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - app-network
    restart: always

  # ===== Spring Boot 应用(实例2)=====
  app2:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: app2
    environment:
      SPRING_PROFILES_ACTIVE: prod
    volumes:
      - app-logs:/app/logs
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - app-network
    restart: always

  # ===== Nginx 反向代理 =====
  nginx:
    image: nginx:alpine
    container_name: nginx
    ports:
      - "80:80"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf
      - nginx-static:/usr/share/nginx/html
      - nginx-cache:/var/cache/nginx
    depends_on:
      - app1
      - app2
    networks:
      - app-network
    restart: always

# ===== 数据卷 =====
volumes:
  mysql-data:
  redis-data:
  app-logs:
  nginx-static:
  nginx-cache:

# ===== 自定义网络 =====
networks:
  app-network:
    driver: bridge

5.2 关键配置解析

① depends_on + healthcheck:控制启动顺序

yaml 复制代码
depends_on:
  mysql:
    condition: service_healthy    # 等 MySQL 健康检查通过后才启动

如果只用 depends_on: mysql(不带 condition),只会等 MySQL 容器启动,不会等 MySQL 真正就绪。MySQL 启动到可连接通常需要几秒,这段时间内应用启动会报"连接数据库失败"。

② 自定义网络:容器间通信

yaml 复制代码
networks:
  app-network:
    driver: bridge

同一自定义网络下的容器可以通过 容器名 互相访问:

  • app1 连接 MySQL:jdbc:mysql://mysql:3306/myapp
  • app1 连接 Redis:redis://redis:6379

不需要写死 IP 地址!

③ restart: always

容器异常退出后自动重启,保证服务高可用。

5.3 常用编排命令

bash 复制代码
# 启动所有服务(后台运行)
docker-compose up -d

# 查看服务状态
docker-compose ps

# 查看日志
docker-compose logs -f app1

# 重新构建并启动
docker-compose up -d --build

# 停止所有服务
docker-compose down

# 停止并删除数据卷(慎用!数据会丢)
docker-compose down -v

5.4 🎯 常见坑点

坑1:app1 和 app2 用了同一个镜像,但日志混在一起

解决:每个容器的日志挂载到不同目录,或在日志配置中加上容器标识。

坑2:Nginx 代理报 502 Bad Gateway

排查:

  1. docker-compose ps 检查 app1/app2 是否正常运行
  2. 检查 Nginx 配置的 upstream IP 是否正确(用容器 IP 还是容器名)
  3. 检查网络:Nginx 容器和 app 容器是否在同一网络

坑3:docker-compose up 后 MySQL 数据为空

init.sql 只在数据卷为空(首次启动)时执行。如果之前已经启动过,数据卷中有旧数据,init.sql 不会再次执行。需要 docker-compose down -v 清除数据卷后重新启动(注意:会丢数据!)。


六、整体架构图

复制代码
                    用户请求
                       │
                       ▼
               ┌──────────────┐
               │    Nginx     │  :80
               │  反向代理/负载均衡 │
               └──────┬───────┘
                      │
              ┌───────┴───────┐
              ▼               ▼
        ┌──────────┐    ┌──────────┐
        │  app1    │    │  app2    │  Spring Boot
        │  :8080   │    │  :8080   │
        └────┬─────┘    └────┬─────┘
             │               │
             └───────┬───────┘
                     │
              ┌──────┴──────┐
              ▼             ▼
        ┌──────────┐  ┌──────────┐
        │  MySQL   │  │  Redis   │
        │  :3306   │  │  :6379   │
        └──────────┘  └──────────┘

        所有容器运行在同一 Docker 自定义网络中
        通过容器名互相访问(无需写死 IP)

七、写在最后

学习建议

  1. Docker 不是"会 docker run"就够了,Dockerfile 优化(多阶段构建、镜像瘦身)、数据持久化、网络配置才是进阶重点
  2. Nginx 是运维必会组件,反向代理、负载均衡、动静分离、缓存策略这四项必须熟练配置
  3. docker-compose.yml 建议背下来,面试时可能让你现场写或改配置
  4. 实操建议:在自己的电脑上用 Docker Desktop 跑一遍这套编排,把 Nginx → app → MySQL 的链路跑通

🎯 面试高频三连问

Q1:Docker 的 Image 和 Container 有什么区别?

Image 是只读模板(类似类),Container 是 Image 的运行实例(类似对象)。一个 Image 可以启动多个 Container。

Q2:容器之间怎么通信?

三种方式:① 同一 Docker 网络下通过容器名访问(推荐);② 通过 IP 地址直接访问;③ 通过端口映射到宿主机,再通过宿主机 IP 访问。

Q3:docker-compose 的 depends_on 能保证服务就绪吗?

不能。depends_on 只保证容器启动顺序,不保证服务就绪。需要配合 healthcheck + condition: service_healthy 才能确保依赖服务真正可用。

相关推荐
阳光九叶草LXGZXJ15 分钟前
达梦数据库-报错-15-列【XXX】长度超出定义
linux·运维·数据库·sql·学习
黄大刀1 小时前
DataPulse一款开箱即用的SCADA(四)-历史记录
运维·scada·动环
归秋1424 小时前
2026 企业 AI 办公工具选型指南:从评估框架到产品适配
大数据·运维·人工智能
Android系统攻城狮4 小时前
Linux Gstreamer深度解析之gst_audio_encoder_set_frame_max调用流程与实战(六十一)
android·linux·运维·音视频·gstreamer音视频·音视频进阶·gstreamer音视频进阶
硅基手札5 小时前
【Linux内核专栏 14】网络协议栈
linux·运维·网络协议
工作10年+,存储芯片行业7 小时前
Linux NVMe 中断排查与性能优化:CPU 亲和性
linux·运维·服务器·windows·性能优化·ssd·pcie
快乐の番薯8 小时前
卷王问卷考试系统自动化功能测试
运维·功能测试·自动化
一号弯8 小时前
装完LINUX,请先新建日常用户
linux·运维·服务器
lpfasd1238 小时前
WinSW在Win7上失败真相-实测与修复
windows·nginx
许彰午9 小时前
03-Linux环境准备依赖包内核参数与用户组
linux·运维·服务器·数据库