大家有没有过这种经历:把MySQL丢到一个容器里跑,又用docker run把Redis拉起来,再敲一个Nginx挂上去,十几条命令下来,端口冲突了,网络又不通,昨天还能访问,今天重启后一堆容器起不来。这种在Docker多容器环境下“野路子”部署,我刚开始折腾的时候真没少踩坑。后来把Docker Compose好好搞明白,才发现以前很多弯路根本没必要走。Docker Compose只需要一份docker-compose.yml,就能把MySQL、Redis、Nginx、Nacos这些服务一次性编排起来,一条docker compose up -d全部搞定。这篇文章适合所有想从“单容器跑通”进阶到“多容器协作”的人,也适合已经在手动敲命令、但被依赖顺序和网络折腾到怀疑人生的朋友。我会把Compose的设计思路、实际部署案例、高频报错排查一次讲透。
1. 先搞清楚Compose到底解决什么问题
1.1 多容器环境下手工部署的四大痛点
很多新手一开始都会觉得,docker run挺好用的,一个Redis启动命令背熟了就够。但真到了多容器场景,手工部署的痛点会一个接一个冒出来。
第一是配置零散。每个容器要传一堆环境变量,比如MySQL要有密码、字符集,Redis要有持久化参数,Nginx要挂载配置文件。这些参数散落在Shell命令里,换台机器就得重新敲一遍,记不住也容易错。第二是网络不通。多个容器之间互相访问,docker run默认用的是默认bridge网络,容器IP不固定,除非你一个个--link或者手动创建网络再指定--network,否则你在这个容器里访问另一个容器的IP,下次重启就失效了。第三是启动顺序。比如你的Java应用依赖MySQL,容器启动并不会自动等数据库就绪,应用先起来了,连接失败,整个服务就崩了。第四是难以整体管理。停服务要一个个docker stop,看日志要一个个docker logs,重启也要找容器ID,命令行一多就乱。
这些痛点单靠docker run都能解决,但每解决一个都要额外敲一大堆参数,又容易引入新问题。Compose存在的意义,就是把这些手工参数变成一份可提交、可版本管理、可复现的声明文件。
1.2 Compose机制与原理解读
Compose的核心机制说穿了并不神秘:它通过一个docker-compose.yml文件描述“我要运行哪些容器、每个容器用什么镜像、端口怎么映射、挂载什么卷、加入哪个网络、依赖哪些服务”。你执行docker compose up -d的时候,Compose会去读这份文件,自动帮你创建网络、创建卷、按依赖顺序启动容器,然后把所有容器都纳管到一个项目里。
一个非常关键的设计是:Compose会自动创建一个独立的bridge网络,网络名默认是“项目目录名_默认网络名”。在这个网络中,每个服务都可以直接用服务名作为主机名互相访问。举个例子,你的Compose里有个服务叫mysql,另一个服务叫app,那么app容器里访问数据库,只需要写jdbc:mysql://mysql:3306/xxx,完全不用关心MySQL容器的IP是多少。这个机制解决了手工部署里最烦人的网络问题。
还有一个重要设计是项目的概念。Compose会把同目录下的所有容器视为同一个项目,通过docker compose ps统一查看状态,通过docker compose logs -f统一查看日志,通过docker compose down一键停止并清理网络。这种“一个项目一把梭”的体验,和docker run一条条管理的差别,用过一次就回不去了。
2. 从零搭建一个Compose项目:以MySQL 8.0 + Redis + Nginx为例
2.1 项目目录与docker-compose.yml框架设计
用一个最典型的前后端加数据库组合来演示:一个Nginx反向代理,一个后端应用,一个MySQL 8.0,一个Redis。先把项目目录组织好,尽量让配置独立出来,不要全塞进YAML里。我的习惯是:
myapp/ ├── docker-compose.yml ├── .env ├── mysql/ │ └── conf.d/ │ └── my.cnf ├── nginx/ │ └── conf.d/ │ └── default.conf └── app/ └── Dockerfile之所以把MySQL的配置单独放一个目录,是为了不改镜像里的默认配置也能自定义字符集、连接数等参数。Nginx的default.conf单独放,方便日后调整反向代理规则。这种做法不是为了花哨,而是为了让你在改配置的时候不用重新构建镜像,只改宿主机上的文件再重启容器就好。
docker-compose.yml的框架看起来是这个样子:
name: myapp services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: demo MYSQL_USER: demo MYSQL_PASSWORD: demo123 ports: - "3306:3306" volumes: - mysql_data:/var/lib/mysql - ./mysql/conf.d:/etc/mysql/conf.d networks: - app_net restart: unless-stopped redis: image: redis:7 command: ["redis-server", "--appendonly", "yes"] ports: - "6379:6379" volumes: - redis_data:/data networks: - app_net restart: unless-stopped nginx: image: nginx:1.27 ports: - "80:80" volumes: - ./nginx/conf.d:/etc/nginx/conf.d networks: - app_net restart: unless-stopped volumes: mysql_data: redis_data: networks: app_net: driver: bridge注意,我现在写Compose文件已经不带version字段了。Docker Compose v2 之后version已经废弃,写了反而会在某些版本里出现提示。name: myapp是给项目命名,如果不写,默认用当前目录名。
2.2 MySQL 8.0和Redis的配置要点
MySQL 8.0 这个镜像有几个关键点值得单独讲。环境变量里的MYSQL_ROOT_PASSWORD、MYSQL_DATABASE、MYSQL_USER、MYSQL_PASSWORD都是官方镜像在首次初始化数据库时读取的。也就是说,只有第一次启动且数据卷为空的时候,这些变量才会生效;如果你的数据卷已经有数据了,再改这些变量是不会改掉现有用户密码的。所以一旦初始化完,密码就不要靠改Compose文件来变,而是进容器用SQL改。
MySQL8.0默认认证插件是caching_sha2_password,如果遇到老版本的客户端连不上,你可以在my.cnf里指定default_authentication_plugin=mysql_native_password,不过新项目建议直接用兼容新插件的客户端,没必要为了老环境降低安全标准。
Redis这边,如果只是单机测试,用redis:7镜像配合command开启AOF持久化就够了。但很多人会在生产环境直接搞主从,我这里给一个双节点主从的示例。注意从节点的启动命令里,主机名要写redis-master而不是IP,这就是Compose网络的好处。
redis-master: image: redis:7 command: ["redis-server", "--appendonly", "yes"] volumes: - redis_master_data:/data networks: - app_net redis-slave: image: redis:7 command: ["redis-server", "--slaveof", "redis-master", "6379", "--appendonly", "yes"] depends_on: - redis-master volumes: - redis_slave_data:/data networks: - app_net--slaveof redis-master 6379里的redis-master会被Docker DNS解析成主容器的IP。这个方案比手工--link一个容器要清晰得多,以后加一个从节点只需要复制粘贴改下名字。
2.3 启动、验证与常用命令
配置文件写好后,先在项目目录执行:
docker compose config这个命令不会启动任何容器,只会把docker-compose.yml和.env合并后的最终配置输出出来,相当于“语法检查”。如果YAML有缩进错误或者字段写错,这里就会报出来,不用等到启动才发现问题。
通过后执行:
docker compose up -d-d表示后台运行。第一次执行会拉取镜像,时间可能比较长。启动完成后:
docker compose ps应该能看到每个服务、状态、端口映射都已经列出来。有问题就看日志:
docker compose logs -f只看某一个服务就用docker compose logs -f mysql。要进入容器调试用docker compose exec mysql bash。要停掉整个项目用docker compose down,如果想把数据卷也一起删掉用docker compose down -v,但-v会清空数据,生产环境慎用。
这里提醒一句:docker compose down和docker stop不一样。前者会把整个项目的网络也删掉,后者只是停容器。如果只是临时重启某个服务,用docker compose restart nginx就够了。
3. 进阶:多容器协作与依赖顺序(Nacos 3.x + MySQL案例)
3.1 为什么服务启动有先后顺序的坑
很多人在部署中间件组合的时候遇到过明明depends_on写了好好的,但服务还是起不来。原因在于depends_on默认只控制“容器的启动顺序”,并不等容器里的程序真正就绪。比如你写:
services: nacos: depends_on: - mysqlNacos容器会在MySQL容器启动后立刻启动,而MySQL容器虽然起来了,但可能还没有完成初始化、还没有监听3306端口。Nacos连接数据库失败,然后退出。这个时候你看到的现象就是“Nacos一直在重启”。
这在多容器协作里是非常典型的问题,尤其是带数据库的服务。MySQL启动后还需要几秒钟初始化、建库建用户,这段时间窗口就是“假就绪”状态。解决方案不是靠sleep,而是用健康检查 + 更严格的依赖条件。
3.2 健康检查与depends_on正确写法
Compose从 v2 开始支持depends_on搭配condition: service_healthy,这才是真正的“等服务就绪”。写法如下:
mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: nacos MYSQL_USER: nacos MYSQL_PASSWORD: nacos123 healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-proot123"] interval: 5s timeout: 3s retries: 10 start_period: 10s networks: - app_net nacos: image: nacos/nacos-server:v3.0.0 environment: MODE: standalone SPRING_DATASOURCE_PLATFORM: mysql MYSQL_SERVICE_HOST: mysql MYSQL_SERVICE_PORT: 3306 MYSQL_SERVICE_DB_NAME: nacos MYSQL_SERVICE_USER: nacos MYSQL_SERVICE_PASSWORD: nacos123 NACOS_AUTH_ENABLE: "true" NACOS_AUTH_TOKEN: 需要设置一个很长的密钥 NACOS_AUTH_IDENTITY_KEY: serverIdentity NACOS_AUTH_IDENTITY_VALUE: security ports: - "8848:8848" depends_on: mysql: condition: service_healthy networks: - app_nethealthcheck里的mysqladmin ping会去确认MySQL真的能响应了。start_period的意思是给容器一段启动宽限期,在宽限期内的健康检查失败不计入重试次数,能有效避免MySQL刚开始启动时还没准备好被误判为失败。
Nacos这边只需要在depends_on下面加mysql: condition: service_healthy,就能保证Nacos一定在MySQL健康检查通过之后才启动。这个习惯建议从一开始就养成,不要靠restart: always硬等,因为反复重启日志刷屏不说,还容易出现初始化竞争。
3.3 bge-m3嵌入模型Compose部署示例
除了常规中间件,Compose同样适合拉起一些AI推理服务。这里举一个向量模型bge-m3的例子。bge-m3是一个文本嵌入模型,常见做法是把它装进一个带推理API的服务镜像里,再配合MySQL、Redis这些一起跑。于是可以在同一个Compose项目里加一个服务:
bge-m3: image: registry.cn-hangzhou.aliyuncs.com/xxx/bge-m3-api:latest ports: - "6006:6006" environment: MODEL_NAME: BAAI/bge-m3 USE_CUDA: "false" volumes: - model_cache:/root/.cache networks: - app_net restart: unless-stopped如果你要调用它,其他服务里的数据库地址还是写mysql,向量服务地址写bge-m3。我实测过这种方式部署bge-m3,最大的惊喜在于不用自己去配一堆PYTHONPATH和CUDA环境变量,Compose把你所有需要设置的东西都收敛在服务配置里。等以后想换成重型的GPU推理服务,只需要把USE_CUDA改成true并加上deploy资源限制,不需要改动其他服务。
4. 高频报错自查:这些错我全踩过
4.1 docker: unknown command "compose"怎么破
新手在Linux服务器上执行docker compose up -d,最常遇到的一个报错是:
docker: unknown command: compose这个错误表明你的Docker环境里没有安装Compose插件。现在Docker官方推荐的是Compose v2插件,它和Docker CLI集成在一起,命令格式是docker compose(中间有空格)。老版本的做法是单独装一个叫docker-compose的Python工具,命令格式是docker-compose(带连字符)。两者不是一回事。
解决方法是给Docker安装官方插件。在Debian系的系统上可以执行:
sudo apt-get update sudo apt-get install docker-compose-pluginCentOS/RHEL系:
sudo yum install docker-compose-plugin装完后再执行docker compose version,能看到版本号就说明OK了。如果上面都不行,也可以去GitHub下载docker-compose-linux-x86_64二进制文件放到/usr/local/bin/docker-compose并加执行权限,这样docker-compose这条老命令也可以用了。我个人的建议是尽快统一到docker compose,因为官方插件更新快,功能也更全。
4.2 Windows Docker Desktop启动失败:Virtualization Support not Detected
Windows上装Docker Desktop,很多人一启动就弹出类似virtualization support not detected的提示,Docker Desktop能装但起不来。出现这个报错,绝大多数是电脑的CPU虚拟化没有开启,或者Windows的WSL2环境没就绪。
排查路径从简单到复杂可以这么做:
- 打开任务管理器,切到“性能”标签,看看底部“虚拟化”一栏是“已启用”还是“已禁用”。如果显示已禁用,需要重启电脑进BIOS/UEFI,找到Intel的
VT-x/Intel Virtualization Technology或AMD的SVM/AMD-V选项,把它设为Enabled。笔记本品牌不同,按键也不同,一般是开机时反复按F2、F10或者Del。 - 检查是否已经启用Windows功能里的“虚拟机平台”和“适用于Linux的Windows子系统”。在控制面板“启用或关闭Windows功能”里勾选这两项,重启电脑。
- 确认WSL2是否安装成功。在管理员PowerShell里执行
wsl --status,能看到默认版本是2就行。如果版本是1,执行wsl --set-default-version 2切换。 - 如果以上都正常,但Docker Desktop还是报错,关掉再以管理员身份运行一次。
这个报错解决之后,Docker Desktop就能正常启动,Compose里的Linux容器也就能跑起来了。我见过不少人卡在这一步很久,其实是BIOS里的虚拟化开关没开,属于最简单也最容易忽略的坑。
4.3 容器网络不通的排查清单
Compose项目里最常见的网络问题就是“容器A访问容器B超时”。明明同一个项目、同一个网络,为什么还访问不通?我自己总结了一套排查清单,按顺序来基本都能找到原因。
第一,确认两个容器是否在同一个网络。执行docker inspect <容器名>看NetworkSettings.Networks下的信息,或者直接docker compose ps确认应用都归属于同一个项目。如果你用docker run额外起了一个容器,没有加入Compose创建的网络,那它当然访问不到Compose内的服务。
第二,在容器内部测试DNS解析。执行docker compose exec <目标服务> ping <对端服务名>,如果提示无法解析主机名,说明网络或者服务名写错了。Compose的服务名大小写敏感,注意保持一致。
第三,检查端口映射。宿主机访问数据库用localhost:3306,容器内部访问数据库用mysql:3306,是两个不同的入口。如果容器内部访问通、宿主机访问不通,优先检查ports是否写对,以及宿主机防火墙是否放行。
第四,看看是否有网络策略或者安全组限制。比如你在云服务器上部署,安全组没放行对应端口,那本地能访问,外部可能就不行。这个问题和Docker无关,但非常容易被甩锅给Compose。
最后,实在不行就重启网络桥接。有时候网络底层状态异常,执行docker compose down && docker compose up -d重建网络往往就能解决。这个操作容器数据不会丢,卷还在。
5. 从折腾到规范:我的Compose工作流建议
5.1 环境变量与敏感信息不写死在YAML里
docker-compose.yml虽然方便,但千万别把数据库密码、密钥这类敏感信息直接写死在文件里。一是因为Compose文件可能被放到Git仓库,会有泄露风险;二是因为不同环境(开发、测试、生产)的密码可能不同,写死了就得改文件,太麻烦。
我习惯在项目目录放一个.env文件:
MYSQL_ROOT_PASSWORD=root123 MYSQL_PASSWORD=demo123 NACOS_AUTH_TOKEN=这里写你的超长密钥然后在docker-compose.yml里用${VAR}的方式引用:
mysql: environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}启动时Compose会自动读取同目录下的.env文件进行变量替换。执行docker compose config可以检查替换结果。注意.env文件默认不会被Git追踪,但你有责任在.gitignore里加上它,防止不小心提交上去。如果是更严格的生产环境,建议直接用Docker Secrets或外部密钥管理,但中小项目用.env已经能解决大部分问题。
5.2 升级与回滚、数据备份
很多人跑通了Compose之后,总觉得一切万事大吉,但升级和回滚是最容易翻车的环节。比如你要把MySQL从8.0升级到8.4,直接改image字段然后docker compose up -d,如果数据目录还在,镜像版本不兼容可能导致数据库无法启动。
我的建议是:升级前先备份数据卷。一个简单的备份方法是在宿主机执行:
docker compose exec -T mysql mysqldump -uroot -p root123 demo > backup.sql如果容器里没有mysqldump,也可以直接备份整个数据卷目录。在Linux上数据卷路径可以加docker volume inspect <项目名>_mysql_data查出来;在Windows上Docker Desktop的卷在WSL2内部,备份起来没有Linux上直观,所以更推荐用mysqldump这种方式逻辑备份。
每次要改动Compose配置前,先确认一个“你信得过能跑起来”的版本。比如手动记录一下当前镜像的tag,或者直接把镜像tag固定成版本号,不要用latest。用latest部署测试可以,但在生产环境就是给自己埋雷,因为某天你执行docker compose pull可能拉到一个行为完全不同的新版本。
回滚就更简单了:把镜像tag改回上一版,执行docker compose up -d即可重建容器。只要数据卷没有破坏,配置没有重大不兼容,一般都能滚回来。
5.3 用Compose重构老项目的心态建议
如果你手头已经有一个用一堆docker run跑起来的遗留项目,别想着一次性全部迁移到Compose。我的做法是先列个清单,把所有容器按依赖关系分组:数据层一组、中间件一组、应用层一组。每组写一个独立的Compose文件,通过docker compose -f xxx.yml -f yyy.yml合并起来,也可以逐步迁移。
比如先把MySQL和Redis用Compose管理起来,剩下的容器暂时还用docker run,通过网络手动加入Compose创建的网络来和新容器互通。这样过渡期的风险小,等全部搬迁完,再删掉老的运行脚本。
实际上我后来遇到新需求,不管多简单的单容器服务,也会下意识地先写Compose文件而不是直接docker run。哪怕只有一个容器,用Compose管理也能获得可复用的配置、固定的启动方式、统一的日志查看入口。这个习惯看起来在“杀鸡用牛刀”,但长期来看维护成本低太多了。
最后再分享一个实用小技巧
每次写完Compose文件,一定要执行一下docker compose config看最终渲染的配置,而不仅仅是docker compose up -d。这个命令能帮你发现很多肉眼注意不到的问题,比如变量没写对、缩进错了、端口映射重复、卷路径不存在等。我到现在发布任何Compose项目前都会先跑一遍这步。
还有一个小细节:给每个服务设置restart: unless-stopped。这样宿主机重启后,Compose管理的容器会自动恢复,不用每次开机都手动去docker start一堆容器。这个策略对Web服务、数据库、Redis都很适用,临时跑一下的任务容器则没必要加。
Docker多容器部署这件事,说白了就是把“人的记忆”和“手工操作”降到最低。Compose帮你把每一个决定都写进文件里,看得见、改得动、能回滚。搞懂它之后,你再也不会回到那个敲满屏docker run参数、被端口冲突和网络不通折磨的日子。