---
url: /courses/cloud-platform-build-management/06-distributed-compose/index.md
---
# 第六次课：用 Docker Compose 组织多服务应用

## 进入本课环境

* `[Windows PowerShell] wsl ~`：打开本机默认的 WSL2 Ubuntu；出现 `用户名@主机名:~$` 后再执行标为 `[WSL]` 的命令。
* `[WSL] ssh root@你的ECS公网IP`：从 WSL 连接自己的 ECS；如果登录用户不是 `root`，请换成控制台显示的实际用户，看到远端提示符后再执行标为 `[ECS]` 的命令。

第五次课的 WordPress 已经包含 Web、PHP 和数据库，但如果只会照着启动，还看不清服务为什么能互相找到，也很难判断某个容器退出后请求停在哪里。

本课把学习站简化成两个容易观察的服务：

* `web`：接收浏览器请求，返回首页，并把 `/api/` 转给内部 API；
* `api`：只在 Compose 网络中提供 `status.json`，不映射宿主机端口。

页面能显示“API 状态：ok”时，两项服务和内部请求链路才算真正跑通。

::: warning 安全边界
所有启停命令都要在 `~/cloud-course/lab-06` 中执行，并先用 `docker compose ls`、`docker compose ps` 确认项目名。不要运行会停止整台服务器所有容器的命令。
:::

## 6.1 分布式不是“服务器越多越好”

**分布式系统**（多个相互通信的进程或服务共同完成一项业务的系统） 既可以跨多台物理机运行，也能在一台宿主机的多个容器间模拟演练。

把应用拆成多服务后，系统的运行特点也跟着变了。

解耦带来的优势：

* Web 前端与 API 后端能独立迭代、按需扩容；
* 各组件的职责、日志和资源边界更清晰；
* 服务之间通过固定名称通信，不再依赖动态变化的容器 IP。

随之而来的挑战：

* 网络联通、域名解析与依赖顺序成了新的潜在故障点；
* 单个容器处于运行状态，并不代表整体业务正常可用；
* 配置项、版本依赖与持久化卷的数量成倍增加；
* 必须有一套规范的启动、巡检与清理流程。

::: tip 关键提醒
拆分服务不会消灭故障，只会转移故障边界。验收时既要看单个容器的存活，更要验证从浏览器到最深层 API 的完整请求链。
:::

## 6.2 复习镜像、容器、网络、端口和卷

Compose 习惯把几类关键 Docker 对象打成一个项目：

* **镜像**（创建容器所需的只读文件系统与启动说明）；
* **容器**（镜像运行后的具体进程实例，拥有独立的网络与文件空间）；
* **网络**（让同一项目里的容器直接通过服务名互相通信）；
* **端口映射**（把宿主机的 IP 和端口转发给容器内部端口）；
* **卷或挂载**（把数据或配置文件独立于容器生命周期持久化）。

本课涉及两条不同的请求路径：

```txt
浏览器 -> ECS 127.0.0.1:18006 -> web:80
web     -> Compose 内部网络     -> api:80
```

`web:80` 和 `api:80` 能够同时存在，是因为它们分别处于各自独立的容器网络空间里。宿主机只暴露了一个 18006 端口，完全不会触发“80 端口冲突”。

## 6.3 从 Compose 文件读出系统结构

进入 **ECS** 上的实验目录后，先准备好环境变量文件：

```bash
[ECS] mkdir -p ~/cloud-course/lab-06
[ECS] cd ~/cloud-course/lab-06
[ECS] cp starter/.env.example .env
[ECS] chmod 600 .env
```

这里的 `.env` 仅用来设定入口端口，不放密钥。接着用三条命令做静态检查：

* `docker compose config --services`：列出项目定义的服务名；
* `docker compose config --images`：列出各服务依赖的镜像；
* `docker compose config --quiet`：检查语法格式与变量展开。

```bash
[ECS] docker compose config --services
[ECS] docker compose config --images
[ECS] docker compose config --quiet
```

顺利的话会看到 `api` 和 `web`。静态检查成功只能说明配置文件写法无误，容器还没跑起来。

需要特别注意 `depends_on`：它只能控制启动优先顺序（比如在 Web 前等待 API 变得健康），并不是持续的守护进程。如果 API 在后续运行中意外挂掉，Compose 并不会自动杀掉 Web。此时 Web 容器虽然还在，但整个业务其实已经断了。

## 6.4 启动项目并确认边界

启动前检查资源、现有项目和端口：

```bash
[ECS] free -h
[ECS] docker compose ls
[ECS] ss -lnt 'sport = :18006'
```

确认 18006 端口未被占用后，再启动：

```bash
[ECS] docker compose up -d
```

检查容器：

```bash
[ECS] docker compose ps
```

`api` 不应显示宿主机端口；`web` 应显示 `127.0.0.1:18006->80/tcp`。随后检查项目网络中的服务名：

```bash
[ECS] docker compose exec web getent hosts api
[ECS] docker compose exec web wget -qO- http://api/status.json
```

第一条确认 `api` 能被内部 DNS 解析，第二条直接从 Web 容器访问 API。输出 JSON 时，说明内部链路可用。

