先说个扎心的事实:我见过不少把 Docker 命令练得滚瓜烂熟的人,一旦服务数量超过三个,照样翻车。Docker 多容器环境里的问题,从来不是“会不会敲 docker run”,而是怎么把好几条命令的参数、网络、依赖关系管理得清清楚楚。于是 Compose 就成了绕不开的工具——它用一份 docker-compose.yml 描述整个应用栈,一条 docker compose up -d 把全部服务拉起来,把多容器部署从手工操作变成了声明式管理。这篇文章适合刚拿到 Docker 还在摸索的新人,也适合已经被一堆中间件容器折磨的运维和开发,照着抄就行。
1. 多容器部署,为什么会变成一团乱麻
1.1 从一条命令到一堆命令:手动部署的失控过程
随便拆一个实际场景:本地要跑一套带前端、后端、MySQL、Redis 的应用。最原始的做法,打开笔记软件,把以前用过的 docker run 命令复制出来改改。MySQL 一条、Redis 一条、后端应用一条、Nginx 又一条。每条命令都要看清楚映射了哪些端口、挂载了哪些目录、设置了哪些环境变量,更不用说容器之间还要加 network 参数才能互相访问。
这个过程的第一个坑是容易漏参数。漏了端口映射最多是访问不到,漏了 volume 就会面临容器删掉后数据全部消失的风险。第二个坑是启动顺序。后端依赖 MySQL,但你不知道 MySQL 什么时候真正把 3306 端口立起来,经常出现后端连不上库直接崩溃退出的情况。第三个坑更难察觉:这些 docker run 命令散落在各处——有的在聊天记录里,有的在 Wiki 页面里,有的在某人的终端历史里。项目换人维护,环境就变成了玄学。
这不是夸张。我见过生产环境里有人在服务器上手动敲了七八条 docker run,某次升级镜像时漏掉一个参数,服务起不来了,最后花了一个多小时复原环境。多容器一旦超过三个,光靠 docker run 已经不可持续。真正的问题不是镜像拉取,而是状态管理:谁跟谁要在一个网络里、谁要先启动、数据往哪里存、资源怎么限制。
1.2 Compose 解决问题的方式:状态用文件管,环境用代码管
Docker Compose 的核心,是把“容器应该怎么跑”从命令行参数变成一份声明式配置文件。你写一个 docker-compose.yml,把这个多容器应用需要的所有服务、镜像、端口、数据卷、网络全部描述清楚,然后 Compose 根据这份文件自动完成创建网络、拉镜像、启动容器的工作。
用 Compose 之后,至少四个层面的问题被解决:
- 可复现。同一份配置文件在任何机器上跑,起出来的环境基本一致。
- 可共享。docker-compose.yml 能提交到 Git 仓库,新人克隆项目后一条命令就能把依赖服务拉起来。
- 可管理。up、down、ps、logs、restart 这些命令统一管理整个服务栈,不再需要挨个容器操作。
- 网络自动编排。Compose 默认会给项目创建一个专属网络,各服务之间直接用服务名通信。
这一步看似只是把命令搬进文件,实际改变的却是协作方式。以前“帮我看看环境”是噩梦,现在只要把 compose 文件发过去,对方自己就能把环境还原出来。下面所有实操内容,都是围绕这份文件展开的。
2. 先把本机环境搞利索:安装 Docker 与 Compose
2.1 Docker Desktop:Windows / macOS 上最容易踩的坑
如果你用的是 Windows 或 macOS,最省事的路径是装 Docker Desktop。安装包很大,自带 Compose 插件,界面也直观。但 Windows 上最容易翻车的是启动时报错,提示虚拟化支持未检测到之类的信息。出现这个提示,通常不是 Docker Desktop 本身坏了,而是电脑没满足虚拟化条件的其中一条:
- Windows 的“虚拟机平台”和“适用于 Linux 的 Windows 子系统”两个可选功能没有开启。
- BIOS / UEFI 里 CPU 虚拟化(Intel VT-x 或 AMD-V)被关闭了。
- 系统版本较老,Hyper-V 或 WSL2 组件不完整。
这类问题的处理顺序,我建议先看功能开关,再进 BIOS 确认虚拟化,最后才考虑重装。WSL2 需要 Windows 10 2004 或更高版本,装好后在 PowerShell 里执行 wsl --status 检查状态。还有个细节:Docker Desktop 安装完通常要重启系统,不重启哪怕服务显示 running 也是假的,后面跑容器往往各种连不上。装好之后打开终端执行 docker version 和 docker compose version,两条命令都正常输出,环境才算真正到位。
2.2 Linux 上在线安装:Docker 引擎与 Compose 插件的关系
Linux 服务器上安装 Docker 没有图形界面,一般走系统安装源。以 Debian / Ubuntu 系为例,先更新包索引,然后安装 docker-ce 那几个核心包。国内网络环境直接从官方源拉取很慢,建议装完引擎后把镜像加速地址写进 /etc/docker/daemon.json,用国内主流云厂商提供的加速地址即可。
新版本 Docker 把 Compose 做成了插件,安装后可以执行 docker compose(带空格)这个命令。如果只装了 Docker 引擎没装插件,一敲 docker compose 必报错。这个报错的修复方式很简单,找到你的发行版对应的 docker-compose-plugin 包装上就行。红帽系用 dnf,Debian 系用 apt,装上后 docker compose version 能正常显示版本号,问题就解决了。
大致流程如下:
# Debian/Ubuntu 系列在线安装 sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin sudo systemctl enable --now docker docker compose version有些定制化 Linux 发行版可能遇到内核模块或依赖缺失的情况,安装前先确认内核版本和 Docker 的兼容性要求,必要时手动安装额外依赖。这一步看着基础,却是大量“Compose 用不了”问题的源头。
2.3 版本与命令的坑:docker-compose 还是 docker compose
这两者的关系经常把人搞晕。docker-compose 是早期独立的 Python 工具,发布为可执行文件,命令是 docker-compose up;docker compose 是后来内置到 Docker 生态的插件形式,命令是 docker compose up。现在官方推荐后者,因为随 Docker 一起安装,升级方便。但网上大量教程还停留在前者,尤其是老项目的脚本里到处是 docker-compose。
我的习惯是优先使用带空格的 docker compose。如果你所在的团队还在用老脚本,那就保持 docker-compose 并用,不要在同一台机器上今天敲这个、明天敲那个。两个命令对 compose 文件版本的兼容策略有差异,混着用时会带来一些莫名其妙的错误,比如某个字段在插件版里正常,在独立版里直接报不支持。
3. 用 Compose 编排你的第一个多容器应用
3.1 从一份最小的 docker-compose.yml 开始
不管你的应用多复杂,Compose 文件的骨架就三块:services、volumes、networks。services 定义每个容器,volumes 声明数据卷,networks 声明自定义网络。写一个最小的例子:一个 Nginx,一个 Redis。
services: web: image: nginx:1.27-alpine ports: - "8080:80" volumes: - ./html:/usr/share/nginx/html redis: image: redis:7-alpine ports: - "6379:6379"这里有几个点值得注意:
- 缩进必须是空格,不能是 Tab,YAML 对缩进敏感,一个 Tab 就会报解析错误。
- ports 里的 "8080:80" 是宿主机端口:容器端口,左边是外面访问用的,右边是容器内部进程监听的。
- volumes 里 ./html 是相对当前目录的路径,Compose 执行时以 docker-compose.yml 所在目录为基准。
- 我刻意没写 version 字段。新版 Compose 已经不需要 version,写上反而会出现弃用警告。
执行 docker compose up -d 后,docker compose ps 能看到两个服务都起来了,浏览器打开 http://宿主机IP:8080 就能看到 Nginx 页面,Redis 也可以从宿主机用 redis-cli -h 127.0.0.1 连上。这个最小案例虽然简单,但已经把 Compose 三大核心要素都用上了,后面所有复杂编排都是在这个骨架上长出来的。
3.2 启动顺序与健康检查:depends_on 不是万能的
多容器应用最常见的痛点就是服务依赖。后端要等数据库就绪,接口网关要等后端就绪。Compose 提供的 depends_on,很多人以为是“等对方准备好再启动我”,其实它只负责“先启动对方”,不负责“确认对方可用”。
举例来说,MySQL 容器进程起来、3306 端口开始监听,可能还需要几秒进行 InnoDB 初始化。如果只写 depends_on,后端容器可能在 MySQL 还没准备好时就开始连接,于是报错。正确姿势是给依赖方配置 healthcheck,然后在 depends_on 里加上 condition: service_healthy。
services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123 healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] interval: 5s timeout: 3s retries: 10 backend: image: my-backend:latest depends_on: mysql: condition: service_healthy这样 Compose 会先启动 MySQL,轮询健康检查直到通过,再启动 backend。healthcheck 的时间参数也要注意:interval 太短、retries 太少,在网络拥堵或镜像冷启动时会误判失败;太长又拖慢整个启动流程。我一般用 5 秒间隔、10 次重试、3 秒超时,这个组合在高负载机器上也扛得住。
3.3 实战:MySQL 8.0 + Redis 主从,一个能直接用的 Compose 文件
下面是一套我经常直接复制给团队的组合:一个 MySQL、一套 Redis 主从。Redis 主从看起来要两个容器,实际上 Compose 里就是两个服务,主库对外提供服务,从库通过 replicaof 指定主库。
services: mysql: image: mysql:8.0 container_name: app-mysql restart: always environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: appdb MYSQL_USER: appuser MYSQL_PASSWORD: apppass ports: - "3306:3306" volumes: - mysql-data:/var/lib/mysql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] interval: 5s timeout: 3s retries: 10 redis-master: image: redis:7-alpine container_name: app-redis-master restart: always command: ["redis-server", "--appendonly", "yes"] ports: - "6379:6379" volumes: - redis-master-data:/data redis-slave: image: redis:7-alpine container_name: app-redis-slave restart: always command: ["redis-server", "--replicaof", "redis-master", "6379"] depends_on: - redis-master ports: - "6380:6379" volumes: - redis-slave-data:/data volumes: mysql-data: redis-master-data: redis-slave-data:这里最值得琢磨的是从库的 replicaof 写法。它指向的是 redis-master 这个服务名,而不是 localhost。因为 Compose 让这两个容器处在一个内部网络里,服务名就是地址。从库的端口映射写成 "6380:6379",意味着宿主机上访问 6380 会进入从库的 6379。验证主从同步,进从库容器执行 redis-cli info replication,能看到 role:slave 以及 master_link_status:up。
这个完整案例覆盖了 Compose 日常九成的功能:端口映射、数据卷、环境变量、健康检查、服务引用和内部 DNS。接下来要单独讲网络,因为凡是报“容器连不上”“服务名解析不了”的人,多半是没搞明白 Compose 网络这一层。
4. 网络配置:容器互通的命门
4.1 Compose 默认网络机制:什么都没写,网络其实已经存在
大多数人第一次接触 Compose 都会好奇:为什么容器之间能用服务名互相连通?答案是 Compose 自动创建了一个默认网络,名字通常是“项目目录名_default”。比如在 myapp 目录下执行 docker compose up,会生成一个 myapp_default 网络,这个项目里所有服务都会接入它,并且自动获得基于服务名的 DNS 解析。
这套机制带来两个便利:一是你不用手工创建网络,二是服务名可以直接当主机名用。前面 Redis 从库写 replicaof redis-master 6379,就完全避开了 IP 地址的硬编码问题。IP 地址在容器重建后会变化,服务名永远是稳定的,这也是容器编排里“逻辑寻址”的核心思维。
需要留意的是两个 Compose 项目之间要互通,或者多个服务需要划分不同网络的场景。这时候用自定义网络更合适:
networks: backend-net: driver: bridge services: mysql: networks: - backend-net backend: networks: - backend-net自定义网络还能指定子网和 IP 池配置,但日常真不建议随便指定静态 IP,除非有极特殊的依赖。静态 IP 帮你省了眼前一时,但之后每次改动网络配置,都会变成灾难。
4.2 “docker 网络不通”排查清单:一条条对过去
网络问题大概是 Compose 排错里最高频的。很多人报“容器连不上”“请求超时”,问题并不在 Compose 配置,而是概念混淆。我按经验整理了清单:
- 宿主机访问容器端口失败:优先检查 ports 里的宿主机端口是否被占用,以及容器内进程是否真的在监听容器端口。docker compose ps 能直接看到端口映射关系。
- 容器访问宿主机服务失败:容器里的 localhost 不是宿主机的 localhost。容器访问宿主机服务,需要特殊地址,Windows 和 macOS 上是 host.docker.internal,Linux 上往往直接走网关 IP。很多人在容器里连宿主机数据库,一写 127.0.0.1 就失败,问题就在这。
- 两个服务之间连不上:先确认它们是否在同一个 Compose 项目里。跨项目的服务默认不互通,要么把两者放进同一个 Compose 文件,要么挂到同一个外部网络。
- DNS 解析不了服务名:检查服务名是否写错。Compose 网络里解析的是 services 下的服务名,而不是 container_name。container_name 只是给宿主机看的名字。
- 端口映射没问题但业务报错:不要急着怀疑网络,先看日志。docker compose logs 服务名,很多时候是连接串写错或配置权限问题。
排查顺序我习惯按“日志 → 端口 → 网络 → 权限”来。网络问题最怕上来就乱改配置,先冷静看 docker compose ps 和 docker network inspect,往往半分钟就能定位。
5. 中间件容器化实战:MySQL、Redis、Nacos 的 Compose 部署
5.1 MySQL 8.0 容器化:环境变量、数据卷与初始化脚本
用 Compose 部署 MySQL 8.0,环境变量是标准手段。MYSQL_ROOT_PASSWORD 设置 root 密码,MYSQL_DATABASE 创建初始库,MYSQL_USER 和 MYSQL_PASSWORD 创建业务账号。很多人忽略的是:一旦数据卷里已经有数据,这些环境变量就失效了。初始化只发生在数据卷第一次为空时。你改了密码配置但数据卷还在,容器启动用的还是老密码,这是“MySQL 密码改不生效”最常见的原因。
数据卷这块,MySQL 必须用 volume 或 bind mount 把 /var/lib/mysql 挂出来。我倾向用命名卷,因为 bind mount 在某些 Linux 环境下会遇到 SELinux 权限问题,而且命名卷的备份和迁移更规范。需要初始化 SQL 的话,可以在挂载目录放 sql 脚本,官方镜像会自动执行 /docker-entrypoint-initdb.d 下的脚本,很适合放表结构初始化、基础数据导入。字符集方面,MySQL 8.0 默认 utf8mb4 基本够用,应用连接串里显式指定字符集是应用侧的事,别指望镜像配置能一步解决。
5.2 Redis 主从与持久化:用同一个 Compose 文件编排多实例
Redis 主从在 Compose 里很能体现“多容器”的优势。主从两个服务,本质是同一个镜像,通过不同 command 参数区分角色。主库开启 AOF 持久化用 --appendonly yes,从库通过 --replicaof 指向主库。这里有个细节:从库启动未必需要显式等待主库就绪,因为 Redis 主从可以异步重连,主库还没起来时从库也会按配置周期性重试。所以 depends_on 在 Redis 主从中不是必须的,写上只是为了启动顺序更明确。
持久化层面,Redis 的 appendonly 数据落在 /data 目录,需要 volume 挂载。主从两个容器各挂各的数据卷,别挂同一个目录,否则两个进程同时写容易出问题。如果想用自定义 redis.conf,记得把配置文件和 volume 分开挂,改配置后重启容器才会生效;如果挂错了位置,只有删除容器重建才能看到变化,这个坑我也踩过。
这类思路其实对所有中间件都适用。哪怕是部署一个向量模型配合数据库,也是镜像 + 数据卷 + 端口映射的组合,换汤不换药。
5.3 Nacos 3.x 部署要点:当中间件遇到依赖数据库
Nacos 用 Compose 部署时,至少涉及两个服务:Nacos 本身和它依赖的 MySQL。这里最能体现健康检查的价值,Nacos 启动时会校验数据库连接,MySQL 没就绪,Nacos 会反复报连接失败。
Nacos 环境变量的重点:
- MODE=standalone 表示单机模式,集群模式需要额外配置通信端口。
- MYSQL_SERVICE_HOST、MYSQL_SERVICE_PORT、MYSQL_SERVICE_DB_NAME、MYSQL_SERVICE_USER、MYSQL_SERVICE_PASSWORD,这些变量告诉 Nacos 往哪个库写配置。
- NACOS_AUTH_ENABLE:生产环境建议开启鉴权,尤其是部署在内网或者办公网的时候,不鉴权相当于把配置中心裸奔。
Nacos 的配置持久化也很关键。它的数据分两部分:一部分存 MySQL,另一部分是 Nacos 自身的工作目录,比如本地缓存、Token 等。建议把 /data 挂出来,升级镜像时不至于丢失状态。部署完先确认 MySQL 里能看到 Nacos 建的表,再访问 Nacos 控制台确认能正常登录。3.x 相比旧版本在架构和鉴权上都有调整,具体环境变量以你拉取的镜像版本对应文档为准。
6. 排错实录与常见问题速查
6.1 命令与启动报错:高频问题速查表
这么多年接触下来,有一批报错极其高频,我整理成表格:
| 报错 / 现象 | 常见原因 | 解决思路 |
|---|---|---|
| docker: unknown command: docker compose | Compose 插件未安装 | 安装 docker-compose-plugin 或使用独立 docker-compose |
| 弃用警告:version is obsolete | compose 文件顶部写了 version 字段 | 删掉该字段,新版 Compose 不需要 |
| port is already allocated | 宿主机端口被其他容器或进程占用 | 换端口或停掉占用方 |
| YAML 解析错误:mapping values are not allowed | Tab 缩进或冒号、引号格式错误 | 检查文件缩进,统一用空格 |
| 容器启动后立即退出:exited (1) | 环境变量缺失、配置文件权限、入口命令异常 | 先看日志 docker compose logs 服务名 |
| volume Permission denied | bind mount 目录属主不匹配 | 调整目录权限或用命名卷 |
遇到启动失败,第一反应不要盯配置文件干瞪眼,先看日志。docker compose logs 是个被低估的命令,它能按服务过滤输出,加 --tail 可以实时跟踪。偶尔会遇到 Compose 文件里挂载的宿主机目录不存在的情况,Compose 会自动创建,但创建出来的目录属主可能是 root,跟容器内用户冲突,这时候用 chown 调整或用命名卷都能绕过去。
6.2 数据持久化与备份:别让 down 命令毁掉你的数据库
Compose 的 down 命令有个非常危险的参数:-v。docker compose down 会停止并删除网络但保留卷,数据和配置都还在;docker compose down -v 会连命名卷一起删掉,数据彻底没了。生产环境里误删卷的案例可真不少,多数都是一顺手把 -v 打出来了。
我的操作习惯是:
- 日常重启用 up -d --force-recreate 或 restart,不要动不动 down。
- 真要 down,绝不加 -v。如果确实需要清数据,先冷静确认卷名再动手。
- 备份用容器化方式做,可以临时起一个 busybox 容器挂同一个卷打 tar 包,或者直接对 MySQL 用 mysqldump。
给一个 MySQL 备份的简单例子:
docker compose exec mysql sh -c 'exec mysqldump -uroot -proot123 appdb' > appdb.sqlRedis 持久化备份更简单,把 AOF 或 RDB 文件从数据卷里拷出来就行,也可以用 redis-cli --rdb 导出。最重要的是把备份命令写进脚本形成习惯,而不是等到出事那天才翻文档。
6.3 日常运维习惯:像管理代码一样管理 Compose 配置
Compose 配置本质是代码,所以应该像代码一样管理。第一条,docker-compose.yml 放进 Git 仓库,变更走 diff 和 review,不要在生产机上顺手编辑。第二条,镜像要固定版本或摘要,不要用 latest,不然哪天镜像更新了,环境就和你的预期不一致了。第三条,给所有服务加上 restart 策略,避免机器重启后容器不复活。第四条,日志要配置轮转,不限制日志大小的话,几个月后磁盘满是你应得的。
在 docker-compose.yml 里配置日志轮转:
services: backend: logging: driver: json-file options: max-size: "20m" max-file: "5"这套习惯看着繁琐,但能避免掉九成的部署事故。我把常用命令整理成一份速查:
| 命令 | 作用 |
|---|---|
| docker compose up -d | 启动所有服务 |
| docker compose ps | 查看服务状态 |
| docker compose logs -f 服务名 | 跟踪服务日志 |
| docker compose exec 服务名 命令 | 进入运行中的容器执行命令 |
| docker compose restart 服务名 | 重启某个服务 |
| docker compose down | 停止并清理网络,保留卷 |
| docker compose config | 验证 compose 文件合法性 |
最后再分享一个体会。我早期也是一个 docker run 遛到飞起的选手,遇到多容器就手写脚本,直到有一次升级缓存中间件时牵一发动全身,把整个服务栈拖垮,才真正体会到 Compose 用文件管理状态的意义。现在不管项目多小,只要超过一个容器,我都坚持写 docker-compose.yml 并放进仓库。用好这份配置文件,比背熟所有命令都重要。后面你还可以继续往这个方向扩展,比如用 profile 区分开发和生产环境、把多个 compose 文件组合成 override 配置。等你在实际项目里跑顺了,大概率会跟我一样感叹:多容器部署,早就该这么干了。