Docker Compose 多服务编排:本地开发环境一键启动
入职第一天的下午,我盯着屏幕上第 5 个报错弹窗——MySQL 端口被占用,Redis 版本不兼容,RabbitMQ 死活连不上。旁边的前辈看了眼我的屏幕,叹了口气:“你这是装了多久?”
“从上午 9 点到现在。” 我小声说。
4 个小时。装 3 个数据库,花了 4 个小时。而这还只是开始,后面还有 ElasticSearch 和 MongoDB 要装。
那是我第一次意识到:本地开发环境,真的是个坑。一个巨大的、看不见底的坑。
后来我们团队换了 Docker Compose。新人 clone 仓库,docker-compose up -d,5 分钟,全部服务启动完毕。MySQL、Redis、RabbitMQ、API、Web,一行命令,干干净净。项目切换?换个目录,启动另一个 compose 文件就行。清理?docker-compose down -v,连数据卷一起删掉,不留痕迹。
这篇文章,我想分享的就是这个转变:如何用 Docker Compose 编排多服务,让本地开发环境从”噩梦”变成”一行命令的事”。
为什么需要多服务编排
说实话,十年前做单体应用的时候,环境配置简单得很——装个 JDK,配个数据库连接字符串,项目就能跑。但现在不一样了。
大多数项目拆成了微服务架构,或者至少也是前后端分离。一个典型的本地开发环境,至少需要这些东西:前端 Web 服务、后端 API 服务、MySQL 存业务数据、Redis 做缓存和 Session、RabbitMQ 处理异步消息。有的项目还要加 ElasticSearch 做搜索,MongoDB 存日志。
这时候问题就来了。
手动安装的痛苦
每台机器都要装一遍。MySQL 要选版本——5.7 还是 8.0?装错了版本,SQL 语法不兼容。Redis 要配端口——默认 6379,但万一被别的服务占用了呢?RabbitMQ 还要装 Erlang 运行环境……光 RabbitMQ 的安装教程,我就看了半小时。
装完之后,还有版本冲突的问题。本地跑过别的项目,MySQL 还留着旧版本的数据。端口冲突,服务启动失败。排查半天,发现是某个僵尸进程没杀干净。
最崩溃的是项目切换。刚做完项目 A,要切换到项目 B。项目 A 的 MySQL 占着 3306,项目 B 也想用 3306。要么改配置文件,要么停掉项目 A 的服务。改完配置,过两天又切回项目 A,配置又要改回来。
来回折腾。
团队协作的噩梦
“在我电脑上能跑啊。”
这句话,每个团队都听过吧。新人入职,clone 代码,装环境,跑不起来。为什么?老项目的 MySQL 版本是 5.7,新人装了 8.0;老项目的 Redis 没设密码,新人本地的 Redis 设了密码。配置文件改了好几处,还是跑不起来。
最后前辈过来帮忙,两个人花了大半天,才把环境调通。
这一天的时间,就这么没了。
Compose 的解决思路
Docker Compose 的思路很简单:把所有服务打包成容器,用一份配置文件统一管理。
你不用关心每个数据库怎么安装、怎么配置版本、怎么分配端口。Compose 文件里写清楚,一键启动,所有服务按配置运行。项目切换?换个目录,启动另一份 Compose 文件。清理?一行命令删掉所有容器和卷。
这就像从”自己组装电脑”变成了”买整机”。你不用管内存条怎么插、显卡怎么装电源线——厂商已经帮你弄好了,开机就能用。
docker-compose.yml 核心配置
先看一份完整的配置文件。假设我们的项目有 4 个服务:Web 前端、API 后端、MySQL 数据库、Redis 缓存。
# docker-compose.yml
version: "3.8" # Compose 文件版本,3.8 支持大多数配置选项
services:
# 前端 Web 服务
web:
build: ./frontend # 从本地 frontend 目录构建镜像
ports:
- "3000:3000" # 宿主机 3000 -> 容器 3000
depends_on:
- api # 依赖 api 服务,api 先启动
environment:
- API_URL=http://api:8080 # 前端访问后端的地址
# 后端 API 服务
api:
build: ./backend # 从本地 backend 目录构建镜像
ports:
- "8080:8080"
depends_on:
- mysql
- redis # 依赖数据库和缓存
environment:
- DB_HOST=mysql # 数据库地址(容器名)
- DB_PORT=3306
- DB_USER=root
- DB_PASSWORD=dev123 # 开发环境密码,生产环境用 .env 文件
- REDIS_HOST=redis
- REDIS_PORT=6379
# MySQL 数据库
mysql:
image: mysql:8.0 # 直接使用官方镜像,不构建
ports:
- "3306:3306"
environment:
- MYSQL_ROOT_PASSWORD=dev123
- MYSQL_DATABASE=myapp # 自动创建数据库
volumes:
- mysql_data:/var/lib/mysql # 数据持久化到卷
# Redis 缓存
redis:
image: redis:7-alpine # alpine 版本更小
ports:
- "6379:6379"
volumes:
mysql_data: # 定义数据卷,MySQL 数据持久化存储
核心字段解释
services 下面定义所有服务。每个服务可以有三种来源:build 从本地代码构建,image 直接拉取官方镜像,或者两者结合(本地构建 + 基于某个镜像)。
ports 做端口映射。格式是 "宿主机端口:容器端口"。上面的配置里,Web 映射到 3000,API 映射到 8080,MySQL 映射到 3306,Redis 映射到 6379。如果本地端口被占用,可以改宿主机端口,比如 "13006:3306",这样你用 localhost:13006 连接 MySQL。
depends_on 控制启动顺序。MySQL 和 Redis 先启动,然后 API(依赖它们),最后 Web(依赖 API)。但有个坑,下面会讲。
environment 设置环境变量。数据库密码、连接地址、端口号都可以在这里配置。注意,生产环境不要把密码写在这里,用 .env 文件或者环境变量注入。
volumes 做数据持久化。MySQL 的数据存到 mysql_data 卷,这样容器删掉后数据不会丢失。下次启动,数据还在。
一个容易踩的坑
容器名就是服务名。上面配置里,API 服务连接数据库用的是 DB_HOST=mysql,不是 localhost。为什么?
因为每个容器都是独立的网络环境。localhost 在 API 容器里指向的是 API 容器自己,不是宿主机,也不是 MySQL 容器。Compose 自动创建一个内部网络,服务之间用服务名互相访问。mysql 这个名字,就是 MySQL 容器在网络里的地址。
第一次写 Compose 文件的时候,我就踩了这个坑——写了 localhost:3306,死活连不上数据库。后来才发现,要用容器名。
服务依赖与启动顺序
depends_on 看起来很简单:MySQL 先启动,API 再启动。但实际上有个微妙的问题。
Compose 的 depends_on 只保证容器启动顺序,不保证服务就绪顺序。换句话说,MySQL 容器启动了,但 MySQL 服务可能还没准备好接受连接——正在初始化数据库、加载配置、启动监听端口。API 服务这时候去连接,大概率会失败。
我就遇到过这种情况。docker-compose up 之后,API 立即报错:数据库连接失败。过了 10 秒再试,又成功了。原因是 MySQL 容器启动了,但 MySQL 服务还没准备好。
解决方案一:健康检查
Compose 支持在配置里加健康检查。只有健康检查通过后,依赖它的服务才会启动。
services:
mysql:
image: mysql:8.0
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s # 每 5 秒检查一次
timeout: 3s # 超时时间
retries: 10 # 失败 10 次才认为 unhealthy
# ... 其他配置
api:
depends_on:
mysql:
condition: service_healthy # 等待 MySQL 健康检查通过
MySQL 会执行 mysqladmin ping 命令,检查自己是否可以接受连接。5 秒检查一次,最多等 10 次(50 秒)。只有健康检查通过了,API 才启动。
这个方法有效,但有个缺点:每个服务都要写健康检查配置。有些官方镜像(比如 Redis)没有提供方便的健康检查命令,要自己想办法。
解决方案二:应用层重试
更省事的办法是在应用代码里加重试逻辑。连接失败,等几秒再试。MySQL 启动慢,等它准备好就行。
指数退避是常用的策略:第一次等 1 秒,第二次等 2 秒,第三次等 4 秒……逐渐增加等待时间。大多数数据库在 30 秒内都能准备好。
Node.js 的话,可以用 mysql2 的连接池配置:
const pool = mysql.createPool({
host: 'mysql',
port: 3306,
user: 'root',
password: 'dev123',
database: 'myapp',
waitForConnections: true, // 等待连接可用
connectionLimit: 10,
queueLimit: 0,
});
Python 的话,用 tenacity 库做重试:
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, min=2, max=10))
def connect_db():
return mysql.connector.connect(host='mysql', ...)
两种方案的选择
健康检查更精确——只有数据库真的准备好了,API 才启动。但配置稍麻烦,每个数据库都要写对应的检查命令。
应用层重试更简单——代码里加几行,不用改 Compose 配置。缺点是 API 启动后会有一段时间不断重试,日志里会有连接失败的报错(虽然不影响最终结果)。
我个人更喜欢应用层重试,因为更省事,而且大多数情况下都能正常工作。健康检查作为备选,用在启动特别慢的服务上。
多环境配置策略
本地开发、测试环境、生产环境,配置通常不一样。比如端口映射——开发环境需要把数据库端口暴露出来,方便本地调试;生产环境不需要,数据库只在容器内部网络访问。
如果把这些配置都写在一份文件里,切换环境就要改文件,改完又改回来。麻烦,还容易改错。
Compose 有个设计解决这个问题:基础文件 + 覆盖文件。
基础文件:公共配置
docker-compose.yml 写所有环境共享的配置——服务定义、镜像版本、内部网络、数据卷。
# docker-compose.yml(基础配置)
version: "3.8"
services:
web:
build: ./frontend
# ports 不写,由覆盖文件补充
api:
build: ./backend
environment:
- DB_HOST=mysql
- REDIS_HOST=redis
# ports 不写
mysql:
image: mysql:8.0
volumes:
- mysql_data:/var/lib/mysql
# ports 不写,生产环境不需要暴露
redis:
image: redis:7-alpine
volumes:
mysql_data:
端口映射没写,因为不同环境的端口配置不一样。
开发环境覆盖文件
docker-compose.override.yml 补充开发环境的特定配置——端口映射、开发用的密码、调试用的环境变量。
# docker-compose.override.yml(开发环境)
version: "3.8"
services:
web:
ports:
- "3000:3000" # 开发环境暴露端口,方便本地访问
api:
ports:
- "8080:8080"
environment:
- DEBUG=true # 开发环境开启调试模式
mysql:
ports:
- "3306:3306" # 开发环境暴露数据库端口,方便连接调试
environment:
- MYSQL_ROOT_PASSWORD=dev123 # 开发环境简单密码
redis:
ports:
- "6379:6379"
Compose 有个默认行为:执行 docker-compose up 时,自动合并 docker-compose.yml 和 docker-compose.override.yml。两份文件的配置叠加,override 的配置覆盖基础配置。
所以本地开发,直接 docker-compose up,就能拿到开发环境的完整配置。
生产环境覆盖文件
docker-compose.prod.yml 补充生产环境的配置——不暴露端口、使用生产密码、连接外部服务。
# docker-compose.prod.yml(生产环境)
version: "3.8"
services:
web:
# 不暴露端口,由反向代理(nginx)访问
api:
environment:
- DB_HOST={{DB_HOST}} # 从环境变量读取,不在文件里写密码
- DB_PASSWORD={{DB_PASSWORD}}
mysql:
# 不暴露端口,外部无法直接连接
environment:
- MYSQL_ROOT_PASSWORD={{MYSQL_ROOT_PASSWORD}}
生产环境启动时,用 -f 参数指定配置文件:
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
两份文件叠加,prod 的配置覆盖基础配置。数据库端口不暴露,密码从环境变量读取。
环境变量注入
生产环境的密码不应该写在文件里。Compose 支持从 .env 文件或系统环境变量读取值。
# .env 文件(不要提交到 git)
DB_HOST=prod-mysql.internal
DB_PASSWORD=super_secret_password_123
MYSQL_ROOT_PASSWORD=another_secret
配置文件里用 {{VAR:-default}} 语法引用:
environment:
- DB_HOST={{DB_HOST:-localhost}} # 如果 DB_HOST 未设置,用 localhost
- DB_PASSWORD={{DB_PASSWORD:-dev123}}
.env 文件不要提交到版本控制,用 .env.example 写示例值,团队成员复制后填自己的实际配置。
一键命令实战
配置写好了,接下来就是启动、停止、调试。掌握这几个命令,日常操作基本够用。
启动全部服务
docker-compose up -d
up 启动所有服务。-d 表示后台运行(detached mode),不会占用终端。不加 -d,所有服务的日志会直接打印到终端,Ctrl+C 可以停止。
启动后,Compose 会拉取镜像(如果用 image)、构建镜像(如果用 build)、创建容器、启动服务。第一次启动会比较慢,因为要拉取镜像。后续启动就快了,镜像已经存在。
查看服务状态
docker-compose ps
列出所有容器的运行状态。输出类似这样:
NAME COMMAND SERVICE STATUS PORTS
myapp-web-1 "npm start" web running 0.0.0.0:3000->3000/tcp
myapp-api-1 "node index.js" api running 0.0.0.0:8080->8080/tcp
myapp-mysql-1 "mysqld" mysql running 0.0.0.0:3306->3306/tcp
myapp-redis-1 "redis-server" redis running 0.0.0.0:6379->6379/tcp
STATUS 显示 running 表示正常运行。如果显示 exited 或 error,说明服务启动失败。
查看服务日志
docker-compose logs -f api
查看 API 服务的日志。-f 表示持续跟踪(follow),新日志会实时显示。不加 -f 只显示已有日志。
不加服务名,docker-compose logs -f 会显示所有服务的日志,但日志量大的时候会很乱。
停止并清理
docker-compose down
停止所有容器,删除容器和网络。但数据卷不会删除——MySQL 的数据还在。
如果要彻底清理,包括数据卷:
docker-compose down -v
-v 删除数据卷。下次启动,MySQL 会重新初始化,数据全部清空。调试的时候经常用这个命令——数据乱了,清掉重来。
重新构建镜像
代码改了,要重新构建镜像:
docker-compose build api
只构建 API 服务的镜像。构建完,要重启容器:
docker-compose up -d api
或者一步到位,构建并重启:
docker-compose up -d --build api
--build 强制重新构建镜像,即使镜像已存在。
常用命令速查表
| 命令 | 作用 |
|---|---|
docker-compose up -d | 后台启动全部服务 |
docker-compose ps | 查看运行状态 |
docker-compose logs -f api | 查看 API 日志 |
docker-compose down | 停止并删除容器 |
docker-compose down -v | 停止并删除容器和数据卷 |
docker-compose restart api | 重启 API 服务 |
docker-compose build api | 重新构建 API 镜像 |
这些命令覆盖了日常 90% 的操作。其他命令(exec、cp、top)用到的时候再查文档。
Docker Compose 多服务编排实战
用 Docker Compose 编排 Web、API、MySQL、Redis 四个服务,实现本地开发环境一键启动
⏱️ 预计耗时: 15 分钟
- 1
步骤1: 创建 docker-compose.yml 文件
在项目根目录创建配置文件:
```yaml
version: "3.8"
services:
web:
build: ./frontend
ports: ["3000:3000"]
depends_on: [api]
api:
build: ./backend
ports: ["8080:8080"]
depends_on: [mysql, redis]
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: dev123
MYSQL_DATABASE: myapp
redis:
image: redis:7-alpine
```
注意:容器间通信用服务名(如 DB_HOST=mysql),不是 localhost - 2
步骤2: 启动全部服务
在 docker-compose.yml 所在目录执行:
```bash
docker-compose up -d
```
• 首次启动会拉取镜像,耗时较长
• 后续启动直接使用已有镜像,秒级完成
• 不加 -d 会占用终端显示日志 - 3
步骤3: 验证服务运行状态
检查所有容器是否正常启动:
```bash
docker-compose ps
```
• STATUS 显示 running 表示正常
• 如有 exited 或 error,用 logs 排查:
```bash
docker-compose logs api
``` - 4
步骤4: 配置多环境(可选)
创建 docker-compose.override.yml(开发环境):
```yaml
version: "3.8"
services:
mysql:
ports: ["3306:3306"]
```
• Compose 自动合并 override 文件
• 生产环境用 -f 指定:
```bash
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
``` - 5
步骤5: 清理环境
停止并删除所有容器:
```bash
docker-compose down # 保留数据卷
docker-compose down -v # 删除数据卷(数据清空)
```
• 切换项目时用 down 清理
• 数据乱了想重来,用 down -v
结尾
对比一下传统方式和 Compose 方式的效率:
| 操作 | 传统方式 | Compose 方式 |
|---|---|---|
| 新人入职装环境 | 4-8 小时 | 5 分钟(clone + up) |
| 项目切换 | 改配置、停服务、重启 | 切目录、启动另一份 compose |
| 清理环境 | 手动卸载、排查残留进程 | 一行命令删容器和卷 |
| 团队环境一致性 | 每台机器都可能不一样 | 配置文件统一,环境完全一致 |
差距很明显。
如果你还在手动装数据库、改配置文件、排查端口冲突,试试 Docker Compose。先从简单的项目开始——一个 API + 一个 MySQL。写一份 docker-compose.yml,跑起来。熟练了,再加 Redis、RabbitMQ,做多环境配置。
团队里推广的话,把 docker-compose.yml 和 docker-compose.override.yml 提交到仓库,加个 README 说明启动步骤。新人入职,clone 代码,一行命令,开发环境就准备好了。
这比写”环境配置文档”靠谱多了。文档会过期,配置文件不会。
常见问题
docker-compose.yml 和 Dockerfile 有什么区别?
depends_on 能保证服务就绪吗?
容器内怎么访问宿主机的服务?
数据存在哪里?容器删了数据会丢吗?
多环境配置(dev/test/prod)怎么管理?
端口被占用怎么办?
13 分钟阅读 · 发布于: 2026年4月9日 · 修改于: 2026年4月9日
相关文章
Supabase Storage实战:文件上传、权限控制与CDN加速
Supabase Storage实战:文件上传、权限控制与CDN加速
n8n 进阶实战:Webhook 触发与 IF/Switch 条件分支设计
n8n 进阶实战:Webhook 触发与 IF/Switch 条件分支设计
GitHub Actions Matrix 矩阵构建:多版本并行测试实战

评论
使用 GitHub 账号登录后即可评论