08. Docker Compose 一键编排:多容器应用的指挥家
前面几篇学下来,你已经会手动
docker run各种容器了。但真实项目一复杂,你就会发现:启动一个 Web 应用要敲五六条docker run,端口映射不能冲突、网络要手动建、环境变量要一个个传、启动顺序还得自己盯......每次部署都像在做"记忆力考试"。Docker Compose 就是来解决这件事的------一个 YAML 文件描述所有服务,一条命令全部拉起来。

▲ Compose 就像交响乐团的指挥:一份总谱(docker-compose.yml),一挥棒所有容器同时就位。
一、Docker Compose 到底是什么?
先说结论:Docker Compose 是一个多容器编排工具,用 YAML 文件定义你的所有服务(容器),然后一条命令把它们全部启动、停止、重建。
打个比方:
| 没有 Compose | 有 Compose |
|---|---|
| 像交响乐团的每个乐手各看各的谱,指挥靠喊 | 所有乐手看同一份总谱,指挥一挥棒全部就位 |
手动 docker run × N 条命令 |
一个 docker-compose.yml + 一条 docker compose up |
| 参数全靠记忆和笔记 | 参数全部写在文件里,版本可追踪 |
| 换台机器得重新抄一遍 | 把 YAML 文件拷过去,直接 up |
一句话:Compose 把你的"启动脚本"变成了"声明式配置",可复用、可分享、可版本控制。
二、先搞清楚版本:v1 vs v2
这是很多新手踩的第一个坑,Docker Compose 有两个版本:
| 对比项 | v1(旧版) | v2(当前推荐) |
|---|---|---|
| 命令 | docker-compose(带横杠) |
docker compose(空格,子命令) |
| 安装方式 | 独立二进制,需单独下载安装 | Docker 内置插件,装 Docker Desktop/Engine 就自带 |
| 开发语言 | Python | Go(与 Docker 引擎同语言,性能更好) |
| 状态 | 已停止维护(2023 年 7 月 EOL) | 活跃开发中 |
结论:现在写文章、做项目,一律用
docker compose(v2)。 如果你在网上看到老教程用docker-compose,把中间的横杠换成空格就行,YAML 写法几乎一样。
确认一下你的环境有没有 Compose v2:
docker compose version
正常输出类似:
Docker Compose version v2.29.2
如果提示找不到命令,说明 Docker 版本太旧,升级 Docker 即可。
三、docker-compose.yml 文件结构详解
Compose 的核心就是这个 YAML 文件。先看一个最简骨架:
# docker-compose.yml
services: # 定义所有服务(容器)
web: # 服务名(自定义)
image: nginx:latest # 用什么镜像
ports:
- "80:80" # 端口映射
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: mypass123
networks: # 自定义网络(可选)
mynet:
volumes: # 持久化卷(可选)
db_data:
四大顶层字段,各司其职:
| 字段 | 作用 | 是否必须 |
|---|---|---|
| services | 定义每个服务(容器)的配置 | ✅ 必须有 |
| networks | 声明自定义网络,让服务间互相通信 | 按需 |
| volumes | 声明持久化数据卷 | 按需 |
| name | 给这个项目起个名字(可选) | 可选 |
老教程里你可能见过
version: "3.8"这个字段------在 Compose v2 中已经不再需要了,写上也不报错,但属于多余代码,直接删掉就好。

