← 全部笔记

Docker · 6 Compose 多容器

把一组容器写进一份文件

services 写“运行什么”,networks 写“通过哪个网络通信”,volumes 写“保留哪些数据”。Compose 按这份声明管理整个项目。

约 15 分钟 · 截至 2026-11-03,Docker Engine 29.x · 实验未实测 · 命令面向 Compose v2

一个项目连接两个服务和一个卷 项目 cfile 的 client 和 web 同在 cfile_default 网络。client 用 web:80 访问 web;宿主机用 127.0.0.1:8080 访问。web 从网络外独立保存的 cfile_site 卷读取文件。 项目 cfile 宿主机 :8080 cfile_default client 发起请求 web 监听 :80 web:80 转发到 :80 虚线框:共同网络 读取网页 cfile_site
图 1 服务通过网络互相访问,通过挂载使用卷;卷独立于容器保存。[2] [4]

基础回看:镜像与容器 · docker run 的执行链路。本页所有 Docker 实验未实测;预期现象待维护者核验,未展示模拟终端输出。

把参数搬进配置树

命令变成服务

Compose 是定义和管理多容器应用的工具。项目(project)是一组由它一起管理的资源;服务(service)是创建容器的配置,本例每个服务运行一个容器。[1]

左边逐条创建资源,右边描述同样的运行配置。为避免名称冲突,左边用 runlab 前缀,右边用项目名 cfile。先读对照;动手时只执行后面的实验。

逐条执行 docker run

docker network create runlab_default
docker volume create runlab_site

docker run -d --name runlab-web \
  --network runlab_default \
  --network-alias web \
  -p 127.0.0.1:8080:80 \
  -v runlab_site:/usr/share/nginx/html:ro \
  nginx:alpine

docker run -d --name runlab-client \
  --network runlab_default \
  --network-alias client \
  busybox:1.37 sleep 86400

网络别名 --network-alias web 让同网络的容器能用 web 找到它;第二个容器保持运行,供实验发请求。此对照未实测。

保存为 compose.yaml

name: cfile

services:
  web:
    image: nginx:alpine
    ports:
      - "127.0.0.1:8080:80"
    volumes:
      - site:/usr/share/nginx/html:ro
  client:
    image: busybox:1.37
    command: ["sleep", "86400"]

networks:
  default: {}

volumes:
  site: {}

{} 表示使用默认配置。default 是 Compose 的默认网络键;这里把通常可以省略的声明写出来,方便看清结构。[3]

YAML 用缩进表达层级:用空格,不用 Tab。web、client 缩进在 services 下;image 再缩进一层。- 开头表示列表项,端口字符串加引号。

docker run 的部分Compose 的位置含义
nginx:alpineservices.web.image用哪个镜像
-p 127.0.0.1:8080:80services.web.ports宿主机回环地址的 8080 → 容器的 80
-v …:/usr/share/nginx/html:roservices.web.volumes挂载数据,ro 表示只读
镜像后的 sleep 86400services.client.command覆盖镜像默认命令,等待 86400 秒
-ddocker compose up -d后台运行,属于这次启动命令的选项

Compose 会按项目生成容器名,无需照搬 --name。服务名写在 services 下即可。镜像标签引用自官方镜像页;标签可能更新,本例不承诺镜像大小或具体构建版本。[10]

把配置放回正确的层级。顶层声明资源,服务内说明怎样使用它。

  • 声明 site 卷
  • 发布 8080:80
  • command: ["sleep", "86400"]
  • 声明 default 网络
  • 挂载 site 到网页目录
项目顶层
services.web 内
services.client 内

顶层 networks、volumes 定义资源;服务配置决定谁使用它们。容器启动参数集中到对应服务下,-d 留在启动命令中。

深入一点:为什么没写 version?

现代 Compose 按 Compose Specification 解析文件。旧教程中的 version: "3.8" 已过时,并不用于选择解析规则;本例直接省略。name: cfile 指定项目名,命令行 -p 或环境变量 COMPOSE_PROJECT_NAME 可以覆盖它。本实验不使用这两种覆盖。[5]

名字和端口都带作用范围

同网用服务名

没有显式指定其他网络的服务,会加入项目的 默认网络(default network)。本例项目名为 cfile,网络名默认是 cfile_default,由 Compose 创建。[2]

域名系统(Domain Name System,DNS)在这里把服务名解析成容器地址。同一网络里的 client 可访问 http://web:80;你在宿主机访问 http://127.0.0.1:8080。即使移除 ports,这两个服务仍能走共同网络通信。

请求从哪里发出地址请求路径
client 容器内http://web:80服务名 → web 容器端口
Docker 所在宿主机http://127.0.0.1:8080宿主机发布端口 → web:80
client 容器内http://localhost:8080localhost 指 client 自己,本例没有监听

