作者:没有四次元口袋的蓝胖
日期:2026-09-16
标签:Docker, Docker Compose, Nginx, 容器化
写在前面
这是一篇 Docker Compose 多容器编排 + Nginx 集群的实战记录。从 Dockerfile 编写、数据持久化,到 Nginx 反向代理负载均衡,再到 docker-compose.yml 一键编排,完整记录了微服务容器化部署的全过程。
一、项目背景
痛点:团队开发、测试、生产三套环境,Java 应用在不同机器上运行结果不一致(JDK 版本不同、依赖缺失、配置混乱),排查问题效率极低。
目标:
- 统一环境 ------ "一次构建,到处运行"
- 简化部署 ------ 多服务(Web + MySQL + Redis + Nginx)一键编排
- 高可用 ------ 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 运行,遵循最小权限原则 |
💡 启动时指定环境:
bashdocker 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
排查:
docker-compose ps检查 app1/app2 是否正常运行- 检查 Nginx 配置的 upstream IP 是否正确(用容器 IP 还是容器名)
- 检查网络: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)
七、写在最后
学习建议
- Docker 不是"会 docker run"就够了,Dockerfile 优化(多阶段构建、镜像瘦身)、数据持久化、网络配置才是进阶重点
- Nginx 是运维必会组件,反向代理、负载均衡、动静分离、缓存策略这四项必须熟练配置
- docker-compose.yml 建议背下来,面试时可能让你现场写或改配置
- 实操建议:在自己的电脑上用 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才能确保依赖服务真正可用。