别再手动敲三遍 docker run 了:学会使用 Docker Compose 多容器编排
摘要:跑一个 NestJS + MySQL + Redis 的项目,你得手动敲三遍 docker run,还要自己建网络让它们互通。Docker Compose 把这些步骤写进一个 YAML 文件,从此只需一条 docker compose up -d,所有服务一起上线。本文从 Compose 的核心定位出发,拆解 YAML 参数、多环境管理、与 Dockerfile 的协作,并以 Milvus 向量数据库的编排为例,展示生产级 Compose 的完整写法。
📑 目录
- 为什么需要 Docker Compose?------ 从"一道菜"到"一桌宴席"
- 核心认知:Compose 不是"合并镜像",而是"编排容器"
- 安装方式:Docker Desktop 自带,Linux 需单独装
- Compose YAML 核心参数全解
- 实战案例:Milvus 向量数据库的 Compose 编排
- 多环境管理:基准文件 + 环境覆盖
- 书写规范与最佳实践
- 互动讨论
为什么需要 Docker Compose?------ 从"一道菜"到"一桌宴席"
在之前的 Docker 学习中,你已经熟悉了 docker run 命令------它像"开火炒一道菜",启动一个容器。但在真实项目中,一个应用往往由多个服务组成:
- 后端 API(NestJS / Express)
- 数据库(MySQL / PostgreSQL)
- 缓存(Redis)
- 消息队列(RabbitMQ / Kafka)
- 向量数据库(Milvus)
如果每个服务都手动敲一遍 docker run,还要自己创建网络让它们互通、管理数据卷、控制启动顺序......不仅繁琐,而且容易出错。
Docker Compose 就是来解决这个问题的。 它把"整桌宴席的配餐单"写进一个 YAML 文件,从此只需一条命令,所有服务一起启动、协同工作。
类比:
- Dockerfile 是"预制菜制作说明书"(做镜像)
- docker run 是"开火炒一道菜"(跑容器)
- Docker Compose 是"整桌宴席的配餐单"------每道菜独立装盘,但一起上桌
核心认知:Compose 不是"合并镜像",而是"编排容器"
这是最容易踩的坑。
误区 :以为 Compose 是把多个镜像"塞进"一个容器里。
真相 :一个容器只能跑一个镜像的运行时实例 ------这是 Docker 的物理边界,无法突破。Compose 的真正作用是 "一次性管理多个容器" ,让它们共享网络、数据卷,协同工作。
正确表述:Compose = 一次性拉起一组容器,让它们共享网络、数据卷,协同工作。
生活中的类比
想象你要办一桌宴席:
| 角色 | Docker 对应物 |
|---|---|
| 每道菜 | 一个容器 |
| 每道菜的菜谱 | 一个镜像 |
| 上菜顺序 | depends_on 启动顺序 |
| 共用调味料 | 数据卷共享 |
| 服务员传菜 | 网络互通 |
| 整桌宴席的配餐单 | docker-compose.yml |
安装方式:Docker Desktop 自带,Linux 需单独装
误区 :以为 Compose 总是随 Docker 一起安装。
真相:取决于安装方式。
| 场景 | 是否自带 Compose | 命令 |
|---|---|---|
| Windows/macOS + Docker Desktop | ✅ 自带,无需额外安装 | docker compose |
| Linux + Docker Engine | ❌ 需单独安装(推荐插件方式) | docker compose(新版)或 docker-compose(旧版) |
Linux 插件安装命令(Ubuntu):
bash
csharp
sudo apt-get update
sudo apt-get install docker-compose-plugin
docker compose version # 验证
注意:新版的 Compose 命令是
docker compose(中间无横杠),旧版是docker-compose。推荐使用新版。
Compose YAML 核心参数全解
一个标准的 docker-compose.yml 文件包含以下核心部分:
yaml
yaml
version: '3.8'
services:
# 服务定义
backend:
build: ...
image: ...
ports: ...
environment: ...
volumes: ...
depends_on: ...
restart: ...
healthcheck: ...
networks: ...
volumes:
# 数据卷定义
networks:
# 网络定义
关键参数速查表
| 参数 | 作用 | 关键注意 |
|---|---|---|
build |
现场构建镜像 | 需指定 context(构建上下文路径)和 dockerfile |
image |
直接拉取或指定镜像 | 可与 build 共存,为构建产物打标签 |
ports |
端口映射 "宿主机:容器" |
生产环境建议去掉或改成 "80:3000" |
volumes |
挂载数据卷或目录 | 开发用 bind mount,生产用 named volume |
environment |
设置环境变量 | 敏感信息用 .env 引用,不要硬编码 |
depends_on |
声明启动顺序依赖 | 仅保证顺序,不保证服务就绪;需配合 healthcheck |
restart |
重启策略 | 生产常用 unless-stopped |
healthcheck |
健康检查 | 让 Compose 知道容器"真的"可用了 |
container_name |
固定容器名 | ⚠️ 固定后无法扩展(scale),慎用 |
networks |
指定容器加入的网络 | 默认会创建并加入 default 网络,服务名可互相访问 |
区分 image 和 build
yaml
yaml
# 方式一:直接拉取现成镜像
mysql:
image: mysql:8.0
# 方式二:现场构建
backend:
build:
context: ./backend
dockerfile: Dockerfile
args:
NODE_ENV: production
# 方式三:两者结合(构建后打标签)
backend:
build: ./backend
image: my-nest-app:latest
depends_on + healthcheck:真正的就绪检查
默认的 depends_on 只保证容器进程启动了,不代表服务已经准备好接受请求(比如 MySQL 需要几秒初始化)。正确的做法是配合 healthcheck:
yaml
yaml
mysql:
image: mysql:8.0
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
backend:
depends_on:
mysql:
condition: service_healthy
实战案例:Milvus 向量数据库的 Compose 编排
项目中提供的 milvus-standalone-docker-compose.yml 是一个真实的生产级编排文件,展示了多服务协同的完整写法。
文件解读
yaml
bash
version: '3.5'
services:
etcd:
container_name: milvus-etcd
image: quay.io/coreos/etcd:v3.5.25
environment:
- ETCD_AUTO_COMPACTION_MODE=revision
- ETCD_AUTO_COMPACTION_RETENTION=1000
- ETCD_QUOTA_BACKEND_BYTES=4294967296
- ETCD_SNAPSHOT_COUNT=50000
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd
command: etcd -advertise-client-urls=http://etcd:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd
healthcheck:
test: ["CMD", "etcdctl", "endpoint", "health"]
interval: 30s
timeout: 20s
retries: 3
minio:
container_name: milvus-minio
image: minio/minio:RELEASE.2024-05-28T17-19-04Z
environment:
MINIO_ACCESS_KEY: minioadmin
MINIO_SECRET_KEY: minioadmin
ports:
- "9001:9001"
- "9000:9000"
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data
command: minio server /minio_data --console-address ":9001"
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 30s
timeout: 20s
retries: 3
standalone:
container_name: milvus-standalone
image: milvusdb/milvus:v3.0.0
command: ["milvus", "run", "standalone"]
security_opt:
- seccomp:unconfined
environment:
MINIO_REGION: us-east-1
ETCD_ENDPOINTS: etcd:2379
MINIO_ADDRESS: minio:9000
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus:/var/lib/milvus
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9091/healthz"]
interval: 30s
start_period: 90s
timeout: 20s
retries: 3
ports:
- "19530:19530"
- "9091:9091"
depends_on:
- "etcd"
- "minio"
networks:
default:
name: milvus
架构分析
这个 Compose 编排了三个服务:
| 服务 | 角色 | 说明 |
|---|---|---|
| etcd | 元数据存储 | 分布式键值存储,Milvus 用它保存元数据 |
| minio | 对象存储 | S3 兼容存储,Milvus 用它保存向量数据文件 |
| standalone | Milvus 主服务 | 向量数据库服务,依赖 etcd 和 minio |
关键设计亮点:
- 数据持久化 :三个服务都挂载了 named volumes(通过
${DOCKER_VOLUME_DIRECTORY:-.}/volumes/*),确保容器重启数据不丢失 - 健康检查 :每个服务都配置了
healthcheck,standalone还设置了start_period: 90s(给 Milvus 启动留出足够时间) - 网络隔离 :所有服务加入
milvus网络,通过服务名互相访问(如ETCD_ENDPOINTS: etcd:2379) - 依赖管理 :
standalone的depends_on显式声明依赖 etcd 和 minio
启动命令:
bash
bash
docker compose -f ./milvus-standalone-docker-compose.yml up -d
启动后,Milvus 服务暴露在宿主机的 19530 端口,可供应用连接。在 index.mjs 中,连接地址就是 192.168.2.147:19530。
多环境管理:基准文件 + 环境覆盖
误区 :为开发、测试、生产维护三份完全独立的 docker-compose.yml。
纠正:用"基准文件 + 覆盖文件"的方式,一份通用配置 + 每环境专属差异配置。
标准做法
bash
bash
# 开发环境(自动加载 docker-compose.override.yml,无需 -f)
docker compose up -d
# 生产环境(显式指定叠加文件)
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
文件分工
| 文件 | 作用 | 是否提交 Git |
|---|---|---|
docker-compose.yml |
基准文件,存放所有环境通用的配置 | ✅ 提交 |
docker-compose.override.yml |
开发环境覆盖(挂载本地代码、开启调试) | ❌ 不提交 |
docker-compose.prod.yml |
生产环境覆盖(资源限制、关闭调试工具) | ✅ 提交 |
备选方案:Profiles
在同一份文件中用 profiles 属性给服务打标签,启动时按需启用:
yaml
yaml
services:
debug-tool:
profiles: ["debug"]
image: my-debug-image
bash
css
docker compose --profile debug up -d
书写规范与最佳实践
- 缩进 :YAML 强制使用空格(2 个空格是社区惯例),禁止使用 Tab。
- 敏感信息 :永远用
${VAR}引用.env文件中的变量,且.env必须加入.gitignore。 - 容器名 :除非有特殊原因,否则不写
container_name,让 Compose 自动生成,以便后期扩展(如docker compose scale)。 - 依赖检查 :
depends_on默认只等容器启动,不等服务就绪。正确做法:为数据库配置healthcheck,然后用condition: service_healthy。 - 区分挂载方式 :开发用
./code:/app(bind mount),生产用data-volume:/app/data(named volume)。 - 端口映射:开发环境可暴露端口,生产环境建议只暴露网关(如 Nginx)的端口,内部服务通过网络互相访问。
互动讨论
💬 Compose 是把多个镜像合并到一个容器里吗?
不是。一个容器只能运行一个镜像的实例。Compose 的真正作用是管理一组容器,让它们共享网络、数据卷,协同工作。
💬 docker compose up 和 docker run 有什么区别?
docker run 启动单个容器,docker compose up 根据 YAML 文件启动一组容器,并自动创建网络、挂载数据卷、处理依赖顺序。Compose 是 docker run 的"批量自动化版本"。
💬 depends_on 能保证服务完全就绪吗?
不能。depends_on 只保证容器进程启动了,不代表服务已经准备好接受请求。要确保服务就绪,必须配合 healthcheck,使用 condition: service_healthy。
💬 开发环境和生产环境怎么区分配置?
使用"基准文件 + 覆盖文件"的方式。docker-compose.yml 存放通用配置,docker-compose.override.yml 存放开发环境覆盖(自动加载),docker-compose.prod.yml 存放生产环境覆盖(手动指定 -f 叠加)。
💬 Compose 文件中的 container_name 该不该写?
除非有特殊原因(比如需要固定名称供其他工具引用),否则不建议写 。固定名称后无法使用 docker compose scale 水平扩展,限制了部署灵活性。