仍用页首的图:从 client 请求 web,填入主机名和端口。

http://:

同网络内用服务名 web 和容器端口 80。8080 是宿主机入口,容器之间不需要绕行这个入口。

深入一点:服务名、容器名、hostname

“服务名就是主机名”指的是同网络中可用于访问的 DNS 名。它不表示在容器内运行 hostname 一定打印 web。hostname: 是另一个可单独配置的属性;container_name: 则是容器对象的名字。本页都无需设置。容器重建后 IP 可能变化,访问者应继续用服务名查找。[1] [2]

只把端口映射改成 "127.0.0.1:9090:80"。client 该访问哪里?

9090 是宿主机发布端口,web 容器仍监听 80。

client 中的 localhost 回到 client 自己。

映射变化只改变宿主机入口。同网络内仍直接访问 web:80,并不经过宿主机的 9090。

深入一点:自定义网络也要“声明 + 连接”

需要明确命名时,可把原文件中的 networks 改成下面三处;其他服务字段照旧。只声明 app 不会自动把所有服务接入它;显式选择 app 的服务也不会自动再加入 default。[3]

services:
  web:
    # image、ports、volumes 保持原样
    networks: [app]
  client:
    # image、command 保持原样
    networks: [app]
networks:
  app: {}

这只是局部修改示意,不是可独立启动的完整文件。只有共享网络的服务才能用该网络上的服务名互通;本例实际创建的网络名是 cfile_app。

同名字段,两个层级

卷要声明和挂载

命名卷(named volume)是由 Docker 管理、独立于容器生命周期的数据存储。顶层 volumes.site 声明卷;services.web.volumes 把它挂载到容器目录。少了挂载,web 就用不到这个卷。[4]

声明卷和挂载卷的区别 顶层 site 声明对应 cfile_site 卷;web 服务内的 volumes 把这个卷接入 /usr/share/nginx/html,ro 表示 web 只读。client 没有这个挂载。 顶层 volumes.site 创建或复用 cfile_site 卷 挂载,ro 只读 web 容器内 /usr/share/nginx/html
图 2 site:/usr/share/nginx/html:ro 把项目卷接到 web 的目录上,供它只读访问。

默认情况下,项目名会作为卷名前缀:配置键 site 对应实际卷 cfile_site。docker compose down 删除容器和项目网络,默认保留命名卷;再次 up 会复用它。[4] [7]

你只在顶层写了 volumes: {site: {}},删掉 web 内的 volumes。web 还能从这个卷读网页吗?为什么?

不能。顶层只声明资源,没有把卷接到 web 的文件系统。要读取它,web 仍需要 volumes: ["site:/usr/share/nginx/html:ro"]。另外,ro 只限制这次挂载,不会把整个卷永久变成只读。

深入一点:site 和 ./site 不一样

site:/usr/share/nginx/html 使用命名卷,需要顶层声明;./site:/usr/share/nginx/html 使用绑定挂载(bind mount),把宿主机目录接进去,不需要顶层卷声明。单文件实验中,相对路径从 compose.yaml 所在目录解析。别为了修缩进随手加 ./,那会改变存储类型。[1]

只运行这一条实验路线

启动到清理

目标:写入一份网页,让两个入口都能读取;删除容器后,再验证卷中的文件仍在。需要 Linux 容器模式、可用的 Docker 与 Compose v2,以及拉取镜像的网络。命令在 Docker 所在的本机终端运行。

宿主机影响 · 未实测

实验会拉取镜像、创建项目网络和卷,并发布本机 127.0.0.1:8080。Linux Engine 的桥接网络和端口发布可能由 Docker 调整宿主机防火墙规则;Docker Desktop 在其虚拟机中管理容器网络。请使用空闲端口及未使用的 cfile 项目名。不需 --privileged,也不需手工改 iptables。

项目实验的六个阶段 按顺序准备配置、up 创建、ps 检查、发送请求并读 logs、down 删除容器和网络、重新 up 验证命名卷仍在。高亮框显示当前阶段。 启动 检查 请求 清理 重建 准备 compose.yaml up -d ps 请求 → logs down up -d → 再读
  1. 保存配置,用 config -q 校验 YAML。
  2. up -d 创建并启动服务,终端返回后容器继续运行;再向卷写入测试网页。
  3. ps 查看本项目的服务状态与发布端口。
  4. 发请求,再用 logs web 读取 web 的标准输出和错误输出。先预测:下一步 down 会删掉网页数据吗?
  5. down 删除项目容器和网络,命名卷保留。
  6. 再次 up -d 复用卷;再请求网页,验证内容保留。
图 3 四个常用命令管理项目生命周期;普通 down 的清理边界不包含命名卷。[6] [7] [8]

1. 保存并校验文件

新建空目录 compose-structure-lab 并进入。把前面的完整 YAML保存成 compose.yaml。后续命令都留在此目录,不运行左栏的 runlab 命令。