▲ 服务、网络、数据卷三块配置拼在一起,就是一套完整的多容器应用。
四、常用命令速查
在 docker-compose.yml 所在目录下执行:
# 后台启动所有服务
docker compose up -d
# 查看所有服务状态
docker compose ps
# 查看某个服务的日志(实时跟踪)
docker compose logs -f web
# 进入某个服务的容器
docker compose exec web bash
# 重新构建镜像(修改了 Dockerfile 后)
docker compose build
# 重启某个服务
docker compose restart db
# 停止所有服务(保留容器和数据)
docker compose stop
# 恢复被 stop 的服务
docker compose start
# 停止并删除容器、网络(数据卷默认保留)
docker compose down
# 停止并删除容器、网络、数据卷(⚠️ 数据全清)
docker compose down -v
命令对照表:
| 命令 | 干什么 | 常用场景 |
|---|---|---|
up -d |
后台启动 | 日常启动项目 |
down |
停止并清理 | 收工或重新部署 |
down -v |
停止并清理一切 | 彻底重置环境 |
ps |
查看状态 | 看哪个服务挂了 |
logs -f |
跟踪日志 | 调试排错 |
exec |
进入容器 | 临时调试 |
build |
重新构建 | 改了代码/Dockerfile |
restart |
重启单个服务 | 改了配置后快速生效 |
五、实战:一键启动完整 Web 应用
光说概念没意思,直接上手。我们要编排这样一个应用:
用户浏览器 → Nginx(反向代理:80)→ Flask API(:5000)→ MySQL(:3306)
→ Redis(:6379)
四个服务,分工明确:
| 服务 | 角色 | 镜像来源 |
|---|---|---|
| nginx | 反向代理 + 静态文件 | 官方 nginx 镜像 |
| web | Flask API 后端 | 本地 Dockerfile 构建 |
| db | MySQL 数据库 | 官方 mysql 镜像 |
| redis | 缓存 / 会话存储 | 官方 redis 镜像 |
5.1 项目目录结构
myapp/
├── docker-compose.yml
├── .env # 环境变量文件
├── web/
│ ├── Dockerfile
│ ├── app.py # Flask 应用
│ └── requirements.txt
└── nginx/
└── nginx.conf # Nginx 配置
5.2 Flask 应用代码
# web/app.py
from flask import Flask, jsonify
import mysql.connector
import redis
import os
app = Flask(__name__)
# 从环境变量读取配置
DB_HOST = os.getenv("DB_HOST", "db")
DB_PASSWORD = os.getenv("MYSQL_PASSWORD", "secret")
REDIS_HOST = os.getenv("REDIS_HOST", "redis")
r = redis.Redis(host=REDIS_HOST, port=6379, decode_responses=True)
@app.route("/")
def index():
# 用 Redis 做访问计数
visits = r.incr("visit_count")
return jsonify({"message": "Hello Docker Compose!", "visits": visits})
@app.route("/health")
def health():
try:
conn = mysql.connector.connect(
host=DB_HOST, user="appuser", password=DB_PASSWORD, database="myapp"
)
conn.close()
db_status = "connected"
except Exception as e:
db_status = f"error: {e}"
return jsonify({"db": db_status, "redis": r.ping()})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
# web/requirements.txt
flask==3.0.0
mysql-connector-python==8.3.0
redis==5.0.1
# web/Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
COPY . .
CMD ["python", "app.py"]
5.3 Nginx 反向代理配置
# nginx/nginx.conf
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://web:5000; # 用服务名 "web" 直接访问 Flask 容器
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
注意
proxy_pass http://web:5000里的web就是 docker-compose.yml 里定义的服务名。Compose 会自动创建一个网络,同一网络下的容器可以直接用服务名当域名互相访问,不需要写 IP 地址。
5.4 环境变量文件
# .env
MYSQL_ROOT_PASSWORD=super_secret_2024
MYSQL_PASSWORD=app_secret_2024
MYSQL_DATABASE=myapp
MYSQL_USER=appuser
把密码放在
.env文件里而不是直接写在 YAML 中 ,这是工程最佳实践。.env文件记得加进.gitignore,别把密码提交到 Git。
5.5 完整的 docker-compose.yml
# docker-compose.yml
services:
# ──────────── Nginx 反向代理 ────────────
nginx:
image: nginx:1.27-alpine
ports:
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
web:
condition: service_started
networks:
- appnet
# ──────────── Flask API ────────────
web:
build: ./web # 从 web/ 目录的 Dockerfile 构建
ports:
- "5000:5000"
environment:
- DB_HOST=db
- REDIS_HOST=redis
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
depends_on:
db:
condition: service_healthy # 等 MySQL 真正就绪再启动
redis:
condition: service_started
networks:
- appnet
restart: unless_stopped # 崩了自动重启
# ──────────── MySQL 数据库 ────────────
db:
image: mysql:8.0
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
volumes:
- db_data:/var/lib/mysql # 数据持久化
networks:
- appnet
healthcheck: # 健康检查:MySQL 真的能连接才算就绪
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
# ──────────── Redis 缓存 ────────────
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
networks:
- appnet
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
# ──────────── 网络 ────────────
networks:
appnet:
driver: bridge
# ──────────── 数据卷 ────────────
volumes:
db_data: # MySQL 数据持久化
redis_data: # Redis 数据持久化
5.6 一键启动
docker compose up -d --build
--build 参数会先构建 web 服务的镜像(因为你用了 build: 而不是 image:),然后启动所有服务。
输出类似:
[+] Building 3.2s (8/8) FINISHED
=> [web internal] load build definition from Dockerfile
=> [web internal] load .dockerignore
=> [web internal] load build context
=> [web 1/3] FROM docker.io/library/python:3.12-slim
=> [web 2/3] WORKDIR /app
=> [web 3/3] RUN pip install --no-cache-dir -r requirements.txt
=> [web] exporting to image
=> => naming to docker.io/library/myapp-web
[+] Running 5/5
✔ Network myapp_appnet Created
✔ Container myapp-redis Started
✔ Container myapp-db Started
✔ Container myapp-web Started
✔ Container myapp-nginx Started
5.7 验证一下
# 查看服务状态
docker compose ps
NAME SERVICE STATUS PORTS
myapp-db-1 db running 0.0.0.0:3306->3306/tcp
myapp-nginx-1 nginx running 0.0.0.0:80->80/tcp
myapp-redis-1 redis running 0.0.0.0:6379->6379/tcp
myapp-web-1 web running 0.0.0.0:5000->5000/tcp
# 通过 Nginx 访问
curl http://localhost
{"message": "Hello Docker Compose!", "visits": 1}
多访问几次,visits 会递增------说明 Flask → Redis 链路通了:
curl http://localhost
{"message": "Hello Docker Compose!", "visits": 2}
# 检查数据库连接
curl http://localhost/health
{"db": "connected", "redis": true}
完美!四个服务全部正常通信。
六、depends_on 的坑:启动了 ≠ 就绪了
很多人第一次写 Compose 时都会踩这个坑:
# 看似没问题,实际上 web 启动时 MySQL 可能还没初始化完
services:
web:
depends_on:
- db
depends_on 只保证容器启动了,不保证里面的服务就绪了。MySQL 容器启动后还要 10~30 秒才能真正接受连接。
解决方案:配合健康检查使用 condition。
services:
web:
depends_on:
db:
condition: service_healthy # 等 db 的健康检查通过
redis:
condition: service_started # 只等启动就行
db:
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s # 每 10 秒检查一次
timeout: 5s # 超时 5 秒算失败
retries: 5 # 连续失败 5 次判定为不健康
start_period: 30s # 启动后给 30 秒"宽限期"
condition 有三个值:
| condition | 含义 |
|---|---|
service_started |
容器启动就行(默认) |
service_healthy |
必须通过 healthcheck 才算就绪 |
service_completed_successfully |
必须正常退出(用于一次性初始化任务) |
经验法则:数据库、消息队列这类有初始化过程的服务,一定要加
healthcheck+service_healthy。 否则你的应用容器启动了也是白启动------连不上数据库直接报错退出。
七、环境变量:告别硬编码
在上面的例子中,我们用 ${MYSQL_PASSWORD} 这种语法引用了外部变量。Compose 查找变量的优先级是:
Shell 环境变量 > .env 文件 > docker-compose.yml 中 environment 里的默认值
你还可以在 YAML 里给变量设默认值:
environment:
- DB_HOST=${DB_HOST:-db} # 如果没设 DB_HOST,默认用 "db"
- FLASK_DEBUG=${DEBUG:-0} # 默认关闭调试模式
更实用的技巧------不同环境用不同的 .env 文件:
# 开发环境
docker compose --env-file .env.dev up -d
# 生产环境
docker compose --env-file .env.prod up -d
八、Profiles:按需启动服务
有些服务不是每次都要跑的,比如调试工具、监控面板。用 profiles 来控制:
services:
web:
build: ./web
# 没有 profiles,默认就启动
db:
image: mysql:8.0
# 没有 profiles,默认就启动
# 这个服务默认不启动,只有指定 profile 才跑
debug:
image: alpine
profiles: ["debug"]
command: sleep infinity
# 监控面板
grafana:
image: grafana/grafana
profiles: ["monitor"]
ports:
- "3000:3000"
使用方式:
# 只启动默认服务(web + db)
docker compose up -d
# 额外启动 debug 服务
docker compose --profile debug up -d
# 同时启动 debug + monitor
docker compose --profile debug --profile monitor up -d
这个功能在开发环境和生产环境共用同一份 Compose 文件时特别好用。
九、常用运维操作
9.1 只看某个服务的日志
docker compose logs -f --tail=50 web
9.2 进入容器调试
docker compose exec db mysql -u root -p
# 输入密码后直接进入 MySQL 命令行
9.3 水平扩展某个服务
# 把 web 服务扩到 3 个实例
docker compose up -d --scale web=3
注意:扩展多实例时不能写死
ports映射(端口会冲突),需要前面有 Nginx 之类的负载均衡器来分发。
9.4 彻底重置环境
# 停掉一切,删掉容器、网络、数据卷
docker compose down -v
⚠️ -v 会删除所有数据卷,数据库里的数据会全部丢失。生产环境慎用!
十、本文要点回顾
| 要点 | 核心知识 |
|---|---|
| Docker Compose 是什么 | 一个 YAML 文件定义所有服务,一条命令全部编排 |
| v1 vs v2 | 现在统一用 docker compose(v2),v1 已停止维护 |
| YAML 四大字段 | services(必须)、networks、volumes、name |
| 核心命令 | up -d 启动、down 停止、ps 查状态、logs -f 看日志 |
| depends_on 的坑 | 只管启动顺序,不管服务就绪;配合 healthcheck + service_healthy 使用 |
| 环境变量 | .env 文件 + ${VAR} 语法,密码别硬编码 |
| profiles | 按需启动服务,开发和生产共用一个文件 |
| 实战架构 | Nginx + Flask + MySQL + Redis 四容器完整编排 |
一句话总结:单容器用
docker run,多容器用docker compose。写一份 YAML,走到哪部署到哪,这才是"基础设施即代码"该有的样子。
下集预告
到现在为止,你已经掌握了 Docker 的日常使用技能。但 Docker 镜像是怎么一层一层构建出来的?为什么有的镜像 1GB,有的才 30MB?下一篇我们聊 Docker 多阶段构建------教你把镜像从"臃肿胖子"瘦身成"精干特种兵",在生产环境中又快又省空间。