Docker Compose — 声明式地运行多服务应用
Docker Compose — 声明式地运行多服务应用
这一章解决一个已经在眼前的问题: 一个应用两个服务(数据库 + 管理工具), 就要开两个终端敲两条长命令;服务一多,重来一遍 = 灾难。 主线例子:一个 PostgreSQL + pgAdmin 的双服务应用,从 compose.yml 的 24 行出发, 走完"起、扩、拆、多环境、模块化"的全程。
1. 先立概念:声明式 vs 命令式
书里用一段对话式的讲解立了全章的地基1:
- 命令式(imperative):一步步告诉系统该做什么、每一步怎么做、 每种意外怎么处理——必须显式、精确、覆盖所有边缘情况;
- 声明式(declarative):只告诉系统期望的状态(我想要什么), 由系统自己琢磨怎么达成、偏离了怎么拉回来。
Docker 明确推荐声明式路线,Compose 就是这条路的工具1。 第 09 章说过"编排 = 调和期望状态",Compose 是这套思想在单机上的第一次完整体验。
2. 主线例子:24 行的 compose.yml
应用的形态:db 服务(PostgreSQL,镜像 postgres:alpine)+ pgadmin 服务 (pgAdmin 4,镜像 dpage/pgadmin4)。compose.yml 的结构(照书里的逐行讲解)2:
services:
db:
image: postgres:alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: pets
volumes:
- pg-data:/var/lib/postgresql/data # 数据持久化
- ./db:/docker-entrypoint-initdb.d # 首次启动跑初始化脚本
pgadmin:
image: dpage/pgadmin4
ports:
- 5050:80 # 宿主 5050 → 容器 80
environment:
PGADMIN_DEFAULT_EMAIL: admin@acme.com
PGADMIN_DEFAULT_PASSWORD: admin
volumes:
- pgadmin-data:/var/lib/pgadmin
volumes:
pg-data:
pgadmin-data:四件事要看懂2:
- services 块:每个服务一个条目;
image指定镜像,environment注入环境变量,ports发布端口,volumes挂卷(命名卷持久化数据,宿主目录可以塞初始化脚本); - 顶层 volumes 块:服务用到的命名卷必须在这里声明;首次运行创建,之后复用—— 数据库崩了重启,数据还在;
docker compose up一条命令:拉镜像、自动建一个叫step1_default的网络、 建两个卷、起两个容器——名字全部带 目录名前缀(step1_db-1、step1-pg-data……)。 这个前缀就是"项目名",可用--project-name demo显式指定; 项目机制把网络、容器、卷分组隔离3;- 服务名就是主机名:pgAdmin 里连数据库,地址栏填
db即可—— Compose 自动建的用户自定义网络内置 DNS(第 10 章的彩蛋在这里兑现)2。
起好以后浏览器开 localhost:5050,用 compose.yml 里定义的邮箱和密码登录,
就能管理数据库了2。
3. 用 Compose 构建和推送
compose.yml 里可以写 build: 段(context + dockerfile),于是:
docker compose build——只构建所有镜像;docker compose up -d --build——构建并后台启动;- 构建出的镜像自动命名,如
<用户名>/ch11-web:1.0; docker compose push——把定义的所有镜像推上注册表(前提:docker login 过)4。
开发循环因此收成一条命令:改完代码,up -d --build。
4. 扩容:一条命令,一个坑
应用火了,web 服务要加实例(水平扩展,scale out):
$ docker compose up -d --scale web=3
→ web-1、web-2、web-3 三个容器
→ 但!实例 2、3 起不来:宿主端口 3000 已被实例 1 占用[^5]坑的解法:ports 里只写容器端口、不写宿主端口(- 3000 而不是
3000:3000),Docker 就为每个实例挑一个临时端口(书里实测分配到 50162、50165、
50166),docker compose ps 里能看到各自映射,curl 逐个验证都能工作5。
这个坑是 Compose 扩容的边界预告:同一服务的多个实例共享不了同一个宿主端口, 要上真正的负载均衡(第 09 章那个外部服务),单机 Compose 需要你在前面再架一层。
5. 多环境:overrides
同一份应用,开发/测试/生产配置不同。做法:基础文件 + 覆盖文件:
$ docker compose -f compose.yml -f compose.prod.yml up
↑ 后文件的字段覆盖/合并前文件同名服务的字段[^6]命名约定:基础叫 compose.yml、覆盖叫 compose.override.yml 时,
docker compose up 会自动叠加,不用写 -f;其他任何文件名都要显式 -f。
服务多、环境多时 override 链会变笨重——这时该用 include6。
6. 模块化:include(Compose v2.20+)
大项目里一个文件涨到没法维护,团队希望"服务定义挨着它自己的代码"。
include 是正式的解法(注意书里强调的两个词:)7
include:
- ../services/db/compose.yaml
- ../services/web/compose.yaml- include ≠ overrides:overrides 是合并/覆盖同名服务的字段; include 是整份文件拉进来,而且同名资源直接报错,绝不静默合并;
- 每个被 include 的文件在自己的项目目录里求值——build context、卷挂载、 env_file 的相对路径都按"它自己单独跑"的样子解析;
- 长语法可以指定
project_directory和env_file,控制得更细; - include 可以递归嵌套(被拉进来的文件还能再 include 别的)——
monorepo 里每个团队拥有自己的 compose 文件,顶层一份文件 include 全部,
一条
docker compose up起整个栈7。
7. Compose 的边界:什么时候必须上编排器
书里给了对照(要点转述)8:
| 维度 | Docker Compose | 完整编排器(Kubernetes/Swarm) |
|---|---|---|
| 主机 | 单机 | 跨多机 |
| 扩容 | 手动 --scale,受单机和端口限制 | 声明式自动扩缩,跨节点调度 |
| 自愈 | 无(容器退出不会自动找别处重建) | 控制循环持续调和:挂了自动补、节点死了自动挪 |
| 服务发现/负载均衡 | 内置 DNS(单机够用) | 集群级 DNS + 虚拟 IP 负载均衡 |
| 适用 | 开发环境、CI、小型单机部署 | 生产、多节点、高可用要求 |
一句话:Compose 是"一台机器上的多容器管家",不是编排器—— 第 09 章那十二个模式里它只实现了服务发现一个半。这也正好回答了 "什么时候读第 14 章":需要跨机自愈那天。
8. 作者的判断与证据
- "声明式优于命令式"是 Docker 官方立场,书里照单全收,依据是命令式 必须穷举边缘情况、天然不可扩展1。
- include 与 overrides 的区分(报错 vs 静默合并)是硬规则,不是风格偏好7。
- **"Compose ≠ 编排器"**的边界判断与全书结构一致(第 14 章才讲编排), 对照表是其论证8。
- 扩容的端口冲突是实测坑(带具体端口号),证据硬5。
判断(我们的,不是书里的): Compose 在 2026 年的真实角色已经从 "开发工具"外溢到"小型生产"——单机 VPS 上跑个小站,Compose + 定时备份 是完全合格的生产形态,不必为了上 K8s 而上 K8s。书里"生产必须编排器"的 暗示可以放宽为:多节点、高可用、频繁发布三者占齐才必须。 如果错,会错在: 对合规/审计要求高的企业环境,即便单机也更倾向托管编排 (拿现成的 RBAC、审计日志、策略引擎),这时候 Compose 的朴素反而是负担。
9. 边界与局限
- 本章示例以 pgAdmin 网页操作为主,UI 截图不可搬运;命令与 YAML 是可靠部分。
--scale不做负载均衡,书里没给"前面架一层"的完整方案(那是第 10 章 Traefik 手艺的用武之地)。- include 是 v2.20(2023 年中)的新特性,旧版 Compose 只有 -f 链;
书上写的是"较新版本",实际部署前核对
docker compose version。 - Compose 的 secrets、healthcheck 依赖、profiles(按 profile 启停服务组) 等高级特性本章未展开。
10. 可带走的
- 声明式 = 描述期望状态;Compose 是 Docker 官方推荐的声明式路线1。
- compose.yml 四要素:services(image/environment/ports/volumes)+ 顶层 volumes 声明2。
- 项目名 = 目录名前缀,资源分组隔离;
--project-name可改3。 - 服务名即主机名——Compose 网络内置 DNS 开箱即用2。
- 开发循环一条命令:
docker compose up -d --build;推送:docker compose push4。 - 扩容只写容器端口(不写宿主端口),让 Docker 分临时端口,否则端口冲突5。
- 多环境:覆盖文件,
compose.override.yml有命名约定;其他名字要-f6。 - include ≠ overrides:整份拉入、同名报错、路径各自求值、可递归——模块化正解7。
- Compose 管单机,编排器管集群;自愈、跨机调度、自动扩缩是分水岭8。
11. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 声明式 vs 命令式 | What has changed since the last edition of the book | text/120-fm-what-has-changed-since-the-last-edition-of-the-b.txt:1(搜「Declarative」) · text/120-fm-what-has-changed-since-the-last-edition-of-the-b.txt:11(搜「desired state」) |
| compose.yml 逐行讲解 | Running a multi-service app | text/121-fm-running-a-multi-service-app.txt:7(搜「declarative」) · text/121-fm-running-a-multi-service-app.txt:21(搜「services」) · text/121-fm-running-a-multi-service-app.txt:29(搜「image」) · text/121-fm-running-a-multi-service-app.txt:37(搜「pg-data」) · text/121-fm-running-a-multi-service-app.txt:61(搜「volumes」) |
| up 的产出与项目前缀 | Running a multi-service app | text/121-fm-running-a-multi-service-app.txt:83(搜「compose up」) · text/121-fm-running-a-multi-service-app.txt:92(搜「step1_default」) · text/121-fm-running-a-multi-service-app.txt:99(搜「prefix」) |
| 服务名当主机名 | Running a multi-service app | text/121-fm-running-a-multi-service-app.txt:139(搜「corresponds to the name」) |
| 构建与推送 | Building images with Docker Compose | text/123-fm-running-an-application-with-docker-compose.txt:7(搜「ch11-web」) · text/126-fm-using-docker-compose-overrides.txt:3(搜「compose push」) |
| --scale 与端口冲突 | Scaling a service | text/124-fm-scaling-a-service.txt:34(搜「--scale web=3」) · text/124-fm-scaling-a-service.txt:42(搜「same host port」) · text/124-fm-scaling-a-service.txt:77(搜「3000」) · text/124-fm-scaling-a-service.txt:95(搜「50162」) |
| 项目名与卷前缀 | Scaling a service | text/124-fm-scaling-a-service.txt:10(搜「prefix」) · text/124-fm-scaling-a-service.txt:17(搜「project-name」) |
| overrides 约定 | Using Docker Compose overrides | text/126-fm-using-docker-compose-overrides.txt:25(搜「override」) · text/127-fm-modularizing-applications-with-include.txt:3(搜「compose.override.yml」) · text/127-fm-modularizing-applications-with-include.txt:7(搜「-f <base」) |
| include 语义 | Modularizing applications with include | text/127-fm-modularizing-applications-with-include.txt:23(搜「include ≠ overrides」) · text/127-fm-modularizing-applications-with-include.txt:26(搜「v2.20」) · text/127-fm-modularizing-applications-with-include.txt:29(搜「project directory」) · text/127-fm-modularizing-applications-with-include.txt:52(搜「error」) · text/127-fm-modularizing-applications-with-include.txt:54(搜「recursively」) |
| Compose vs 编排 | When to use Docker Compose versus a full orchestration system | text/128-fm-when-to-use-docker-compose-versus-a-full-orchest.txt:1(搜「conflicts」) · text/129-fm-summary.txt:6(搜「Scaling」) |
| 章小结 | Summary | text/129-fm-summary.txt:5(搜「declarative」) |
Footnotes
-
出处:「What has changed since the last edition of the book」(
text/120-fm-what-has-changed-since-the-last-edition-of-the-b.txt:1,搜「Declarative」;:11,搜「desired state」;:7,搜「declarative approach」)。 ↩ ↩2 ↩3 ↩4 -
出处:「Running a multi-service app」第 1–67 段(
text/121-fm-running-a-multi-service-app.txt:7,搜「declarative way」;:21,搜「services」;:29,搜「image」;:37,搜「pg-data」;:40,搜「docker-entrypoint-initdb.d」;:61,搜「volumes」;:139,搜「corresponds to the name」)。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 -
出处:「Running a multi-service app」第 80–119 段(
text/121-fm-running-a-multi-service-app.txt:83,搜「compose up」;:91,搜「step1_default」;:94,搜「step1_pgadmin-data」;:99,搜「prefix」)。 ↩ ↩2 -
出处:「Building images with Docker Compose」(
text/123-fm-running-an-application-with-docker-compose.txt:7,搜「ch11-web」;:8,搜「node:20-」)与「Building and pushing an application」(text/126-fm-using-docker-compose-overrides.txt:3,搜「compose push」)。注:部分小节正文因转码顺序错位,锚取相应文件中的真实句。 ↩ ↩2 -
出处:「Scaling a service」第 24–107 段(
text/124-fm-scaling-a-service.txt:34,搜「--scale web=3」;:36,搜「web-1」;:42,搜「same host port」;:47,搜「leave out the host port」;:95,搜「50162」;:100,搜「curl -4」)。 ↩ ↩2 ↩3 -
出处:「Using Docker Compose overrides」(
text/126-fm-using-docker-compose-overrides.txt:25,搜「override」;text/127-fm-modularizing-applications-with-include.txt:3,搜「compose.override.yml」;:7,搜「-f <base compose file>」)。 ↩ ↩2 -
出处:「Modularizing applications with include」第 15–56 段(
text/127-fm-modularizing-applications-with-include.txt:16,搜「cumbersome」;:23,搜「include ≠ overrides」;:26,搜「v2.20」;:29,搜「project directory」;:51,搜「error」;:54,搜「recursively」)。 ↩ ↩2 ↩3 ↩4 -
出处:「When to use Docker Compose versus a full orchestration system」(
text/128-fm-when-to-use-docker-compose-versus-a-full-orchest.txt:1,搜「conflicts」)与「Summary」的对照表(text/129-fm-summary.txt:6,搜「Scaling」;:5,搜「Declarative」)。 ↩ ↩2 ↩3