mkdir compose-structure-lab
cd compose-structure-lab
# 保存 compose.yaml 后执行;不设置项目名覆盖变量
docker compose config -q

预期(未实测):config -q 校验成功时无输出。它只校验配置,不启动服务。[9]

2. 启动、写入并检查

docker compose up -d
docker run --rm --network none -v cfile_site:/data busybox:1.37 sh -c 'printf "compose keeps this page\n" > /data/index.html'
docker compose ps
docker compose exec client wget -qO- http://web:80
curl http://127.0.0.1:8080
docker compose logs --tail 20 web

预期(未实测):ps 列出 web、client 正在运行,web 发布本机 8080。两次 HTTP 请求的正文都应等于刚写入 index.html 的那一行文本。web 日志应能找到访问记录;具体格式、时间和容器名以实机为准,待维护者补录。

先由 up 创建 cfile_site,再用临时容器挂载并写入网页。--rm 删除临时容器,命名卷仍在;web 的只读挂载不妨碍临时容器通过自己的可写挂载更新文件。[11]

exec client … 在已运行的 client 容器内执行命令;BusyBox 提供此处使用的 wget。curl 在宿主机运行,没有它可直接用浏览器打开地址。up -d 返回不保证应用已经就绪;若首次请求过早失败,先查看 ps、logs web,再重试。依赖与健康检查留给下一页。

docker compose logs -f web 可持续跟随日志,按 Ctrl+C 只退出日志查看;后台服务继续运行。ps -a 还会列出已停止的容器。[8]

3. 删除容器,验证数据

docker compose down
docker compose ps -a
docker run --rm --network none -v cfile_site:/data:ro busybox:1.37 cat /data/index.html
docker compose up -d
docker compose exec client wget -qO- http://web:80

预期(未实测):down 后 ps -a 不再列出本例服务容器;临时容器仍能读出你写入的文本。重新启动后的 HTTP 正文保持相同。若请求过早,同样先检查状态再重试。

最后清理:会删除实验数据

确认不再需要实验网页后,在此目录运行 docker compose down -v。这会删除容器、项目网络,以及文件中声明的 cfile_site 命名卷;镜像和本地 YAML 保留。此命令也未实测。不要把 -v 当成查看详情的参数。[7]

卡住时先定位哪一层
  • config -q 失败:检查缩进、冒号以及卷是否声明。
  • 8080 已被占用:把端口映射改为 "127.0.0.1:18080:80",重新 up -d;宿主机改访问 18080,client 仍访问 web:80。
  • 服务退出或网页不可读:用 ps -a 和 logs web 检查;确认初始化命令成功、卷名为 cfile_site。
  • 服务名无法解析:确认你在 client 内执行,并且两个服务连接同一网络。

离开前再检索一次

四条带走

你想删除本例两个容器和网络,同时下次继续使用原网页。哪条命令合适?

-v 会删除文件中声明的命名卷,原网页也随之丢失。

logs 只读取服务输出,不删除任何资源。

普通 down 删除服务容器和项目网络,保留命名卷。项目名仍为 cfile、挂载仍指向 site 时,下次 up 会复用它。

向同伴解释:为什么 YAML 顶层和 web 内都有 volumes,却不算重复?

顶层声明可用的命名卷,服务内把某个卷挂到自己的目录。一个回答“有什么资源”,另一个回答“谁在哪里使用”。

  • 提到顶层声明和服务内挂载的区别
  • 能以 site 与 /usr/share/nginx/html 为例
  • 能解释普通 down 后数据为什么仍在

来源

官方文档核对日期:2026-11-03。Docker 命令均未实测;未给出容器 ID、日志时间等虚构记录。

  1. Docker Docs:Services — 服务结构、image、command、ports、hostname、挂载语法。
  2. Docker Docs:Networking in Compose — 默认项目网络、服务名解析与容器端口。
  3. Docker Docs:Networks — 隐式 default 与显式网络连接。
  4. Docker Docs:Volumes — 顶层卷声明和服务挂载、复用现有卷。
  5. Docker Docs:Version and name、Compose CLI — version 过时、项目名与覆盖优先级。
  6. Docker Docs:docker compose up — 创建、重建和后台运行。
  7. Docker Docs:docker compose down — 默认清理范围、-v;external 资源不被删除。
  8. Docker Docs:ps、logs、exec — 状态、日志与容器内执行。
  9. Docker Docs:config — 校验并展开配置;-q 只校验。
  10. NGINX 官方镜像、BusyBox 官方镜像 — nginx:alpine 和 busybox:1.37 标签与用法。
  11. Docker Docs:Engine volumes — 命名卷生命周期、只读挂载与临时容器。
  12. Docker Docs:Packet filtering and firewalls — Linux bridge 网络和端口发布的防火墙规则。