新人装环境装了两天,我写了份 docker-compose.yml
组里 2 月来了个新同事。第一天拉代码,第二天还在拉代码——不是网速问题,是在装环境:MySQL 8.0、Redis 5.0、RocketMQ 4.6.1、Nacos 1.1.4、Elasticsearch 7.5,一个一个装,中间还把本地 MySQL 5.7 覆盖了,折腾掉两天。
这事我两年前也干过。当时我就想,这套中间件组合其实每个项目都一样,为什么不固化下来。于是花了一下午写了份 docker-compose.yml,现在新人入职只需要三步:git clone、docker-compose up -d、启动应用。
用的是 Docker 19.03 和 docker-compose 1.25.4,文件格式 version: '3.7'。
完整的文件
version: '3.7'
services:
mysql:
image: mysql:8.0.19
container_name: dev-mysql
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: shop
TZ: Asia/Shanghai
command:
--default-authentication-plugin=mysql_native_password
--character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci
--max_connections=500
--innodb_buffer_pool_size=512M
volumes:
- mysql-data:/var/lib/mysql
- ./initdb:/docker-entrypoint-initdb.d
networks:
- dev-net
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot123"]
interval: 5s
timeout: 3s
retries: 20
redis:
image: redis:5.0.7
container_name: dev-redis
ports:
- "6379:6379"
command: redis-server --requirepass redis123 --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redis-data:/data
networks:
- dev-net
rocketmq-namesrv:
image: rocketmqinc/rocketmq:4.6.1
container_name: dev-mq-namesrv
ports:
- "9876:9876"
environment:
JAVA_OPT_EXT: "-server -Xms256m -Xmx256m -Xmn128m"
command: sh mqnamesrv
networks:
- dev-net
rocketmq-broker:
image: rocketmqinc/rocketmq:4.6.1
container_name: dev-mq-broker
ports:
- "10909:10909"
- "10911:10911"
environment:
JAVA_OPT_EXT: "-server -Xms512m -Xmx512m -Xmn256m"
command: sh mqbroker -n rocketmq-namesrv:9876 -c /opt/rocketmq-4.6.1/conf/broker.conf
volumes:
- ./broker.conf:/opt/rocketmq-4.6.1/conf/broker.conf
- mq-data:/root/store
depends_on:
- rocketmq-namesrv
networks:
- dev-net
nacos:
image: nacos/nacos-server:1.1.4
container_name: dev-nacos
ports:
- "8848:8848"
environment:
MODE: standalone
SPRING_DATASOURCE_PLATFORM: mysql
MYSQL_SERVICE_HOST: mysql
MYSQL_SERVICE_PORT: 3306
MYSQL_SERVICE_DB_NAME: nacos_config
MYSQL_SERVICE_USER: root
MYSQL_SERVICE_PASSWORD: root123
depends_on:
mysql:
condition: service_healthy
networks:
- dev-net
networks:
dev-net:
driver: bridge
volumes:
mysql-data:
redis-data:
mq-data:
几个必须讲清楚的坑
一、depends_on 不等于"已经就绪"
这是最坑的一个。depends_on 只保证容器被创建并启动,不保证里面的服务能对外提供服务。MySQL 第一次启动要初始化数据目录、执行 /docker-entrypoint-initdb.d 下的 SQL,我的初始化脚本有 6 张表加 2 万条测试数据,要跑 40 多秒。
结果就是:Nacos 容器起来了,连 MySQL 连不上,直接退出。日志:
Caused by: java.sql.SQLNonTransientConnectionException: Could not create connection to database server.
Attempted reconnect 3 times. Giving up.
compose 文件格式 3.x 里 depends_on 支持 condition: service_healthy(需要 2.1 以上,3.x 的 long syntax 在某些版本被移除了又加回来,1.25.4 是支持的)。配合 healthcheck 用,上面的配置里 Nacos 就是这么写的。
如果你用的是不支持的旧版本,用 wait-for-it.sh 兜底:
nacos:
# ...
command: ["/bin/sh", "-c", "chmod +x /wait-for-it.sh && \
/wait-for-it.sh mysql:3306 -t 90 -- sh /home/nacos/start.sh -m standalone"]
volumes:
- ./wait-for-it.sh:/wait-for-it.sh
二、RocketMQ 的 broker 会注册错 IP
容器里 broker 启动时,会把自己的容器内网 IP(比如 172.20.0.5)注册给 nameserver。宿主机上的应用拿到这个 IP 之后连不上,因为那 IP 只在 Docker 网桥里可达。
报错是这个:
org.apache.rocketmq.remoting.exception.RemotingConnectException:
connect to <172.20.0.5:10911> failed
解决办法是显式指定 brokerIP1。单独的 broker.conf:
# broker.conf
brokerClusterName = DefaultCluster
brokerName = broker-a
brokerId = 0
deleteWhen = 04
fileReservedTime = 48
brokerRole = ASYNC_MASTER
flushDiskType = ASYNC_FLUSH
# 关键:填宿主机的 IP,不是容器 IP
brokerIP1 = 192.168.1.47
然后 volumes 挂载进去并在 command 里用 -c 指定。填宿主机内网 IP 之后,本机应用和局域网内其他同事都能连。
如果觉得写死 IP 太脏,也可以用 extra_hosts 加环境变量拼,但开发环境没必要搞那么复杂。
三、MySQL 8 的认证插件
MySQL 8.0 默认用 caching_sha2_password,而很多老客户端(包括我们项目里 Druid 1.1.20 之前的版本、还有 Navicat 11)不支持。连上去报:
Unable to load authentication plugin 'caching_sha2_password'.
加 --default-authentication-plugin=mysql_native_password 就退回旧认证方式。这个参数只在第一次初始化时生效,如果数据目录已经有数据了,得先 docker-compose down -v 删掉卷重建,或者手动执行:
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'root123';
FLUSH PRIVILEGES;
四、数据卷:named volume 还是挂载目录
我选了 named volume(mysql-data:、redis-data:),理由是跨平台。团队里有 Windows 同事,bind mount 在 Windows 上要处理路径和文件权限,麻烦。
代价是看数据没那么直接。要导数据时用临时容器:
$ docker run --rm -v dev_mysql-data:/data -v $(pwd):/backup \
busybox tar czf /backup/mysql-backup.tar.gz /data
注意 volume 名在 docker 里会被加上项目名前缀,dev 是目录名。用 docker volume ls 确认。
另外 docker-compose down 不会删除 named volume,数据还在。要连数据一起删必须 docker-compose down -v。这个行为有好有坏,我第一次想重建 MySQL 时 down 了三次都没清掉,后来才发现。
五、初始化 SQL 怎么塞进去
MySQL 官方镜像会自动执行 /docker-entrypoint-initdb.d 目录下的 .sh、.sql、.sql.gz,按文件名排序。我把建表脚本放进去:
initdb/
01-schema.sql # 建库建表
02-base-data.sql # 字典表、测试数据
只在数据目录为空时执行一次。已经初始化过之后改这些 SQL 不会重跑,得手动执行。
常用命令
# 后台启动全部
$ docker-compose up -d
# 只启动某个,比如先起 MySQL 和 Redis
$ docker-compose up -d mysql redis
# 看日志,追着看
$ docker-compose logs -f nacos
# 看状态
$ docker-compose ps
# 停掉但保留数据
$ docker-compose stop
# 销毁容器(数据卷保留)
$ docker-compose down
# 销毁容器并且删数据卷,彻底重来
$ docker-compose down -v
# 改了 yml 之后重建单个服务
$ docker-compose up -d --force-recreate redis
进容器里排查:
$ docker-compose exec mysql mysql -uroot -proot123
$ docker-compose exec redis redis-cli -a redis123
$ docker-compose exec nacos sh
资源占用
全起之后我看了一眼:
$ docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"
NAME CPU % MEM USAGE / LIMIT
dev-mysql 0.71% 412MiB / 512MiB
dev-redis 0.15% 8.2MiB / 256MiB
dev-mq-namesrv 0.33% 248MiB / 300MiB
dev-mq-broker 1.02% 596MiB / 700MiB
dev-nacos 0.88% 724MiB / 800MiB
加起来不到 2 GB。我给每个服务都加了 mem_limit(在 deploy 之外 compose 3.x 非 swarm 模式下要用 mem_limit 字段),免得 Nacos 吃太多把笔记本拖垮。8G 内存的机器跑完全没问题。
小结
depends_on只等容器启动,不等服务就绪。要么用condition: service_healthy加healthcheck,要么用wait-for-it.sh。- RocketMQ broker 在容器里会注册错 IP,必须配
brokerIP1指向宿主机。 - MySQL 8 要加
--default-authentication-plugin=mysql_native_password,否则老客户端连不上,且只在首次初始化时生效。 - named volume 跨平台友好,但
docker-compose down不删它,重装要-v。 - 给每个中间件加内存限制,不然开发机上跑一天就卡了。
写完之后新同事的环境花了 18 分钟搭好,其中 12 分钟在拉镜像。内网如果有镜像仓库的话,把这几个镜像推上去会更快。ES 我没放进这份文件,它单独占 1 GB 内存,需要的人再单独 up。