几年前,老王在小区门口开了家小面馆:煮面、浇头、收银,一个人全包。后来生意火了,他扩成一家大餐厅,请了切配、掌勺、传菜、收银好几个师傅。麻烦也随之而来——切配把牛肉切好了,掌勺的却还没热锅;传菜员等了半天,不知道哪桌的菜该先上。整个后厨乱成一锅粥。老王终于醒悟:后厨缺的不是更多师傅,而是一份让所有人都照章办事的《总菜单》,外加一个能统筹全局的领班。
Docker 的世界也在经历同样的事。单个容器像一个全能的“小面馆”;但一个完整应用——尤其是一套 AI 应用——通常需要数据库、缓存、模型推理、前端页面等多个容器同时协作。Docker Compose 正是那个领班,它手里的《总菜单》就是 docker-compose.yml:把每个服务用什么镜像、暴露哪些端口、谁先启动、数据存在哪儿全部写清楚。你只需敲一条命令,整个后厨按单开工。本文就带你彻底读懂这份总菜单。
掌握 Docker Compose,你就能像点菜一样一键编排整套 AI 应用。这是 AI 工程师走向工程化、DevOps 工程师落地多容器部署的必备技能。
目录
可视化演示
展示Docker Compose多容器编排三大维度:服务拓扑(6容器bridge网络+DNS解析+端口映射)、生命周期(up启动/scale扩展/down停止)、网络与数据卷(bridge通信+命名卷+绑定挂载),三模式递进呈现编排核心机制。
本节配有交互动效(进入视口后自动加载,约 3KB)。
多容器服务拓扑:bridge网络+DNS解析 DC管理节点编排6个服务:WEB/API/DB/RED/WRK/NGX 服务名即主机名,Compose内置DNS自动解析容器IP Service → DNS → Container_IP ports字段映射宿主机端口到容器端口
总菜单是什么:Compose 概述
想象你从一家小面馆做起,生意好到不得不开成餐厅。一个人时,煮面、浇头、收银全包;人一多,反而乱了:切配把牛肉切好了,掌勺却还没热锅;传菜员干等半天,不知道哪桌先上。Docker 世界也一样。一个 docker run nginx 是“一个人的小面馆”;但完整的应用通常要数据库、缓存、后端、前端多个容器一起跑,谁先启动、谁连谁、数据放哪,必须有人统筹。
Docker Compose 就是 Docker 官方推出的“后厨领班”,解决单机上的多容器协作问题。它通过一份 docker-compose.yml 文件,把应用涉及的所有服务声明清楚,然后由 Compose 统一创建、启动、停止、清理。
使用 Compose 只有三步:写配置、拉镜像、启动。安装也简单——Docker Desktop 自带 Compose;Linux 上安装 docker-compose-plugin 插件即可。启动时执行 docker compose up -d,它会按依赖顺序把容器逐一拉起;收工时执行 docker compose down,一键清理容器和网络。
这份“总菜单”把每个岗位写得明明白白:谁掌勺、谁切配、谁传菜。老板只需一声令下,后厨自动按单运转——这就是 Compose 的魅力。
- 应用全家福
S_all表示整个应用系统。 - 集合容器花括号是集合符号,装下所有服务成员。
- 单个服务
s_i是第 i 个服务,例如数据库或推理接口。
总菜单的文书格式:配置语法
总菜单不能随手涂鸦:菜名写哪、配料写哪、口味备注写哪,都有固定格式。如果不按格式写,传菜员看不懂,整个后厨停摆。docker-compose.yml 的“文书格式”就是 YAML。YAML 靠缩进表达层级,冒号表示键值对。最常见的翻车现场,就是有人用了 Tab 缩进,Compose 直接拒绝启动。
一个最小配置包含三个层级:顶层声明项目元信息,services 列出所有服务,每个服务名下面是它的具体配置。核心字段有四个:image 指定用哪个镜像;ports 配置端口映射;depends_on 声明先启动谁;volumes 挂载数据卷。
例如一个经典三件套:web 依赖 api,api 依赖 db,在文件里就是三个服务名,各自配上镜像和依赖关系。这段配置看似简单,却是整个后厨的“岗位说明书”——每个容器是什么角色、什么时候入场,全都定死了。
菜单文书格式对了,每个岗位才知道自己做什么;YAML 缩进对了,Compose 才知道每个服务如何定义。
- 服务的四要素
s_i代表一个服务,尖括号里是它的完整档案。 - 镜像字段
image_i决定容器从哪个镜像创建,相当于“用哪本菜谱”。 - 端口映射
ports_i定义容器对外暴露的入口。 - 存储与配置
volumes_i处理持久化,env_i注入运行时配置。
传菜窗口与路线:网络编排
后厨传菜靠固定窗口和路线:热菜走热菜窗口,凉菜走凉菜电梯。没人会在走廊里大喊“红烧肉你算哪根葱”——大家认窗口、认编号。容器世界也一样,可靠沟通要靠网络规则。
每个容器启动后 IP 都会变。如果程序里写死某个容器的 IP,一重启就断连。Compose 的解决方案是:为每个项目默认创建独立网络,并内置 DNS 服务发现。同一个网络内的容器,直接用“服务名”当主机名访问,IP 变化不影响通信。
在 docker-compose.yml 里可以自定义网络,比如让数据库待在“内网”,只允许 API 服务访问它;前端则通过端口映射把入口暴露给宿主机。容器间通信时,URL 写成 http://api:8000,而不是 http://172.18.0.2:8000。对外提供服务则需要 ports 映射,例如 "8080:80" 表示宿主机 8080 端口转发到容器内 80 端口——这是外部世界进入后厨的唯一“窗口”。
传菜员认窗口编号,容器认服务名。菜单上写清楚走哪个窗口、哪个电梯,请求就能准时送达。
- 服务名
name_i是 compose 里定义的服务名。 - 解析动作
longmapsto表示“映射到”某个具体地址。 - 目标 IP
IP_i是容器当前的内网地址,由 Docker 自动分配。
后厨的冷柜:卷与存储
总菜单上会注明“高汤存放在 3 号冷柜,多个灶台共用”。容器是一次性餐盒——用完就扔;但高汤、秘制酱料这些“老本”,必须留在冷柜里,下次还能用。数据卷(volume)就是这个冷柜。
容器文件系统是临时的,容器删除后,里面写入的文件全部蒸发。数据卷是 Docker 管理的独立存储区,生命周期与容器分离。这是“持久化”的本质:锅可以换,但冷柜里的老汤不能丢。
声明一个命名卷,只需在文件顶层 volumes 下写个名字,服务里用 卷名:容器路径 的格式挂载。例如 model_cache:/cache,就把卷挂到了容器的 /cache 目录。还有一种是 bind mount,直接把宿主机目录映射进容器,适合开发时热更新代码。多个容器挂同一个卷时,它们就像共用一个冷柜:模型服务写缓存,另一个服务能立刻读到。注意 docker compose down 默认不删卷,只有显式加 -v 才会连冷柜一起清空。
冷柜里的老汤不会因为锅刷了就消失;卷里的模型权重,也不会因为容器删了就丢失。
- 两个容器
c_i与c_j代表参与共享的两个服务。 - 共享卷
V是命名卷,相当于公共冷柜。 - 双向读写
rightleftharpoons表示两个方向都可以读写同一份数据。
菜单上的流动贴纸:环境管理
总菜单上随时可以贴一张流动贴纸:“今日 VIP 席有忌口,改上素汤。”这些话不用重印菜单,贴上去就行。环境变量就是容器上的流动贴纸。
同一个镜像如果写死配置,换环境就得重新构建镜像。环境变量让“镜像不动,配置可变”——开发、测试、生产共用同一个镜像,只换变量。这就是 12-Factor 应用里“配置与代码分离”的思想。
Compose 提供三种写法。environment 直接在 yml 里写键值对,适合少量配置;env_file 引用一个外部 .env 文件,适合放较多或敏感的配置,注意别提交进 Git 仓库;变量替换 ${VAR:-default} 则让 compose 文件从宿主机环境或 .env 中取值,取不到就用默认值。比如 ${GPU_MODE:-cpu},同一份编排文件既能跑 CPU 也能跑 GPU,全看运行时怎么传。
贴纸撕下还能换新的;环境变量改完重启就生效——菜单(镜像)不用重印。
- 宿主变量
V_env是当前环境已经设置的变量。 - 默认回退若变量未定义,则使用
default。 - 最终取值
V_final是容器内实际拿到的值。
上菜顺序与火候:部署编排
总菜单上的出菜顺序是“先文火炖高汤,再爆炒,最后摆盘”。顺序一乱,整桌菜全砸。Compose 也要回答:谁先启动?什么时候才算“真正准备好”?
很多初学者以为 depends_on 就是“等上游就绪后再启动”,其实不然。它只保证上游容器先被创建并启动,但容器启动不等于服务可用——数据库镜像起来了,端口可能还在初始化。如果 API 立刻连接,照样失败。这就好比高汤锅刚放上灶台,你就让人开始炒菜,火候根本不对。
靠谱的姿势是配合健康检查:在服务上写 healthcheck,用命令反复探测就绪状态;然后在依赖方写 depends_on: condition: service_healthy,等待上游“体检合格”再启动自己。此外,Compose 还支持简单的扩缩容:docker compose up -d --scale worker=3 可以把 worker 服务扩到 3 个副本,让多个“传菜员”同时上工。
厨师看到高汤沸腾冒泡,才通知配菜师傅下料;容器看到上游 healthy,才真正开工。
- 下游启动
start(s_j)表示服务 j 开始启动。 - 上游健康
h(s_i)是服务 i 的健康检查结果。 - 等价条件
iff表示“当且仅当”,只有条件成立才放行。
打烊后的日常:部署运维
总菜单挂上墙只是开始。每天还要查菜卖了多少、哪个灶台糊锅了、午市后要不要歇业打扫。运维,就是后厨的日常管理。
Compose 提供一组命令管理容器生命周期。记住一条主线:启动 → 查看 → 查日志 → 进容器 → 清理。docker compose up -d 后台启动整个项目;docker compose ps 查看所有容器状态;docker compose logs -f api 跟踪某个服务的日志;docker compose exec api bash 钻进容器执行命令排查问题。
还有两个“项目治理”神器:docker compose config 会校验 yml 并渲染出最终配置,排查变量替换问题特别有用;docker compose down 停止并清理容器和默认网络,项目整体收摊。这些命令就像后厨总管的每日巡场——睁眼看订单(ps)、听厨师反馈(logs)、必要时亲自进后厨(exec)、打烊清场(down)。
后厨总管每天睁眼先看订单、听反馈、偶尔亲自下厨、打烊清场,一切井井有条——这就是运维的日常。
- 启动
up创建并启动所有服务,-d表示后台。 - 观察
ps看状态,logs看输出。 - 干预与收尾
exec进入容器排障,down停止并清理。
AI 的满汉全席:AI 场景实战
一套 AI 应用是满汉全席:模型推理是主菜,向量库是配菜,Redis 是传菜电梯,Postgres 是食材仓库,Nginx 是迎宾门童。总菜单把它们一次安排明白,谁都不能缺席。
AI 服务容器化后通常有多个角色:GPU 推理服务、任务队列、元数据库、对象存储、网关。Compose 能在单机或开发机上把整套系统编排起来,是 AI 工程化落地的轻量选择。一个典型配置骨架:api 服务通过 deploy.resources.reservations 申请 GPU;worker 从 Redis 队列拉取推理任务;postgres 保存请求与结果记录;nginx 做统一入口;模型权重用命名卷挂载,避免每次启动重新下载。
这套编排直接服务于 DevOps 流程:CI 里构建镜像、测试编排文件;CD 里用 up -d 发布新版本,用 down 回收旧环境。一条命令拉起全链路,一条命令整体换新——这就是 AI 应用工程化的“后厨总指挥”。
满汉全席要主厨整体调度,AI 应用也需要一份“总菜单”统一编排——这正是 Docker Compose 的用武之地。
- 应用整体
App代表整个 AI 系统。 - 多角色协作括号里是 web、api、worker、cache、db 五个容器角色。
- 编排函数
f表示 Compose 把它们组织成可运行整体。
AI 应用场景
大模型推理服务私有化部署
阶段:模型部署 / 推理服务化
解决的问题:传统方式要手动启动多个进程并管理依赖,端口冲突、环境不一致问题频发。AI 解决:一份 Compose 把 Nginx 网关、GPU 推理服务、Redis 缓存、Postgres 记录一次性编排起来,一条命令启动全链路。
具体案例:某企业把对话模型部署到内网,前端 + 推理 api + redis + 数据库在同一台 GPU 服务器上运行,运维同学只需 docker compose up -d,升级模型时替换镜像并 up -d 即可。
机器学习实验训练环境标准化
阶段:环境搭建 / 实验管理
解决的问题:复现实验时经常出现“我这儿能跑、你那儿跑不了”。AI 解决:用 Compose 同时启动 Jupyter、TensorBoard、GPU 训练容器,用卷共享代码与输出,新成员一条命令就获得完全一致的实验环境。
具体案例:算法团队在 GPU 服务器上通过 Compose 拉起 Jupyter + TensorBoard + 数据集卷,成员用浏览器写代码、看训练曲线,实验记录全部落在共享卷里。
数据标注流水线平台
阶段:数据工程 / 标注管理
解决的问题:标注平台由前端、API、任务队列、对象存储多个组件构成,手工启动极易踩依赖坑。AI 解决:Compose 编排标注后端 + MinIO + Redis 队列 + MySQL,一次启动整个流水线。
具体案例:一个标注小组用 Compose 跑起标注平台套件,数据通过 API 批量导入,标注结果写回数据库,全链路一个 up 搞定,下班一个 down 干净收摊。
Python 演示
# -*- coding: utf-8 -*-"""生成一个 AI 推理服务的 docker-compose.yml 编排文件""" class ComposeGenerator: def __init__(self, project): self.project = project self.services = {} def add_service(self, name, image, ports=None, depends_on=None, volumes=None, environment=None): self.services[name] = { 'image': image, 'ports': ports or [], 'depends_on': depends_on or [], 'volumes': volumes or [], 'environment': environment or {}, } def to_yaml(self): text = 'version: "3.8"\nservices:\n' for name, conf in self.services.items(): text += f' {name}:\n' text += f' image: {conf["image"]}\n' if conf['ports']: text += ' ports:\n' for p in conf['ports']: text += f' - "{p}"\n' if conf['depends_on']: text += ' depends_on:\n' for d in conf['depends_on']: text += f' - {d}\n' if conf['volumes']: text += ' volumes:\n' for v in conf['volumes']: text += f' - {v}\n' if conf['environment']: text += ' environment:\n' for k, v in conf['environment'].items(): text += f' {k}: {v}\n' text += ' networks:\n - ai_net\n' text += 'volumes:\n model_cache:\n' text += 'networks:\n ai_net:\n' return text def start_order(self): order = [] remaining = set(self.services.keys()) while remaining: ready = [n for n in remaining if all(d in order for d in self.services[n]['depends_on'])] if not ready: raise ValueError('存在循环依赖!') order.extend(ready) remaining -= set(ready) return order if __name__ == '__main__': gen = ComposeGenerator('ai-inference') gen.add_service('redis', 'redis:7-alpine') gen.add_service('api', 'my-ai/api:latest', ports=['8000:8000'], depends_on=['redis'], volumes=['model_cache:/cache'], environment={'MODEL_NAME': 'bert-base'}) gen.add_service('web', 'my-ai/web:latest', ports=['8080:80'], depends_on=['api']) print(gen.to_yaml()) print('启动顺序: ' + ' -> '.join(gen.start_order()))
class ComposeGenerator:定义“总菜单生成器”类,负责收集服务和渲染配置。self.services = {}用字典登记所有服务,服务名是索引,配置是内容。def add_service(self, name, image, ...)往菜单里添加一道“菜”:服务名、镜像、端口、依赖、卷、环境变量。text += f' {name}:\n'在 YAML 中为当前服务开一个新段落。if all(d in order for d in self.services[n]['depends_on']):检查某个服务的所有依赖是否都已启动。order.extend(ready)将本轮“可以启动”的服务加入启动队列。remaining -= set(ready)从未启动集合中移除本轮已就绪的服务。print('启动顺序: ' + ' -> '.join(gen.start_order()))打印拓扑排序结果,直观看到“谁先谁后”。
常见误区
误区“我在 api 下写了 `depends_on: [db]`,可 api 还是连不上数据库,是不是我写错了?”
正解depends_on 只保证 db 容器先被创建并启动,不保证 db 内部的服务已经可用。数据库进程可能还在初始化。要真正等就绪,需要给 db 配置 healthcheck,再用 condition: service_healthy 声明等待。就像传菜员不能只看“锅上灶了”就上菜,得等汤真的沸腾。
为何常见:名字太有误导性,“依赖”很容易让人理解成“就绪”,加上早期教程普遍只讲依赖不讲健康检查。
误区“我在 web 容器里用 `localhost:8000` 访问 api,为什么连不上?”
正解localhost 指的是当前容器自己,不是身边那个容器。Compose 网络里正确的写法是用服务名,例如 http://api:8000。就像找后厨的人不能喊办公室自己的电话分机,要打传菜窗口对应的内线。
为何常见:平时在宿主机开发习惯了 localhost,容易忘记每个容器都是一台独立的小主机。
误区“我怕 down 会把数据库清空,所以容器一直挂着不敢停。”
正解down 默认只停止并删除容器与默认网络,命名卷里的数据会完整保留。只有显式加 -v 才会连卷一起删除。放心每天 down 也没问题,冷柜里的老汤不会丢。
为何常见:看到 down 会“删除容器”就想当然以为全部数据都没了,没分辨容器层与卷的生命周期差异。
误区“既然 Compose 能编排多容器,是不是不用学 K8s 了?”
正解Compose 是单机多容器编排,适合开发、测试与轻量生产;Kubernetes 是跨多台机器的集群调度系统,负责自动伸缩、故障转移、服务网格。两者是“便利店”与“连锁集团总部”的关系,定位完全不同。
为何常见:学会 Compose 后觉得“什么都能管”,把单机编排与集群调度混为一谈——这是容器领域最经典的认知跳跃。
面试题
Q1. Docker Compose 默认查找的配置文件名是?
- A.dockerfile.yml
- B.docker-compose.yml← 正确答案
- C.container.yaml
- D.deploy-apply.yml
答案:B。Compose 默认查找 docker-compose.yml(新版也支持 compose.yaml)。这就像后厨只认那份固定的《总菜单》,换文件名它就找不到了。其他选项是干扰项,dockerfile.yml 容易和 Dockerfile 混淆。
Q2. 在同一个 Compose 网络内,web 容器要访问 api 容器的 8000 端口,正确写法是?
- A.宿主 IP 加映射端口
- B.固定容器 IP 直连
- C.服务名加容器端口← 正确答案
- D.随机分配的域名
答案:C。Compose 内置 DNS 服务发现,容器之间通过“服务名 + 容器内端口”通信,如 http://api:8000。容器 IP 重启会变,所以不应写死;宿主 IP 加映射端口虽然也能通,但不是内部网络的推荐方式。
Q3. 关于 depends_on 的说法,正确的是?
- A.等上游健康后再启动
- B.仅控制启动先后顺序← 正确答案
- C.自动重启异常上游
- D.指定容器内存上限
答案:B。depends_on 只保证上游容器先创建并启动,不保证服务就绪。要等上游健康,需要配合 healthcheck 与 condition: service_healthy。就像“锅先上灶”不等于“汤已煮沸”。
Q4. Compose 中推荐用什么方式持久化数据库数据?
- A.放容器 /tmp 临时目录
- B.写入宿主机任意目录
- C.使用命名卷 volume← 正确答案
- D.写进环境变量配置
答案:C。命名卷由 Docker 管理,生命周期独立于容器,是生产环境持久化数据的推荐方式。容器 /tmp 会随容器销毁而消失;写宿主任意目录属于 bind mount,适合开发但不推荐直接用于数据库。
Q5. 在 compose 文件中写“变量没设置时使用默认值”,正确语法是?
- A.$VAR:default
- B.${VAR:-default}← 正确答案
- C.$(VAR:default)
- D.$VAR-default
答案:B。${VAR:-default} 是 Compose 的变量替换语法:变量有值则用该值,无值则回退到 default。例如 ${GPU_MODE:-cpu} 在未设置 GPU_MODE 时自动用 cpu,其他写法都不是 Compose 支持的格式。
Q6. 执行 docker compose down 后,会发生哪些事?
- A.停止并删除容器← 正确答案
- B.删除默认网络← 正确答案
- C.总是删除命名卷
- D.删除 docker 镜像
答案:AB。down 会停止并删除容器、删除默认网络,但命名卷默认保留(加 -v 才会删除),镜像也从不被删除。这就像打烊清场:桌子收好、灯关掉,但冷藏库里的老汤继续留着。
Q7. 关于 Compose 网络的说法,正确的是?
- A.Compose 自动创建项目专用网络← 正确答案
- B.服务名可在网络内充当域名← 正确答案
- C.宿主机可直接访问容器端口
- D.不同 Compose 项目默认网络隔离← 正确答案
答案:ABD。每个 Compose 项目默认创建独立网络,服务名在网内可解析;不同项目网络互相隔离。宿主机不能直接进容器网络,必须通过 ports 映射才能访问,所以 C 错误。
Q8. 要让 api 服务在 db 的数据库真正可连接后再启动,应如何配置?
- A.在 api 里写
depends_on: [db] - B.给 db 加 healthcheck,api 写
condition: service_healthy← 正确答案 - C.在 api 启动命令前 sleep 10 秒
- D.把 api 和 db 合并到同一个容器
答案:B。depends_on 只保证启动顺序;sleep 是碰运气。正确做法是给 db 配置健康检查,api 通过 depends_on 的 condition: service_healthy 等待。就像传菜员要等厨师喊“出菜”,而不是自己默数 10 秒。
Q9. 配置 ports: "8080:80" 的含义是?
- A.容器 8080 映射到宿主机 80
- B.宿主机 8080 映射到容器 80← 正确答案
- C.容器内双向互通流量
- D.自动随机分配端口
答案:B。ports 的格式是“宿主机端口:容器端口”,"8080:80" 表示访问宿主机 8080 端口会转发到容器的 80 端口。这是外部世界进入后厨的唯一窗口。
Q10. 用 Compose 部署 GPU 模型推理服务,合理的做法包括?
- A.用 deploy 配置申请 GPU 资源← 正确答案
- B.模型权重放命名卷避免重复下载← 正确答案
- C.每次改模型都重新构建镜像
- D.用 healthcheck 探测推理接口是否就绪← 正确答案
答案:ABD。GPU 要用 deploy.resources.reservations 显式申请;模型权重放卷里可跨容器复用、避免每次重启重下;healthcheck 能探测推理接口是否真正就绪。每次改模型都重打镜像既慢又浪费,应该用卷承载模型文件。