1. 项目概述与需求拆解
1.1 为什么要用 Docker Compose 装中间件
我最早接触中间件部署的时候,干的还是最原始的活儿:去官网下载安装包,解压、改配置、加环境变量、写 systemd 服务脚本,然后一台一台机器重复。如果只是装个 MySQL 还好说,等后面 Redis、RabbitMQ、Nginx、Kafka 这些堆到一起,每一套都有自己的依赖和配置方式,光是记安装步骤就能让人头大。换一台机器,整套流程又要重新来一遍。当时我就想,要是能把这些乱七八糟的安装过程统一起来、一键搞定就好了。
后来切到 Docker Compose,这个问题算是彻底解决了。Docker Compose 做的事情简单说就是:用一个 YAML 文件把多个容器的启动参数、网络、存储、依赖关系全部描述清楚,然后一行docker compose up -d全部拉起来。对于中间件这种“部署方式高度相似、版本参数成体系”的组件,它几乎是最合适的载体。你不需要再关心宿主机上是不是少了某个依赖库,不需要担心卸载的时候留下一堆残留文件,更不需要在团队里传一份几百字的“手工安装指南”。一份 compose 文件,所有人拉下来直接跑。
从实际效果看,我后来接手过的项目,凡是用 Compose 管理中间件的,环境搭建速度普遍从“半天起步”缩到了“十分钟以内”,出问题的概率也低得多。对于个人开发者、小团队、以及希望快速搭建开发/测试环境的场景来说,Compose 这套方案基本是当下最优解。
1.2 这套方案适合谁、能解决什么问题
先给一个大概的受众画像,你可以对照看看自己属于哪一类:
- 后端开发人员:尤其是做 Spring Boot、微服务架构的。本地开发经常需要 MySQL、Redis、RabbitMQ 这些基础组件,用 Compose 起一套,写代码的时候不再被环境问题打断思路。
- 运维 / 实施工程师:需要在多台服务器(包括内网环境)快速部署一套业务系统。Compose 文件写好以后,批量拷贝到各机器执行即可,只需要改环境变量。
- 测试工程师:需要在干净环境中验证功能,可能频繁重建数据库、清缓存。Compose 的重建成本极低,几秒钟就能恢复一个干净环境。
- 正在学习容器化、准备向 Kubernetes 迁移的人:Compose 是理解容器编排的最佳起点。它把“网络、存储、依赖”讲清楚了,后面看 K8s 的 Service、PVC、Deployment 会觉得顺很多。
其实不只是中间件,凡是那些“需要长期稳定运行、配置相对固定、依赖明确的组件”,都适合用 Compose 管理。我见过有人把定时任务、日志采集器、监控告警系统全部用 Compose 编排,效果都不错。它的核心价值在于:把环境状态固化为代码,让所有人共享同一套可复现的启动方式。
2. 整体设计与方案选型思路
2.1 Compose 文件的核心设计原则
Compose 文件那套约定其实不难,难的是怎么设计得合理、可维护。我刚开始写的时候完全是“能用就行”的风格:所有服务塞在默认网络里,数据目录乱挂,密码直接写死在文件里,端口能映射就映射。后来维护得多了,才慢慢总结出几条比较重要的原则。
第一,每个中间件服务单独定义,不搞“大杂烩”。有人喜欢把一个容器里同时装 MySQL 和 Redis,或者把 Nginx 和 PHP-FPM 塞在一起。这在交付阶段可能省事,但维护起来非常痛苦——只要其中一个组件出问题,就得重启整个容器。中间件这种基础组件,天然适合单容器单职责。Compose 里多写几个 service 没有任何成本,反而排查问题的时候能精确定位。
第二,数据与容器分离。容器是“一次性”的,MySQL 容器删了重建,如果数据跟着消失,那就真的悲剧了。所以数据目录、配置文件、日志目录都应该挂载到宿主机(或者用 named volume)。这一点在后面的章节我会具体演示。很多新手上来就会踩这个坑——容器一删,库没了,人傻了。
第三,敏感信息走环境变量或.env文件。密码、密钥这类东西不应该硬编码在 compose 文件里。Compose 原生支持.env文件替换变量,这个机制一定要用起来。你的 compose 文件可以提交到 Git 仓库,但.env文件必须加入.gitignore。
第四,版本固定,不追新。镜像标签尽量用具体的版本号,比如mysql:8.0、redis:7.0,而不是裸的latest。原因很简单:工具链的升级常常伴随配置格式变化,你今天写好的 compose 文件,三个月后碰到新版本镜像可能就跑不起来了。固定版本能保证可复现性。
2.2 网络模式与依赖关系怎么规划
Compose 默认会自动创建一个网络(默认是 bridge 驱动),所有在该 compose 文件里定义的服务都会加入这个网络。在这个网络里,服务之间通过“服务名”进行 DNS 解析,而不是 IP。比如你的 Spring Boot 服务连接 MySQL,JDBC 地址写jdbc:mysql://mysql:3306/dbname即可。介个特性非常关键——它意味着你不需要关心容器 IP 的变化,只要服务名不变,网络内部就能稳定互通。
有一个配置需要注意:depends_on这个选项。它解决的问题是“启动顺序”——比如某个业务服务依赖数据库先启动。但我用下来的体会是,depends_on只能保证“容器被创建和启动”的先后,不能保证“服务真正可用”。MySQL 容器起来了,但 MySQL 内部可能还在初始化,此时业务服务去连接就会失败。如果要严谨一点,需要配合healthcheck使用,或者让业务服务的启动逻辑带有重试机制。
再说一下端口映射的策略。凡是只需要内部通信的中间件,尽量不要映射到宿主机。比如你的业务容器和消息队列容器都在同一个 Compose 网络里,那 Kafka 的端口完全不需要暴露到宿主机。只有需要从宿主机外部访问的服务,才需要做ports映射。很多人的安全漏洞,其实就是把一堆不必要暴露的服务全部映射到了0.0.0.0:端口。如果你的中间件只是内部使用,建议不要映射端口,或者映射到127.0.0.1:端口,这样只有本机能访问。
2.3 选型对比:Compose 与其他方式的区别
我知道有人会问:既然有 Docker Compose,那 Dockerfile、docker run、Kubernetes 这些是什么关系?
简单说:
- Dockerfile负责构建“镜像”本身,解决的是“环境如何制作”的问题。比如你要做一个包含特定 SDK 的 Java 运行镜像。
- docker run是一次性启动一个容器的命令。适合临时调试,不适合管理多个服务。
- Docker Compose站在
docker run之上,解决的是“多个容器如何协同启动”的问题,可以用一个声明式的文件描述整个服务栈。它的优势是简单、直接、学习成本低,特别适合开发环境和中小型部署场景。 - Kubernetes则解决的是更大规模下的“编排调度、自动扩缩容、自愈”等问题。功能强很多,但复杂度也高不少。
所以在中间件部署这个场景,Compose 是性价比最高的方案。你不需要为“跑一个 MySQL”就去搭一套 K8s——那是拿大炮打蚊子。合理的路径是:先用 Compose 理清服务之间的关系,等业务增长到需要水平扩展、滚动更新的时候,再迁移到 K8s。后面我会单独谈一下迁移时要注意的点。
3. 核心配置解析与文件编写要点
3.1 一个最基础但完整的示例
我先给一个可运行的例子。假设我们要在本地搭建一套常用的开发环境:MySQL 8.0、Redis 7.0、RabbitMQ 3.13。这是很多后端系统(Spring Boot 项目)的基础组合。
# docker-compose.yml version: "3.8" services: mysql: image: mysql:8.0 container_name: dev-mysql restart: always environment: MYSQL_ROOT_PASSWORD: root123456 MYSQL_DATABASE: demo_db MYSQL_USER: demo MYSQL_PASSWORD: demo123456 TZ: Asia/Shanghai ports: - "3306:3306" volumes: - ./data/mysql:/var/lib/mysql - ./conf/mysql/my.cnf:/etc/mysql/conf.d/my.cnf command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-proot123456"] interval: 10s timeout: 5s retries: 5 redis: image: redis:7.0 container_name: dev-redis restart: always ports: - "6379:6379" volumes: - ./data/redis:/data - ./conf/redis/redis.conf:/etc/redis/redis.conf command: ["redis-server", "/etc/redis/redis.conf"] healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5 rabbitmq: image: rabbitmq:3.13-management container_name: dev-rabbitmq restart: always environment: RABBITMQ_DEFAULT_USER: admin RABBITMQ_DEFAULT_PASS: admin123 ports: - "5672:5672" - "15672:15672" volumes: - ./data/rabbitmq:/var/lib/rabbitmq healthcheck: test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"] interval: 15s timeout: 10s retries: 5对应的目录结构:
compose-env/ ├── docker-compose.yml ├── .env ├── conf/ │ ├── mysql/ │ │ └── my.cnf │ └── redis/ │ └── redis.conf └── data/ ├── mysql/ ├── redis/ └── rabbitmq/启动命令只有一行:
docker compose up -d查看状态:
docker compose ps看到healthy字样说明服务已通过健康检查,可以正常使用。
3.2 配置项逐个拆解:环境变量、端口、数据卷
很多人第一次看到这样的 compose 文件,会觉得“这不就是一堆配置堆起来吗”。但实际上每一段背后的意义都值得展开。
环境变量的优先级意识。注意看 MySQL 那个 service。我设置了MYSQL_ROOT_PASSWORD、MYSQL_DATABASE、MYSQL_USER、MYSQL_PASSWORD。这些是官方镜像约定好的环境变量——镜像里的启动脚本(entrypoint)会读取它们,在首次初始化数据目录时创建对应的用户、库。有一个容易被忽视的点:这些环境变量只在数据目录为空时生效。如果你之前已经跑过一次,数据目录里已经有内容了,那再改MYSQL_DATABASE是没有用的。改环境变量前要想清楚,要么删数据卷重新初始化,要么手动进去改库。
端口映射的取舍。我把 3306、6379、5672、15672 全部映射到了宿主机。这在本地开发环境没问题,因为你需要用 Navicat、Redis Desktop Manager 这类 GUI 工具连上去。但在生产环境,你大概率不希望暴露 Redis 的 6379——Redis 本身没有足够强的访问控制,裸奔在公网上极容易被攻击。生产环境建议物理网络隔离,或者至少不要把这几个端口暴露到公网。
数据卷使用 bind mount 还是 named volume。两种方式各有适用场景:
- bind mount(格式
./data/mysql:/var/lib/mysql):把宿主机的目录直接挂进去。优点是数据文件就在当前目录下,你可以直接进去查看、备份、dump。缺点是受宿主机目录权限影响比较大。比如某些 Linux 发行版的 SELinux 策略会阻止容器写目录,需要做额外处理。 - named volume(格式
mysql-data:/var/lib/mysql):由 Docker 管理存储,隔离性更好,性能也比较稳定。缺点是你不太容易直接看到数据文件在哪个位置,备份的时候要借助docker run --rm -v mysql-data:/data -v $(pwd):/backup alpine tar czf /backup/mysql.tar.gz /data之类的命令。
我个人的习惯:开发环境用 bind mount,因为直观;有状态的正式服务用 named volume,因为省心安全。交给你自由选择。
3.3 添加 Sentinel、MinIO、Nacos 等中间件的技巧
现实环境里,我们经常还要加 Sentinel(流量防护)、MinIO(对象存储)、Nacos(配置中心和注册中心)这类的中间件。它们的 compose 配置也不复杂,但有几个小细节值得注意。
以 Sentinel 为例,它的控制台是一个 Spring Boot 应用,默认端口是 8858。从某个版本之后,官方提供的 Docker 镜像中,如果要在控制台配置持久化,需要挂载一个日志目录。一个最小配置:
sentinel: image: bladex/sentinel-dashboard:1.8.8 container_name: dev-sentinel restart: always ports: - "8858:8858" environment: JAVA_OPTS: "-Dserver.port=8858 -Dcsp.sentinel.dashboard.server=localhost:8858 -Dproject.name=sentinel-dashboard" volumes: - ./data/sentinel:/root/logs这里JAVA_OPTS是镜像约定的环境变量,用来覆盖 JVM 参数。如果你要配置-Dcsp.sentinel.dashboard.server指向某个远程地址,就改这里。
Nacos 稍微特殊一点,它有单体模式和集群模式。在本地开发用单体即可。注意 Nacos 2.x 默认需要 MySQL 存储配置数据,需要先配置好数据库:
nacos: image: nacos/nacos-server:v2.3.2 container_name: dev-nacos restart: always environment: MODE: standalone PREFER_HOST_MODE: hostname MYSQL_SERVICE_HOST: mysql MYSQL_SERVICE_DB_NAME: nacos_config MYSQL_SERVICE_USER: demo MYSQL_SERVICE_PASSWORD: demo123456 NACOS_AUTH_ENABLE: "false" ports: - "8848:8848" - "9848:9848" depends_on: mysql: condition: service_healthy注意MYSQL_SERVICE_HOST这里直接填了mysql,而不是 IP。这正是利用了 Compose 自带的 DNS 解析——容器之间通过服务名互通。这招在多个中间件互相依赖的时候特别有用。
MinIO 的配置就简单很多:
minio: image: minio/minio:latest container_name: dev-minio restart: always environment: MINIO_ROOT_USER: minioadmin MINIO_ROOT_PASSWORD: minioadmin ports: - "9000:9000" - "9001:9001" volumes: - ./data/minio:/data command: server /data --console-address ":9001"9000 是 API 端口,9001 是 Web 控制台端口。MINIO_ROOT_USER和MINIO_ROOT_PASSWORD是初始化管理员账号的约定环境变量。
3.4 配置文件外置:把 my.cnf / redis.conf 挂进去
上一节我们提到了conf目录的挂载。这里补充说明一下为什么这样做,以及怎么写这些配置。
MySQL 默认的字符集不是 utf8mb4(旧版本是 latin1),如果直接建表,中文和 emoji 可能会出现乱码问题。所以我通常在宿主机conf/mysql/my.cnf里写:
[mysqld] character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci default-time-zone = '+08:00' max_connections = 500 [client] default-character-set=utf8mb4 [mysql] default-character-set=utf8mb4然后挂载到容器的/etc/mysql/conf.d/my.cnf。这个路径是 MySQL 官方镜像约定好的“补充配置目录扩展点”,镜像启动时会自动读取这个目录下的所有.cnf文件。你不用去改镜像内的主配置,省心很多。
Redis 同理。我要开启 AOF 持久化、设置密码、限制内存使用策略,就会在宿主机写conf/redis/redis.conf:
appendonly yes appendfsync everysec requirepass redis123456 maxmemory 256mb maxmemory-policy allkeys-lru然后挂载到容器的/etc/redis/redis.conf,启动时让 redis-server 显式加载它:
command: ["redis-server", "/etc/redis/redis.conf"]注意这里不能写成command: redis-server /etc/redis/redis.conf这样的 string 格式。Compose 的 command 字段如果用字符串形式,它不会经过 shell 分词,所以容易被解析成一个整体参数。用列表形式(YAML array)最稳。
这里推荐一个经验:配置文件永远放在宿主机里,不要在容器内用 vi/vim 临时改。因为容器一旦重建,任何在容器内做的修改都会丢失。放进宿主机并挂载,你的“环境配置”也成为了可以被 Git 追踪的资产。
4. 实操全流程:从零到一搭建完整环境
4.1 前置检查:版本和依赖
正式开始之前,先确认一下宿主机环境。Compose 本身依赖 Docker Engine。这里给出我建议的最低版本:
| 组件 | 最低版本 | 推荐版本 |
|---|---|---|
| Docker Engine | 20.10 | 24.0+ |
| Docker Compose | 2.0+ | 2.24+ |
检查命令:
docker --version docker compose version注意:旧版的docker-compose(带横线的)是 Python 写的,功能上已经停更。新版的docker compose(带空格的)是 Docker 官方用 Go 写的插件,推荐使用。如果你的服务器还是老版本,建议先升级:
# 以 Ubuntu / Debian 为例 sudo apt-get update sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin如果是离线环境,下面我会单独讲。
4.2 完整步骤:目录初始化、编写文件、启动与验证
第一步,创建项目目录并进入。
mkdir -p ~/compose-env/{conf/{mysql,redis},data/{mysql,redis,rabbitmq}} cd ~/compose-env第二步,创建.env文件,把敏感变量集中管理:
cat > .env <<'EOF' MYSQL_ROOT_PASSWORD=root123456 MYSQL_DATABASE=demo_db MYSQL_USER=demo MYSQL_PASSWORD=demo123456 REDIS_PASSWORD=redis123456 TZ=Asia/Shanghai EOF然后在 compose 文件里引用这些变量:
environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: ${MYSQL_DATABASE} MYSQL_USER: ${MYSQL_USER} MYSQL_PASSWORD: ${MYSQL_PASSWORD} TZ: ${TZ}第三步,编写docker-compose.yml(内容参考 3.1 节,把环境变量替换为${...}形式)。
第四步,启动并观察日志。
docker compose up -d docker compose ps docker compose logs -f mysql # 观察 MySQL 初始化日志MySQL 首次初始化可能会需要十几秒,期间日志里会有Initializing database的输出。等到ready for connections出现,就说明数据库已经可用。这里最容易踩的坑是:刚执行完docker compose up -d就去连数据库,此时 MySQL 还没准备好,连接直接报错。所以加了 healthcheck,配合docker compose ps看状态是否为healthy,会更稳。
第五步,验证连通性。比如从宿主机连 MySQL:
mysql -h127.0.0.1 -P3306 -udemo -pdemo123456 demo_db如果不想安装 MySQL 客户端,可以直接进入容器内部操作:
docker exec -it dev-mysql mysql -uroot -proot123456Redis 验证:
docker exec -it dev-redis redis-cli如果已经在 redis.conf 里配置了requirepass,需要先执行AUTH redis123456。
4.3 镜像拉取太慢、离线安装怎么办
这是国内环境绕不开的一个问题,也是我看到热词里专门有“docker compose离线安装”的原因。
先说正常情况。如果服务器能访问公网,但拉取 Docker Hub 镜像速度很慢,我的经验是用可靠的镜像加速地址。Docker Daemon 配置文件/etc/docker/daemon.json里可以配置多个 registry-mirrors:
{ "registry-mirrors": [ "https://docker.1ms.run" ] }配置后重启 Docker:
sudo systemctl daemon-reload sudo systemctl restart docker注意:镜像加速只对 Docker Hub 官方仓库有效。如果你用的是其他镜像仓库,请参考对应文档。
更麻烦的是“纯离线环境”。这种情况通常出现在政企内网、机房隔离网络等场景。我的做法分两步:
第一步,在能联网的机器上拉取镜像并打包成 tar。
# 假设要安装的是 mysql:8.0, redis:7.0, rabbitmq:3.13-management docker pull mysql:8.0 docker pull redis:7.0 docker pull rabbitmq:3.13-management # 分别保存 docker save -o mysql-8.0.tar mysql:8.0 docker save -o redis-7.0.tar redis:7.0 docker save -o rabbitmq-3.13-management.tar rabbitmq:3.13-management第二步,把 tar 文件和 compose 项目目录一起拷贝到离线服务器上,然后加载镜像。
docker load -i mysql-8.0.tar docker load -i redis-7.0.tar docker load -i rabbitmq-3.13-management.tar镜像加载成功之后,docker compose up -d就不会再去拉取镜像,直接使用本地已有的镜像创建容器。需要提醒的是,中间件的镜像体积通常不小(mysql:8.0 约 600MB,minio 约 300MB),拷贝到离线环境时要规划好磁盘空间。
4.4 数据备份与恢复的两种常用姿势
用 Compose 管理中间件,备份数据这件事也不能忽略。我推荐两种方式。
方式一:直接备份挂载目录(适合 bind mount)。
如果你用的是./data/mysql这种目录挂载,直接打包目录就行:
tar czf mysql-backup-$(date +%F).tar.gz ./data/mysql恢复时解压到原路径即可。但这种方式要求备份期间数据一致性好——最好是先停掉 MySQL 写入,或者执行FLUSH TABLES WITH READ LOCK后再打包。
方式二:使用容器内工具导出(适合所有场景)。
MySQL:
docker exec dev-mysql sh -c 'exec mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" --all-databases' > all-databases.sqlRedis:
docker exec dev-redis redis-cli -a redis123456 BGSAVE # 稍等片刻,RDB 文件生成到 /data/dump.rdb,对应宿主机 ./data/redis/dump.rdbRabbitMQ:它的配置和消息数据都存储在/var/lib/rabbitmq,直接挂载目录备份即可。
我用得最多的还是 mysqldump + 定时任务(crontab)的方式。把 dump 脚本丢到 crontab 里每天凌晨执行,保留最近 7 天备份。这套组合对于中小型项目完全够用。
5. 常用中间件清单与调优速查
5.1 一份可直接抄作业的常用中间件 Compose 参考
这里整理一份“开箱即用”的清单,涵盖后端开发中最高频的几个中间件。你不需要全部启动,按需裁剪即可。
services: # ---------- MySQL ---------- mysql: image: mysql:8.0 container_name: mid-mysql restart: always environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root123456} MYSQL_DATABASE: ${MYSQL_DATABASE:-app_db} TZ: Asia/Shanghai command: - --character-set-server=utf8mb4 - --collation-server=utf8mb4_unicode_ci - --max_connections=500 ports: - "3306:3306" volumes: - ./data/mysql:/var/lib/mysql - ./conf/mysql:/etc/mysql/conf.d healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p${MYSQL_ROOT_PASSWORD:-root123456}"] interval: 10s timeout: 5s retries: 5 # ---------- Redis ---------- redis: image: redis:7.0 container_name: mid-redis restart: always ports: - "6379:6379" volumes: - ./data/redis:/data - ./conf/redis/redis.conf:/etc/redis/redis.conf:ro command: ["redis-server", "/etc/redis/redis.conf"] # ---------- RabbitMQ ---------- rabbitmq: image: rabbitmq:3.13-management container_name: mid-rabbitmq restart: always environment: RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:-admin} RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASS:-admin123} ports: - "5672:5672" - "15672:15672" volumes: - ./data/rabbitmq:/var/lib/rabbitmq # ---------- Nginx ---------- nginx: image: nginx:1.25 container_name: mid-nginx restart: always ports: - "80:80" - "443:443" volumes: - ./conf/nginx/conf.d:/etc/nginx/conf.d:ro - ./www:/usr/share/nginx/html:ro - ./logs/nginx:/var/log/nginx # ---------- MinIO ---------- minio: image: minio/minio:latest container_name: mid-minio restart: always environment: MINIO_ROOT_USER: ${MINIO_USER:-minioadmin} MINIO_ROOT_PASSWORD: ${MINIO_PASSWORD:-minioadmin} ports: - "9000:9000" - "9001:9001" volumes: - ./data/minio:/data command: server /data --console-address ":9001" # ---------- Nacos ---------- nacos: image: nacos/nacos-server:v2.3.2 container_name: mid-nacos restart: always environment: MODE: standalone PREFER_HOST_MODE: hostname MYSQL_SERVICE_HOST: mysql MYSQL_SERVICE_DB_NAME: nacos_config MYSQL_SERVICE_USER: root MYSQL_SERVICE_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root123456} ports: - "8848:8848" - "9848:9848" depends_on: mysql: condition: service_healthy # ---------- Sentinel ---------- sentinel: image: bladex/sentinel-dashboard:1.8.8 container_name: mid-sentinel restart: always environment: JAVA_OPTS: "-Dserver.port=8858 -Dcsp.sentinel.dashboard.server=localhost:8858 -Dproject.name=sentinel-dashboard" ports: - "8858:8858" # ---------- Elasticsearch ---------- elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:8.11.3 container_name: mid-elasticsearch restart: always environment: - discovery.type=single-node - xpack.security.enabled=false - ES_JAVA_OPTS=-Xms512m -Xmx512m ports: - "9200:9200" volumes: - ./data/elasticsearch:/usr/share/elasticsearch/data这个清单可以直接复制到你的docker-compose.yml里,按需注释/取消注释。
5.2 各中间件的关键参数与调优建议
上节给了文件,这节讲一下常见的优化方向,避免“照抄清单但不知道如何调整”。
MySQL:max_connections决定最大连接数,默认 151,对小型应用够用;如果你有连接池且核心线程较多,建议调大。innodb_buffer_pool_size 默认 128M,这个参数开大能明显提升查询性能。在 compose 里可以通过command追加--innodb-buffer-pool-size=1G或者写在my.cnf里。
Redis:maxmemory必须设置,因为默认情况下 Redis 会一直吃内存直到操作系统 OOM。maxmemory-policy建议设置为allkeys-lru,这样在内存满的时候 Redis 自动淘汰最久未使用的 key,而不至于写入失败。
RabbitMQ:关键调优项是连接心跳(heartbeat)和 TCP 连接 backlog。如果客户端大量掉线,通常是 heartbeat 时间设置太短。在 compose 中可以通过command设置:
command: ["rabbitmq-server"]或者通过 extra 配置文件调整。多数情况下默认配置已经能满足需求,不需要过度调优。
Elasticsearch:容器方式跑 ES 要特别注意两个系统配置:vm.max_map_count至少需要 262144。如果启动报错,执行:
sudo sysctl -w vm.max_map_count=262144若要永久生效,写入/etc/sysctl.conf。另外,ES 非常吃内存,ES_JAVA_OPTS建议 Xms 和 Xmx 设置成一样,避免 JVM 动态调整引发性能抖动。
5.3 为什么推荐同时启用 healthcheck
在前面的示例里,我多次写了healthcheck。这可能看起来像是“多此一举”,但实际使用中它救过我很多次。
场景一:depends_on只关心容器启动,不关心服务可用。有healthcheck之后,你可以在depends_on里写明:
depends_on: mysql: condition: service_healthy这样业务容器就只会在 MySQL 健康通过后才启动。Compose 2.x 之后完全支持这种写法。
场景二:docker compose ps输出的状态非常直观。未配置 healthcheck 时,容器的状态通常只有running或exited。配置之后,你会看到(healthy)或(unhealthy)。这是一个非常方便的“健康仪表盘”,排查问题时一眼就能定位到哪个组件不正常。
写 healthcheck 的时候有一个经验:探针命令要尽可能轻量。不要用那种会执行大量查询的命令,否则频繁探测反而会拖累服务。MySQL 官方推荐mysqladmin ping,Redis 用redis-cli ping,RabbitMQ 用rabbitmq-diagnostics -q ping,这些都是官方认可的轻量探针。
6. 常见问题的排查与修复实录
6.1cannot stop docker compose application类问题的处理
热词里专门有一个 “cannot stop docker compose application. reason: compose [stop] exit status 1”,这正是我自己实操里遇到过多次的坑。
现象是:执行docker compose stop的时候报错,exit status 1,部分容器停止失败。常见原因有以下几个:
原因一:容器内的进程不响应 SIGTERM 信号。Docker 停容器时默认先发 SIGTERM,等一段时间(默认 10 秒)之后如果进程还没退出,就会发 SIGKILL。有些中间件(尤其是老版本 Nacos、Elasticsearch)在收到 SIGTERM 后需要很长时间才能优雅退出。如果等了 10 秒还没退出,Docker 会强制杀掉容器,此时docker compose stop就会报错。
解决方法很简单,给docker-compose.yml中对应服务配置更长的停止超时时间:
services: nacos: image: nacos/nacos-server:v2.3.2 stop_grace_period: 60s原因二:容器内的 PID 1 进程不是真正的业务进程。比如有些镜像使用了一个简单脚本作为 entrypoint,脚本启动子进程之后自己退出了,或没有正确把信号转发给子进程。这种时候容器收到 SIGTERM 也无法优雅退出。排查方法:
docker inspect dev-nacos --format '{{.State.Pid}}'进入容器看 PID 1 是什么进程。如果发现是sh或bash而不是真正的 Java 进程,优先考虑换新版镜像。
原因三:容器处于异常状态,无法正常停止。可以用最粗暴但有效的方式:
docker compose kill docker compose downkill直接发 SIGKILL,down会删除容器和默认网络(不删数据卷)。等清理完成后,重新up -d即可。
6.2 中间件容器间网络连不通的排查方法
另一个很常见的问题是:两个中间件容器明明在同一个 compose 网络里,但程序连不上。特别是连接 Nacos、连接数据库时经常出现Connection refused。
排查思路按照这三步走:
第一步,确认容器是否正常:
docker compose ps第二步,确认容器是否真的在同一个网络:
docker inspect dev-mysql --format '{{json .NetworkSettings.Networks}}' docker inspect dev-nacos --format '{{json .NetworkSettings.Networks}}'看看两边是不是都有同一个网络名(通常是项目名_default)。如果其中一个是单独用docker run启动的,它就不会在这个网络里,也就没法用服务名互相访问。
第三步,从业务容器里手动测试连通性:
docker exec -it dev-nacos ping mysql docker exec -it dev-nacos telnet mysql 3306如果 ping 不通或端口连不上,多数情况下是网络模式配置有问题,或者目标服务的端口绑定被本地防火墙挡住了。
还有一个极容易忽略的坑:服务名中的下划线。Compose 服务名支持字母、数字、下划线,但在某些场景下,DNS 解析下划线可能出问题。为了避免不必要的麻烦,服务名尽量用-代替_(比如mysql、rabbitmq,不要写mysql_db这种)。
6.3 docker compose 覆盖策略:override 文件的使用技巧
关于 Compose 有一个高阶用法值得单独提一下:docker-compose.override.yml。它与docker-compose.yml放在同一目录时,执行任何 compose 命令都会自动合并docker-compose.yml和docker-compose.override.yml的配置。
这个特性在“同一个项目,不同环境不同配置”的场景下非常有用。我的习惯是:
docker-compose.yml只放“所有环境都相同”的基础配置。docker-compose.override.yml放本机特有的配置(比如开发环境要映射端口、要挂载源码目录)。- 生产环境用
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d显式指定两个文件。
举个具体例子:
# docker-compose.yml services: app: image: myapp:latest environment: SPRING_PROFILES_ACTIVE: dev# docker-compose.override.yml(本地开发) services: app: ports: - "8080:8080" volumes: - ./src:/app/src这样在本地跑docker compose up -d,app服务会自动带上端口映射和源码挂载;而在生产环境,指定-f docker-compose.yml -f docker-compose.prod.yml时不加载 override 文件,也就不会意外暴露端口。这个模式对团队协作非常友好。
6.4 容器日志膨胀与服务资源限制
容器跑久了,日志文件会越来越大。默认情况下 Docker 的 json-file 日志驱动会无限增长,如果不处理,/var/lib/docker 目录可能被撑爆。
在docker-compose.yml里加一个全局配置:
services: mysql: image: mysql:8.0 logging: driver: "json-file" options: max-size: "50m" max-file: "3"这段配置表示每个容器的日志文件最大 50MB,最多保留 3 个文件。超过范围后自动滚动覆盖,避免磁盘被日志占满。这个配置强烈建议每个服务都加上。
资源限制也是一样。默认容器不限制 CPU 和内存,如果某个中间件出现内存泄漏或突发高负载,可能拖垮整个宿主机。给关键服务加上:
services: mysql: image: mysql:8.0 deploy: resources: limits: cpus: "2.0" memory: 2g reservations: cpus: "0.5" memory: 512m注意,独立的docker compose(非 swarm 模式)也能识别部分deploy.resources配置。如果版本太低不支持,也可以用mem_limit: 2g和cpus: 2.0这种旧式写法。
7. 从 Compose 平滑迁移到 Kubernetes
7.1 设计层的差异点
热词里提到了 “docker compose升级到k8s”,这是很多团队走到一定规模后都会碰到的问题。先说结论:Compose 和 K8s 在“编排理念”上有一定关联,但并不是简单地把 YAML 翻译成 Deployment 就能直接跑。需要关注这几个差异点:
Compose 的“服务”对应 K8s 的多个资源。在 Compose 里,一个 service 包含了镜像、端口、卷、环境变量、健康检查、资源限制等全部信息。到了 K8s,这些配置要拆开部署到多个资源上:Deployment(管理副本数)、Service(提供稳定的访问入口)、ConfigMap(保存配置)、Secret(保存密码)、PVC(持久化存储)。粒度更细,但同时也意味着你要管理更多资源。
网络模式完全不同。Compose 默认让所有服务在同一个扁平网络里,天然互通;K8s 则是每个 Pod 有独立 IP,Pod 之间通过 Service 或 DNS 访问。如果你在 Compose 里依赖了固定端口映射、或者用 localhost 访问兄弟服务,迁移时必须改代码或重新设计。
存储方式也要换。Compose 里最常用的是宿主机路径挂载(bind mount),到了 K8s 里则应该使用 PV + PVC,或者直接用 StorageClass 动态供应。本地路径挂载在 K8s 里只在单节点集群(比如 k3s)中可用,多节点集群下 Pod 漂移后数据就找不到了。
7.2 迁移的常见路径和配套工具
一个比较务实的迁移步骤如下:
- 先把 Compose 中的每个 service 理解独立化:确定哪些服务有状态(需要持久化),哪些服务无状态(可以随意重建)。
- 有状态服务优先考虑用 StatefulSet + PVC:比如 MySQL、Redis、RabbitMQ 这类中间件,有固定的网络标识需求(主从节点名)。无状态服务直接用 Deployment。
- 配置统一抽到 ConfigMap/Secret:把环境变量、配置文件迁移过去,避免在 Deployment 里硬编码。
- 服务暴露方式的调整:Compose 里靠
ports映射宿主机端口,K8s 里通常是用 Service 的 ClusterIP 供集群内访问,对外用 Ingress 或 LoadBalancer。端口暴露方式要重新规划。
如果项目规模相对小,不想完全手工迁移,可以考虑一些辅助工具。比如 Compose 官方有一个实验性的docker compose convert命令,可以尝试输出 K8s 风格的 YAML 作为起点。但坦白说,这类工具生成的资源往往需要手工修正,尤其是存储和网络部分。
我的建议是:不要指望自动化转换一步到位,把 Compose 文件当作需求说明书,自己动手写 K8s 资源定义。你在 Compose 里写的每个环境变量、每个卷映射、每个 healthcheck,都在指导你如何设计 K8s 的 ConfigMap、PVC 和探针。从这个角度看,先学好 Compose 完全可以认为是学习 K8s 的前置课。
8. 写在最后的经验
回到开头那句话,我把这套 Compose 文件发给团队后,最大的变化不是“安装变快了”,而是“环境不可复现”这件事彻底消失了。新同事入职第一天,拉下代码、装个 Docker、执行一条命令,本地环境和线上几乎一致。以前那种“在我机器上是好的呀”的对话,基本绝迹。
这中间我踩过的坑确实不少——数据卷忘挂载导致容器重建丢数据、healthcheck 没配导致服务启动顺序错乱、镜像用 latest 导致升级后配置失效、日志不限制导致磁盘爆掉……每一个都是血泪教训。这些内容都写在前面了,希望你能一次避开。
最后再分享一个小技巧:永远保留一份“最小可启动版”的 compose 文件。当你往里面加新中间件的时候,如果启动失败、配置有问题,就先把新服务从文件中注释掉,确保原有核心服务不受影响。这种“最小化变更 + 渐进式集成”的思路,能让你在面对一堆中间件的时候保持清晰,而不是一锅粥越搅越乱。
Docker Compose 不是终点,但它是理解现代应用部署方式的最佳起点。把这份基础打牢,后面学 K8s、学 Service Mesh 都会轻松很多。