08. Docker Compose 一键编排:多容器应用的指挥家

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 多阶段构建------教你把镜像从"臃肿胖子"瘦身成"精干特种兵",在生产环境中又快又省空间。

相关推荐
珍珠先生16 分钟前
第14章 设计模式入门:单例、工厂、建造者与代理
java
坐吃山猪22 分钟前
JDK 8 到 JDK 21 新特性知识要点
java·开发语言·windows
坐吃山猪24 分钟前
【多线程】FutureTask多线程底层实现
java·开发语言·数据库
weixin1997010801628 分钟前
[特殊字符]《从0到1:闲鱼开放平台授权登录 + AccessToken 刷新 + 聚石塔部署完整链路》(附Python源码)
java·数据库·python
Freak嵌入式1 小时前
RP2040 PIO 编程模型与状态机原理:从硬件架构到工作逻辑全解析
java·大数据·开发语言·单片机·嵌入式硬件·硬件架构
kakawzw1 小时前
Netty源码笔记
java·服务器·后端
zhuodedao1 小时前
Spring AI + MCP 文件工具未调用问题复盘:为什么初始化成功却没有写入文件?
java·debug·agent·springai·mcp
Java小白笔记1 小时前
Java中大数据实时归集与指标汇总方案
java·大数据·开发语言
随遇而安zx1 小时前
【地基篇】---Java 8 JVM 知识大纲
java·开发语言·jvm