📚前言
📒FDE系列内容总纲:
【大纲】FDE 前沿部署工程师学习系列教程-CSDN博客
🚄前置课程列表:
见文档结尾附录。
🚀阶段2·Day 43:Docker Compose — 多容器一键编排
FDE 学习系列教程 · 第二阶段 · 第 5 周 · Day 3预计时长:3 小时 | 难度:★★★☆☆ | 前置知识:Day 41-42 容器操作与 Dockerfile
📌一句话目标:看懂并编写 docker-compose.yml,用 Docker Compose 一键编排 FastAPI + MySQL 多容器应用,掌握服务依赖、环境变量、数据卷和常用管理命令。
🧑🤝🧑 开场:一个容器好养,一套容器难带
昨天你能把自己的 FastAPI 打成镜像了。但真实项目从来不是"一个容器包打天下"。回想你的工单系统:
工单系统需要: ┌──────────┐ 连接 ┌──────────┐ │ FastAPI │ ──────► │ MySQL │ │ 容器 │ │ 容器 │ └──────────┘ └──────────┘ 明天还要加:Nginx 容器(反向代理)、Redis 容器(缓存)……如果用昨天的docker run手搓,你得:
# ① 先建个网络让容器互通 docker network create fde-net # ② 启动 MySQL(记一堆 -e、-v、--network 参数) docker run -d --name db --network fde-net \ -e MYSQL_ROOT_PASSWORD=root123 -e MYSQL_DATABASE=fde_db \ -v db_data:/var/lib/mysql mysql:8.0 # ③ 等 MySQL 就绪(多久?只能靠猜) # ④ 启动 API(又是一堆参数,还得保证数据库先起来) docker run -d --name api --network fde-net -p 8000:8000 \ -e DB_HOST=db ... fde-api:1.0三个容器就要敲四五个长命令,换台机器全部重来一遍,顺序错了还连不上。Docker Compose就是来终结这种手工活的:把所有容器的配置写进一个 YAML 文件,一条命令让整套环境按正确的姿势全部就位。
手工 docker run: Docker Compose: 一条条命令,参数靠脑子记 一个 docker-compose.yml 描述全部 顺序、网络、卷手动管 up 一键启动 / down 一键销毁 换机器要重新敲一遍 文件拷过去,up 一下原样复现📖 一、Compose 是什么 & 版本说明
Docker Compose 是 Docker 官方的多容器编排工具。你用一个 YAML 文件声明"系统由哪些服务组成、每个服务用什么镜像、开什么端口、挂什么卷、彼此什么关系",Compose 负责把它们全部创建好、连进同一个网络。
┌────────────────────────────────────────────────────────┐ │ docker-compose.yml (一份声明,描述整套系统) │ │ │ │ services: │ │ api: ← 服务1(FastAPI 容器) │ │ db: ← 服务2(MySQL 容器) │ │ nginx: ← 服务3(Nginx 容器,明天加) │ │ │ │ docker compose up -d → 三个容器 + 网络 + 数据卷 │ │ 一次性全部创建 │ └────────────────────────────────────────────────────────┘💡命令的新旧写法:新版 Docker(带 Compose V2 插件)用
docker compose(中间空格),老版本 V1 用docker-compose(中间横杠,独立程序)。你昨天验证过docker compose version有输出,所以本教程统一用新写法docker compose。两种写法参数基本一致,看到老教程的docker-compose别慌,等价。
YAML 语法速览(30 秒入门)
Compose 文件是 YAML 格式,核心就两条规矩:
靠缩进表示层级(每层通常 2 个空格,不能用 Tab)
键值对用冒号加空格:
key: value(冒号后那个空格不能少)
services: # 顶层 api: # 缩进 2 格:services 的子项 image: nginx # 再缩进 2 格:api 的配置 ports: # 列表用 - 开头 - "80:80"🖥️ 二、最小例子:用 Compose 跑一个 Nginx
先跑个最小的,建立"yml → 容器"的直观感受。
mkdir -p ~/docker_lab/compose_demo && cd ~/docker_lab/compose_demo新建docker-compose.yml:
services: web: image: nginx:alpine container_name: demo-nginx ports: - "8080:80" restart: unless-stopped逐行对应到你昨天学的docker run参数:
docker run 写法 | compose 写法 |
|---|---|
|
|
|
|
|
|
|
|
启动:
docker compose up -d # [+] Running 2/2 # ✔ Network compose_demo_default Created # ✔ Container demo-nginx Started注意它自动建了一个网络(compose_demo_default)——这就是 Compose 的省心之处。浏览器访问 http://localhost:8080 看到欢迎页。
常用管理命令先混个眼熟:
docker compose ps # 看这套服务的容器状态 docker compose logs -f # 看所有服务日志(-f 实时) docker compose stop # 停止(容器保留) docker compose start # 再启动 docker compose down # 停止并删除容器+网络(数据卷默认保留) docker compose up -d # 改了配置后重新生效先docker compose down清掉,进入正题。
🖥️ 三、实战:FastAPI + MySQL 两容器编排
现在把昨天的 fde-api 和一个 MySQL 数据库编排起来。注意:容器之间通过服务名互相访问,这是 Compose 最巧妙的地方,先看项目结构:
~/docker_lab/fde_stack/ ├── main.py ├── requirements.txt ├── Dockerfile ← 昨天写的(构建 api 镜像用) ├── .dockerignore ├── init.sql ← 数据库初始化脚本(待会写) └── docker-compose.yml ← 今天的主角第 1 步:main.py(连上数据库的版本)
from fastapi import FastAPI import os import mysql.connector import uvicorn app = FastAPI(title="FDE API + MySQL", version="1.0") def get_db(): """从环境变量读取数据库连接信息(配置不写死)""" return mysql.connector.connect( host=os.getenv("DB_HOST", "db"), port=int(os.getenv("DB_PORT", "3306")), user=os.getenv("DB_USER", "root"), password=os.getenv("DB_PASSWORD", "root123"), database=os.getenv("DB_NAME", "fde_db"), ) @app.get("/") def root(): return {"msg": "FDE Stack", "services": ["api", "mysql"]} @app.get("/health") def health(): """健康检查:API 活着 + 数据库连得上""" try: conn = get_db() conn.ping() conn.close() return {"status": "ok", "db": "connected"} except Exception as e: return {"status": "degraded", "db": str(e)} @app.get("/devices") def list_devices(): conn = get_db() cursor = conn.cursor(dictionary=True) cursor.execute("SELECT id, name, type, temperature FROM devices LIMIT 10") rows = cursor.fetchall() conn.close() return {"devices": rows, "count": len(rows)} if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000)requirements.txt:
fastapi==0.115.0 uvicorn==0.30.0 mysql-connector-python==9.1.0Dockerfile(昨天的,直接用):
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 . . EXPOSE 8000 CMD ["python", "main.py"]第 2 步:init.sql(数据库建表 + 初始数据)
MySQL 官方镜像有个贴心机制:容器第一次启动时,会自动执行/docker-entrypoint-initdb.d/目录下的.sql文件。我们把建表语句放进去,数据库一就绪就自动初始化。
-- init.sql:首次启动自动执行 CREATE TABLE IF NOT EXISTS devices ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, type VARCHAR(20) NOT NULL, temperature DECIMAL(5,1) DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); INSERT INTO devices (name, type, temperature) VALUES ('注塑机A1', '注塑', 65.0), ('注塑机A2', '注塑', 82.0), ('注塑机A3', '注塑', 91.0), ('冲压机B1', '冲压', 75.0), ('冲压机B2', '冲压', 88.0);第 3 步:docker-compose.yml(核心)
# ========================================== # FDE 技术栈:FastAPI + MySQL # ========================================== services: # ---------- 服务 1:API ---------- api: build: . # 不用现成镜像,用当前目录 Dockerfile 现场构建 container_name: fde-api ports: - "8000:8000" environment: # 注入容器的环境变量 DB_HOST: db # ⭐ 主机名直接写服务名 db! DB_PORT: 3306 DB_USER: root DB_PASSWORD: root123 DB_NAME: fde_db depends_on: # 依赖:先启动 db,再启动 api - db restart: unless-stopped # 容器挂了/机器重启后自动拉起 # ---------- 服务 2:MySQL 数据库 ---------- db: image: mysql:8.0 # 直接用官方镜像,不用自己 build container_name: fde-mysql environment: MYSQL_ROOT_PASSWORD: root123 # root 密码(必填) MYSQL_DATABASE: fde_db # 启动时自动创建这个数据库 ports: - "3306:3306" volumes: # 命名卷:数据库数据持久化(容器删了数据不丢) - db_data:/var/lib/mysql # 绑定挂载:把初始化脚本放进镜像规定的目录 - ./init.sql:/docker-entrypoint-initdb.d/init.sql restart: unless-stopped # 顶层声明用到的命名卷(Compose 会自动创建) volumes: db_data:第 4 步:一键启动
docker compose up -d --build # --build:启动前先根据 Dockerfile 构建 api 镜像 # 首次会拉 mysql:8.0 镜像(几百MB,耐心等)观察启动过程:
[+] Running 4/4 ✔ Network fde_stack_default Created ✔ Volume "fde_stack_db_data" Created ✔ Container fde-mysql Started ✔ Container fde-api Started看状态:
docker compose ps # NAME STATUS PORTS # fde-api Up 5 seconds 0.0.0.0:8000->8000/tcp # fde-mysql Up 8 seconds 0.0.0.0:3306->3306/tcp⚠️ 第一次启动 MySQL 需要 10~30 秒初始化。API 可能比数据库先就绪而连接失败,下一节专门讲怎么处理。先等半分钟再验证。
第 5 步:验证整条链路
# ① API 自己活着 curl http://localhost:8000/ # {"msg":"FDE Stack","services":["api","mysql"]} # ② API 能连上数据库 curl http://localhost:8000/health # {"status":"ok","db":"connected"} # ③ 数据库里的初始数据能通过 API 查出来 curl http://localhost:8000/devices # {"devices":[{"id":1,"name":"注塑机A1",...},...],"count":5}通了!一条docker compose up,应用和数据库两个容器全部就位、自动建库建表、网络互通。这就是编排的威力。
📖 四、核心机制:服务名就是主机名
上面配置里最神奇的一行是DB_HOST: db——为什么主机名写db就行?IP 都不用知道?
┌────────────────────────────────────────────────────────┐ │ Compose 自动创建一个专用网络,把所有服务接进去 │ │ 并提供一个内置 DNS:服务名 ↔ 容器 IP 自动解析 │ │ │ │ 容器 api 想连数据库: │ │ 代码写 host="db" │ │ │ │ │ ▼ │ │ 内置 DNS:db 是哪个容器?→ fde-mysql 的内网 IP │ │ │ │ │ ▼ │ │ api ───────────────────────────────► fde-mysql:3306 │ │ │ │ 服务名 db 来自 compose 文件里 db: 这个 key │ └────────────────────────────────────────────────────────┘📌关键认知:同一 Compose 网络里的容器,互相访问时主机名直接写服务名(
db、api),端口写容器内端口(3306,不是映射到宿主机的那个)。容器 IP 会变,但服务名永不过期——这比写死 IP 靠谱一万倍。注意区分:你在Windows 浏览器访问 API 用
localhost:8000(走端口映射);容器之间互通用服务名:容器端口(走内部网络,不需要 ports 映射)。
📖 五、depends_on 的坑:启动了 ≠ 就绪了
你可能发现了:刚up的头几秒,/health可能报数据库连不上。原因在于depends_on的真实语义:
depends_on: - db 它只保证【启动顺序】:db 容器先启动,api 容器后启动 它【不保证】db 已经可以接受连接! 时间线: 0s db 容器启动(mysqld 进程开始初始化,要 20 秒) 1s api 容器启动,立刻去连 db:3306 → 拒绝连接 ❌ 20s MySQL 才真正就绪三种应对方法,从简到优:
方法 1:API 加重试(最实用,推荐)
让应用连接数据库时带重试逻辑——连不上等几秒再试。生产应用本来就该有这个容错。可以用restart: unless-stopped让 api 容器崩溃后自动重启,直到数据库就绪。
方法 2:Compose 的健康检查条件(V2 支持)
给 db 配 healthcheck,让 api 等数据库"真正健康"再启动:
db: image: mysql:8.0 # ... 其他配置 healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-proot123"] interval: 5s timeout: 5s retries: 10 api: # ... 其他配置 depends_on: db: condition: service_healthy # 等 db 健康检查通过才启动方法 3:自己 sleep(土办法,不推荐)
api: command: sh -c "sleep 20 && python main.py"💡 FDE 实操建议:学习阶段用方法 1(重启策略 + 应用重试)最简单;正式交付推荐方法 2(healthcheck + condition),语义最准确。
📖 六、docker-compose.yml 配置项速查
配置项 | 作用 | 对应 docker run |
|---|---|---|
| 用现成镜像 | 直接写镜像名 |
| 从 Dockerfile 构建 | 先 build 再 run |
| 容器名 |
|
| 宿主机:容器端口映射 |
|
| 数据卷/绑定挂载 |
|
| 环境变量(键值对) |
|
| 从文件批量读环境变量 |
|
| 启动顺序/就绪依赖 | 无(手工控制) |
| 重启策略 |
|
| 自定义网络 |
|
| 覆盖镜像默认启动命令 | 镜像名后的命令 |
| 工作目录 |
|
环境变量的两种安全写法
写法 A:直接写在 yml 里(学习用,简单直观,就是你现在用的)
environment: DB_PASSWORD: root123写法 B:敏感信息用 .env 文件,别提交到 Git(生产推荐)
项目根目录建.env:
DB_PASSWORD=root123 MYSQL_ROOT_PASSWORD=root123compose 文件里用${变量名}引用:
db: environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}.env加进.gitignore和.dockerignore,密码就不会进版本库。再提供一个.env.example(不含真实密码的模板)给同事参考。
📌12-Factor 原则:配置(尤其是密码、地址等随环境变化的东西)通过环境变量注入,而不是写死在代码里。这样同一份镜像,开发/测试/生产环境靠不同环境变量切换,镜像本身一个字都不用改。
🖥️ 七、Compose 常用命令全掌握
# ---------- 生命周期 ---------- docker compose up -d # 构建+后台启动整套服务 docker compose up -d --build # 强制重新构建镜像后启动(代码更新后用) docker compose down # 停止并删除容器+网络(卷保留,数据还在) docker compose down -v # ⚠️ 连数据卷一起删(数据库数据清空!) docker compose stop # 停止(容器保留,可 start) docker compose start # 启动已停止的 docker compose restart # 重启所有服务 docker compose restart api # 只重启 api 一个服务 # ---------- 查看 ---------- docker compose ps # 服务状态 docker compose logs -f # 所有服务实时日志 docker compose logs -f api # 只看 api docker compose logs --tail=20 db # db 最后 20 行 docker compose top # 各容器里跑的进程 # ---------- 执行 ---------- docker compose exec api bash # 进入 api 容器 docker compose exec db mysql -uroot -proot123 fde_db # 直接进 MySQL 命令行 docker compose build # 只构建镜像,不启动数据持久化验证(重要实验)
# ① 通过 API 插入一条数据(用 mysql 客户端或 exec 进去插) docker compose exec db mysql -uroot -proot123 fde_db \ -e "INSERT INTO devices (name,type,temperature) VALUES ('测试机X','测试',50);" # ② down 销毁容器(注意:不加 -v) docker compose down # ③ 重新 up docker compose up -d # 等数据库就绪后查询 curl http://localhost:8000/devices # 测试机X 还在!数据活在 db_data 卷里,与容器生死无关 ✅这个实验一定要亲手做——它会让你彻底理解"容器是临时的,数据是持久的"。
📝 本课小结
知识点 | 一句话记住 |
|---|---|
Compose 作用 | 一个 yml 编排多个容器,up 一键起 down 一键灭 |
services | 每个服务对应一个容器(api/db/...) |
image vs build | 现成镜像用 image,自己的应用用 build: . |
ports | 宿主机:容器 端口映射 |
volumes | 命名卷持久化数据;绑定挂载挂配置/脚本 |
environment | 注入环境变量,配置与代码分离 |
depends_on | 只管启动顺序,不管是否就绪 |
服务名即主机名 | 容器互访写服务名:容器端口 |
自动网络 | Compose 自动建网络和 DNS |
数据初始化 | MySQL 自动执行 initdb.d 下的 .sql |
healthcheck | 配合 condition: service_healthy 等就绪 |
down vs down -v | 前者保留数据,后者清空数据卷 |
更新部署 | 改代码 → up -d --build 重建变动的服务 |
🧠 核心认知:Compose 把"一套系统"作为管理单元。docker-compose.yml就是你这套环境的唯一事实来源——它把"用哪些镜像、怎么连、数据放哪、什么顺序"全部声明清楚,纳入 Git 管理后,团队任何人、任何机器,一条up就能得到一模一样的环境。这就是"基础设施即代码"思想的入门。
📋 课后练习
练习 1:独立搭起双容器栈(约 40 分钟)
不看教程,新建目录完成 FastAPI + MySQL 的 Compose 编排:
准备 5 个文件(main.py / requirements.txt / Dockerfile / init.sql / docker-compose.yml)
docker compose up -d --build启动用
/health和/devices验证链路(数据库没就绪就稍等或重启 api)docker compose exec db mysql -uroot -p...进数据库手动插一条设备,再通过 API 查到它
练习 2:持久化实验(约 15 分钟)
插入一条新设备,
docker compose down(不加 -v),再up确认数据还在
docker volume ls找到这个项目的数据卷,说出它的命名规律(通常是目录名_卷名)思考:什么情况下才该用
down -v?
练习 3:加一个 phpMyAdmin 服务(约 25 分钟)
在 compose 文件里加第三个服务,用浏览器图形化管理 MySQL:
pma: image: phpmyadmin:latest container_name: fde-pma ports: - "8080:80" environment: PMA_HOST: db # 指向数据库的服务名 PMA_PORT: 3306 depends_on: - db restart: unless-stopped启动后浏览器打开 http://localhost:8080,用 root / root123 登录,图形界面查看 fde_db 里的 devices 表。
🔭 下节预告
现在你的整套服务在 8000、3306、8080 各种端口上跑。明天补两大块拼图:
Nginx 反向代理:让用户访问标准 80 端口就能打到你的 API,顺便学会负载均衡的原理
Git 版本控制:你的代码、Dockerfile、compose 文件都需要版本管理——提交历史、分支、回滚、推送远程仓库,告别"文件夹复制改名"式备份
学完明天,"部署"和"代码管理"两件事的工具链就齐了。明天见!
🌍附录:前置课程列表
阶段一:认知启蒙(AI 认知与 FDE 角色)
AI 认知
【FDE系列】阶段1Day 1:AI 层级关系 — 四个嵌套的圈-CSDN博客
【FDE系列】阶段1Day 2:AI 三阶段发展史 — 会认 → 会判断 → 会创造-CSDN博客
【FDE系列】阶段1Day 3:符号 AI vs 机器学习 — 两条路线的本质区别-CSDN博客
【FDE系列】阶段1Day 4:Transformer 的历史意义 — 2017 年的分水岭-CSDN博客
【FDE系列】阶段1Day 5:本周复习与自测 — 检验你的 AI 认知地基-CSDN博客
【FDE系列】阶段1Day 6:Transformer 架构 — 一张图纸盖出千千万万栋楼-CSDN博客
【FDE系列】阶段1Day 7:LLM 本质 — 文字接龙机器-CSDN博客
【FDE系列】阶段1Day 8:Token — 模型眼中的最小单位-CSDN博客
【FDE系列】阶段1Day 9:AI 幻觉 — 为什么会一本正经地胡说八道-CSDN博客
【FDE系列】阶段1Day 10:上下文窗口 — 模型的记忆力上限 + 本周复习-CSDN博客
【FDE系列】阶段1Day 11:Prompt — 给模型立规矩-CSDN博客
【FDE系列】阶段1Day 12:Memory — 让模型记住上下文
【FDE系列】阶段1Day 13:RAG — 给模型配图书管理员-CSDN博客
【FDE系列】阶段1Day 14:Tool Use — 让模型动手操作-CSDN博客
【FDE系列】阶段1Day 15:MCP — 统一的工具接口标准 + 第三周复习-CSDN博客
FDE 基础概念
【FDE系列】阶段1Day 16:什么是 FDE — 把 AI 变成客户结果的人-CSDN博客
【FDE系列】阶段1Day 17:FDE vs 传统实施 — 三大本质区别-CSDN博客
【FDE系列】阶段1Day 18:FDE 三重身份 + C6 胜任力模型-CSDN博客
【FDE系列】阶段1Day 19:七阶段行动路径 + 行业经验的价值-CSDN博客
【FDE系列】阶段1Day 20:阶段总结与产出物 — 第一阶段收官-CSDN博客
阶段二:技术地基(Python + FastAPI + SQL + Docker + API 集成)
Python基础
【FDE系列】阶段2:Day 21:Python 环境搭建 — 写出你的第一行代码-CSDN博客
【FDE系列】阶段2:Day 22:变量、数据类型、条件判断 — Python 的“记忆“和“判断“-CSDN博客
【FDE系列】阶段2:Day 23:循环与函数 — 让代码跑 100 遍、把逻辑打包复用-CSDN博客
【FDE系列】阶段2:Day 24:数据结构 — 列表、字典、集合、元组-CSDN博客
【FDE系列】阶段2:Day 25:文件读写与 JSON — 让程序连通外部数据(第一周收官)-CSDN博客
【FDE系列】阶段2:Day 26:模块化编程 — 把代码拆成“抽屉柜“-CSDN博客
【FDE系列】阶段2:Day 27:异常处理与日志 — 让程序“摔不烂、查得到“-CSDN博客
FastAPI入门到进阶
【FDE系列】阶段2:Day 28:FastAPI 入门 — 把你的函数变成 API 服务-CSDN博客
【FDE系列】阶段2:Day 29:FastAPI 进阶 — Pydantic 模型与完整 CRUD 实战-CSDN博客
【FDE系列】阶段2:Day 30:生产代码规范 — 测试、类型注解、配置管理(第二周收官)-CSDN博客
SQL基础
【FDE系列】阶段2:Day 31:SQL 基础 — 增删改查一把梭-CSDN博客
【FDE系列】阶段2:Day 32:多表查询 — JOIN 与聚合-CSDN博客
【FDE系列】阶段2:Day 33:进阶查询 — 窗口函数与 CTE-CSDN博客
【FDE系列】阶段2:Day 34:数据清洗 — 把脏数据捋干净-CSDN博客
【FDE系列】阶段2:Day 35:Python + SQL — 工单接入 MySQL + 本周收官-CSDN博客
Linux基础
【FDE系列】阶段2:Day 36:Linux 入门与文件操作 — 扔掉鼠标的第一天-CSDN博客
【FDE系列】阶段2:Day 37:权限、进程与文本三剑客-CSDN博客
【FDE系列】阶段2:Day 38:Shell 脚本 — 把命令串起来自动跑-CSDN博客
【FDE系列】阶段2:Day 39:Linux 综合实战 — 让服务无人值守-CSDN博客
【FDE系列】阶段2:Day 40:Shell 进阶 — 生产级脚本与本周收官-CSDN博客
Docker
【FDE系列】阶段2:Day 41:Docker 入门 — 把环境装进盒子-CSDN博客
【FDE系列】阶段2:Day 41(up详细版):Docker 入门 — 把环境装进盒子-CSDN博客
【FDE系列】阶段2:Day 42:Dockerfile 实战 — 把你的应用打包成镜像-CSDN博客
【FDE系列】阶段2:Day 42(UP详细版):Dockerfile 实战 — 把你的应用打包成镜像-CSDN博客