📚前言
📒FDE系列内容总纲:
🚄前置课程列表:
见文档结尾附录。
🚀阶段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 写法 |
|---|---|
nginx:alpine(镜像) |
image: nginx:alpine |
--name demo-nginx |
container_name: demo-nginx |
-p 8080:80 |
ports: - "8080:80" |
--restart unless-stopped |
restart: unless-stopped |
启动:
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.0
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 . .
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 |
|---|---|---|
image |
用现成镜像 | 直接写镜像名 |
build: . |
从 Dockerfile 构建 | 先 build 再 run |
container_name |
容器名 | --name |
ports |
宿主机:容器端口映射 | -p |
volumes |
数据卷/绑定挂载 | -v |
environment |
环境变量(键值对) | -e |
env_file |
从文件批量读环境变量 | --env-file |
depends_on |
启动顺序/就绪依赖 | 无(手工控制) |
restart |
重启策略 | --restart |
networks |
自定义网络 | --network |
command |
覆盖镜像默认启动命令 | 镜像名后的命令 |
working_dir |
工作目录 | -w |
环境变量的两种安全写法
写法 A:直接写在 yml 里(学习用,简单直观,就是你现在用的)
environment:
DB_PASSWORD: root123
写法 B:敏感信息用 .env 文件,别提交到 Git(生产推荐)
项目根目录建 .env:
DB_PASSWORD=root123
MYSQL_ROOT_PASSWORD=root123
compose 文件里用 ${变量名} 引用:
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博客