
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 就自带开发语言PythonGo与 Docker 引擎同语言性能更好状态已停止维护2023 年 7 月 EOL活跃开发中结论现在写文章、做项目一律用docker composev2。如果你在网上看到老教程用docker-compose把中间的横杠换成空格就行YAML 写法几乎一样。确认一下你的环境有没有 Compose v2docker 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重新构建改了代码/Dockerfilerestart重启单个服务改了配置后快速生效五、实战一键启动完整 Web 应用光说概念没意思直接上手。我们要编排这样一个应用用户浏览器 → Nginx反向代理:80→ Flask API:5000→ MySQL:3306 → Redis:6379四个服务分工明确服务角色镜像来源nginx反向代理 静态文件官方 nginx 镜像webFlask API 后端本地 Dockerfile 构建dbMySQL 数据库官方 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(hostREDIS_HOST, port6379, decode_responsesTrue) 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( hostDB_HOST, userappuser, passwordDB_PASSWORD, databasemyapp ) conn.close() db_status connected except Exception as e: db_status ferror: {e} return jsonify({db: db_status, redis: r.ping()}) if __name__ __main__: app.run(host0.0.0.0, port5000)# web/requirements.txt flask3.0.0 mysql-connector-python8.3.0 redis5.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_PASSWORDsuper_secret_2024 MYSQL_PASSWORDapp_secret_2024 MYSQL_DATABASEmyapp MYSQL_USERappuser把密码放在.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_HOSTdb - REDIS_HOSTredis - 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 Started5.7 验证一下# 查看服务状态 docker compose psNAME 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: - dbdepends_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必须正常退出用于一次性初始化任务经验法则数据库、消息队列这类有初始化过程的服务一定要加healthcheckservice_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 --tail50 web9.2 进入容器调试docker compose exec db mysql -u root -p # 输入密码后直接进入 MySQL 命令行9.3 水平扩展某个服务# 把 web 服务扩到 3 个实例 docker compose up -d --scale web3注意扩展多实例时不能写死ports映射端口会冲突需要前面有 Nginx 之类的负载均衡器来分发。9.4 彻底重置环境# 停掉一切删掉容器、网络、数据卷 docker compose down -v⚠️-v会删除所有数据卷数据库里的数据会全部丢失。生产环境慎用十、本文要点回顾要点核心知识Docker Compose 是什么一个 YAML 文件定义所有服务一条命令全部编排v1 vs v2现在统一用docker composev2v1 已停止维护YAML 四大字段services必须、networks、volumes、name核心命令up -d启动、down停止、ps查状态、logs -f看日志depends_on 的坑只管启动顺序不管服务就绪配合healthcheckservice_healthy使用环境变量.env文件 ${VAR}语法密码别硬编码profiles按需启动服务开发和生产共用一个文件实战架构Nginx Flask MySQL Redis 四容器完整编排一句话总结单容器用docker run多容器用docker compose。写一份 YAML走到哪部署到哪这才是基础设施即代码该有的样子。下集预告到现在为止你已经掌握了 Docker 的日常使用技能。但 Docker 镜像是怎么一层一层构建出来的为什么有的镜像 1GB有的才 30MB下一篇我们聊Docker 多阶段构建——教你把镜像从臃肿胖子瘦身成精干特种兵在生产环境中又快又省空间。