{{guided-demo:lesson-06-compose-path}}

## 6.5 从浏览器验证完整请求

在 **WSL** 建立 SSH 隧道：

```bash
[WSL] ssh -N -L 18006:127.0.0.1:18006 你的ECS登录目标
```

Windows 浏览器打开 `http://127.0.0.1:18006/`。页面脚本会请求 `/api/status.json`，Nginx 再将其转发给 `api:80`。

浏览器页面比单独的 `docker compose ps` 多验证了一层：不仅证明两个进程在运行，还证明前端真实请求拿到了 API 数据。

## 6.6 一组生命周期命令分别改变什么

`docker compose` 的常用命令不能混用：

* `up -d`：按当前配置创建或更新并启动服务。
* `ps`：查看本项目容器状态和端口。
* `logs`：读取本项目服务日志，不改变运行状态。
* `stop api`：停止指定服务，容器仍保留。
* `start api`：启动已经存在的 API 容器。
* `down`：停止并删除本项目容器与网络，默认保留命名卷。

只查看最近日志：

```bash
[ECS] docker compose logs --tail 20 web api
```

持续跟随日志会占住终端，可用 `Ctrl+C` 结束查看；这不会停止容器。

## 6.7 停止依赖服务会发生什么

故障演练前，先确认当前服务正常：

```bash
[ECS] curl -sS http://127.0.0.1:18006/api/status.json
```

只停止当前项目的 API：

```bash
[ECS] docker compose stop api
```

分别观察“容器状态”和“业务状态”：

```bash
[ECS] docker compose ps
[ECS] curl --max-time 8 -sS -o /dev/null -w 'http=%{http_code}\n' \
  http://127.0.0.1:18006/api/status.json
[ECS] docker compose logs --since 30s --tail 8 web
```

Web 仍可能显示 `Up`，但 API 请求返回 502/504，日志出现上游连接失败或超时。这正是“单个服务活着，整体业务仍然失败”的例子。

恢复后回归验证：

```bash
[ECS] docker compose start api
[ECS] docker compose ps
[ECS] curl -sS http://127.0.0.1:18006/api/status.json
```

{{chat-lab:lesson-06-compose-evidence}}

## 6.8 配置、内容和秘密怎样进入项目

本课使用三种挂载：

* `./web`：学习站静态页面；
* `./api`：API 演示数据；
* `./gateway.conf`：网关配置。

它们都在项目目录中，适合教学时直接阅读。生产环境还需要考虑镜像版本、只读文件系统、配置发布和权限，本课先把“配置来源”和“容器内位置”对应起来。

`.env.example` 只描述变量接口。后续涉及密码时：

* `.env` 只在个人环境中保存并限制权限；
* `.env.example` 不放有效值；
* 报告、截图和代码仓库都不出现秘密；
* 容器环境变量也不等于完善的秘密管理。

## 6.9 分层实训与提交

完整步骤见[实训 06：部署并排查多服务站点](./lab-06.md)，其中可以下载 `compose.yaml`、`.env.example` 和应用文件。

**保底任务**

* 从 Compose 文件画出 Web 与 API 关系；
* 完成 `config --services`、镜像与端口识读；
* 根据给定 `ps`、HTTP 和日志判断故障层。

**标准任务**

* 在个人 ECS 启动双服务项目；
* 通过 SSH 隧道访问学习站 v2；
* 停止 API，记录 Web 状态、HTTP、日志和恢复结果；
* 清理项目并证明 18006 已释放。

**挑战任务**

* 修改 `status.json` 的版本号并观察是否需要重建容器；
* 为 Web 增加一个只读健康检查路径；
* 解释为什么 `depends_on` 不能代替持续健康监控。

提交：

```txt
班级_学号_姓名_第06次课_DockerCompose多服务报告.docx
```

只上传一个 Word 到智慧职教“第06次课”，不上传 `.env`、公网地址、SSH 配置或整个容器数据目录。

{{reflection-checkpoint:lesson-06-compose-ready}}

## 6.10 第六次课小测

{{assessment:lesson-06-check}}

## 6.11 小结

* Compose 把服务、网络、端口和挂载组织成一个可重复操作的项目。
* 服务名是内部通信入口，容器临时 IP 不应写进配置。
* 宿主机端口映射和容器内部端口属于不同网络空间。
* `depends_on` 解决启动顺序，不保证运行期间依赖永远健康。
* `ps`、HTTP 响应和日志结合起来，才能完整反映容器与业务的真实状态。
* 所有故障演练和清理都必须限定到当前 Compose 项目。

下一次课将把关注点转向数据库，比较复制、备份和恢复三种不同的数据保护能力。

## 6.12 资料来源

以下资料在 2026-07-28 核对：

* [Docker Compose 文件参考](https://docs.docker.com/reference/compose-file/)
* [Docker Compose 网络](https://docs.docker.com/compose/how-tos/networking/)
* [Docker Compose 概览与应用生命周期能力](https://docs.docker.com/compose/)
* [Compose `depends_on`](https://docs.docker.com/reference/compose-file/services/#depends_on)

{{assistant-invite:lesson-06-finish}}
