Docker 入门学习笔记 07:用一个多服务案例真正理解 Docker Compose

Docker 入门学习笔记 07:用一个多服务案例真正理解 Docker Compose

文章目录

  • [Docker 入门学习笔记 07:用一个多服务案例真正理解 Docker Compose](#Docker 入门学习笔记 07:用一个多服务案例真正理解 Docker Compose)
    • [一、Docker Compose 在解决什么问题](#一、Docker Compose 在解决什么问题)
    • 二、先分清两个最容易混的东西
      • [1. `docker compose`](#1. docker compose)
      • [2. `compose.yaml`](#2. compose.yaml)
    • [三、`docker compose up -d` 默认读取哪个文件](#三、docker compose up -d 默认读取哪个文件)
    • [四、最小 Compose 文件怎么写](#四、最小 Compose 文件怎么写)
    • [五、Compose 中服务名为什么能当主机名](#五、Compose 中服务名为什么能当主机名)
    • 六、宿主机访问服务,和容器之间访问服务,不是一回事
      • [1. 宿主机访问容器](#1. 宿主机访问容器)
      • [2. 容器访问容器](#2. 容器访问容器)
    • [七、Compose 里的环境变量怎么写](#七、Compose 里的环境变量怎么写)
    • [八、为什么服务连接配置通常写服务名,而不是 `localhost`](#八、为什么服务连接配置通常写服务名,而不是 localhost)
    • [九、`depends_on` 到底解决什么问题](#九、depends_on 到底解决什么问题)
    • [十、`healthcheck` 在解决什么问题](#十、healthcheck 在解决什么问题)
    • 十一、怎么让依赖服务等到健康后再启动
      • [1. 没有 `depends_on`](#1. 没有 depends_on)
      • [2. 只有 `depends_on`](#2. 只有 depends_on)
      • [3. `depends_on + condition: service_healthy`](#3. depends_on + condition: service_healthy)
    • [十二、Compose 的启动顺序和删除顺序](#十二、Compose 的启动顺序和删除顺序)
    • [十三、Compose 里的 `volumes` 怎么理解](#十三、Compose 里的 volumes 怎么理解)
    • [十四、`COPY` 和 `volumes` 的区别必须分清](#十四、COPYvolumes 的区别必须分清)
    • [十五、bind mount 和 named volume 有什么区别](#十五、bind mount 和 named volume 有什么区别)
      • [1. bind mount](#1. bind mount)
      • [2. named volume](#2. named volume)
    • [十六、为什么 named volume 要在最下面单独声明](#十六、为什么 named volume 要在最下面单独声明)
      • [服务里的 `volumes`](#服务里的 volumes)
      • [顶层的 `volumes`](#顶层的 volumes)
    • [十七、`docker compose up --build` 到底会做什么](#十七、docker compose up --build 到底会做什么)
    • [十八、`docker compose up --build` 每次都会重新 build 吗](#十八、docker compose up --build 每次都会重新 build 吗)
    • [十九、一个可完整复现的 Compose 综合案例](#十九、一个可完整复现的 Compose 综合案例)
      • [1. 目录结构](#1. 目录结构)
      • [2. `compose.yaml`](#2. compose.yaml)
      • [3. `app/requirements.txt`](#3. app/requirements.txt)
      • [4. `app/main.py`](#4. app/main.py)
      • [5. `app/Dockerfile`](#5. app/Dockerfile)
    • 二十、这个综合案例怎么运行
    • [二十一、为什么 `docker compose ps` 有时只看到 `cache`](#二十一、为什么 docker compose ps 有时只看到 cache)
    • 二十二、这一篇学完后应该真正掌握什么
    • 二十三、这一篇最值得记住的一句话
    • 二十四、下一步要学什么

本专栏文章导航

这一篇进入 Docker 学习中的下一个关键阶段:

从"会运行单个容器",进入"会声明式管理多个服务"。

前面已经学过:

  • 镜像和容器
  • 基础命令
  • 端口映射
  • 环境变量
  • Dockerfile

这些内容能解决"一个服务怎么跑"。

Docker Compose 要解决的问题是:

如果一个项目里不止一个容器,应该怎么统一组织、启动、停止和观察?

这正是微服务、本地开发环境和依赖服务联调中最常见的场景。

一、Docker Compose 在解决什么问题

如果没有 Compose,两个服务可能需要手动敲很多命令:

bash 复制代码
docker run -d --name mynginx -p 8080:80 nginx
docker run -d --name myredis redis:7

如果再加上:

  • 环境变量
  • 依赖关系
  • 健康检查
  • 自己构建的镜像

命令会越来越长,也越来越难维护。

所以 Compose 的核心价值可以概括成一句话:

把多服务的运行方式写进一个 YAML 文件,然后统一启动、统一停止、统一查看。

二、先分清两个最容易混的东西

1. docker compose

这是命令,例如:

bash 复制代码
docker compose up -d
docker compose ps
docker compose logs
docker compose down

2. compose.yaml

这是配置文件,例如:

yaml 复制代码
services:
  web:
    image: nginx:latest

所以要记住:

  • docker compose 是命令
  • compose.yaml 是配置文件
  • YAML 是文件格式

三、docker compose up -d 默认读取哪个文件

最常见的情况是:

docker compose up 默认读取当前目录下的 compose.yaml。`

例如:

bash 复制代码
cd ~/compose-demo
docker compose up -d

如果当前目录里就有:

bash 复制代码
compose.yaml

Compose 会直接使用它。

如果不在这个目录下,也可以显式指定文件:

bash 复制代码
docker compose -f /path/to/compose.yaml up -d

四、最小 Compose 文件怎么写

最小多服务示例可以是这样:

yaml 复制代码
services:
  web:
    image: nginx:latest
    ports:
      - "8080:80"

  cache:
    image: redis:7

这份文件里最重要的结构是:

  • services:定义这一组服务
  • webcache:服务名
  • image:服务使用哪个镜像
  • ports:端口映射

执行:

bash 复制代码
docker compose up -d

后,再执行:

bash 复制代码
docker compose ps

就能看到这组服务的运行状态。

五、Compose 中服务名为什么能当主机名

这是 Compose 最核心的网络概念之一。

当执行 docker compose up 时,Compose 通常会自动为这一组服务创建一个默认网络。

同一个 compose.yaml 里的服务,如果都在这个网络里,就可以:

  • 直接通过服务名互相访问
  • 把服务名当成内部网络里的主机名使用

例如:

  • web
  • cache

不只是配置里的名字,也可以作为网络里的可解析地址。

所以在 Compose 中,常见连接配置会写成:

  • REDIS_HOST=cache
  • DB_HOST=db
  • API_HOST=backend

这不是公网域名,而是:

Compose 默认网络里可被解析的服务名。

六、宿主机访问服务,和容器之间访问服务,不是一回事

这也是最容易混淆的地方之一。

1. 宿主机访问容器

例如:

bash 复制代码
curl http://localhost:8080

这是因为:

yaml 复制代码
ports:
  - "8080:80"

表示宿主机 8080 映射到容器的 80

2. 容器访问容器

如果另一个容器要访问 web,通常不需要写:

  • localhost:8080

而是直接写:

  • web:80

因为容器之间走的是 Compose 自动创建的内部网络,不走宿主机端口映射。

要特别记住:

在容器里,localhost 只表示当前容器自己,不表示别的服务。

七、Compose 里的环境变量怎么写

例如:

yaml 复制代码
services:
  helper:
    image: ubuntu:22.04
    command: ["sleep", "3600"]
    environment:
      APP_ENV: dev
      TARGET_SERVICE: web
      TARGET_PORT: "80"

这里的 environment 表示:

把环境变量传进容器。

进入容器后,可以用:

bash 复制代码
docker compose exec helper env

或者:

bash 复制代码
docker compose exec helper bash -c 'echo $APP_ENV'

去查看这些变量。

这一步非常重要,因为真实项目里服务之间的连接配置往往就是这样传进去的。

八、为什么服务连接配置通常写服务名,而不是 localhost

例如:

yaml 复制代码
environment:
  REDIS_HOST: cache
  REDIS_PORT: "6379"

这里写 cache 是因为应用要连接的是另一个服务,而不是自己。

所以:

  • REDIS_HOST=cache 是对的
  • REDIS_HOST=localhost 往往是错的

因为在容器里:

  • localhost 指向当前容器自己
  • 不指向同一个 Compose 项目中的其他服务

九、depends_on 到底解决什么问题

depends_on 用来声明服务依赖关系。`

例如:

yaml 复制代码
services:
  cache:
    image: redis:7

  app:
    image: redis:7
    command: ["sleep", "3600"]
    depends_on:
      - cache

这表示:

  • app 依赖 cache
  • Compose 会先启动 cache
  • 再启动 app

但这里一定要注意一个误区:

depends_on 主要解决启动顺序,不等于依赖服务已经完全就绪。

也就是说:

  • Redis 容器可能已经启动
  • 但 Redis 服务本身未必已经能接收连接

所以 depends_on 的价值是:

让启动顺序更合理。

而不是:

单独保证依赖一定已经可用。

十、healthcheck 在解决什么问题

既然"启动了"不等于"就绪了",就需要一种机制判断:

这个服务现在到底健康不健康,能不能真正提供服务?

这就是 healthcheck

例如 Redis 的健康检查可以写成:

yaml 复制代码
healthcheck:
  test: ["CMD", "redis-cli", "ping"]
  interval: 5s
  timeout: 3s
  retries: 5

这里的重点不是单纯看输出文字,而是看命令退出码:

  • 退出码 0:检查成功
  • 0:检查失败

所以:

  • PONG 是人类可读现象
  • 退出码才是 Docker 真正拿来判断成功与否的依据

如果连续失败达到 retries 指定次数,容器通常会被标记为:

  • unhealthy

但要注意:

unhealthy 不等于容器一定退出。

它可能仍然是运行中的,只是服务健康状态不正常。

十一、怎么让依赖服务等到健康后再启动

如果只是写:

yaml 复制代码
depends_on:
  - cache

表示先启动 cache,但不保证它已经健康。

如果想表达:

等 cache healthy 后,再启动 app

就可以写成:

yaml 复制代码
depends_on:
  cache:
    condition: service_healthy

这时要分清三种情况:

1. 没有 depends_on

  • 没有明确依赖关系

2. 只有 depends_on

  • 只表达启动顺序

3. depends_on + condition: service_healthy

  • 不仅有依赖顺序
  • 还要求依赖服务变成 healthy 后再启动当前服务

这一步是 Compose 中非常重要的进阶理解。

十二、Compose 的启动顺序和删除顺序

如果配置了:

yaml 复制代码
depends_on:
  cache:
    condition: service_healthy

则通常会看到:

  • 启动时先 cache
  • cache 健康
  • app

而在执行:

bash 复制代码
docker compose down

时,又常会看到:

  • 先删除 app
  • 再删除 cache

这并不是偶然,而是依赖关系的逆序处理。可以概括为:

启动通常按依赖正序,停止和删除通常按依赖逆序。

十三、Compose 里的 volumes 怎么理解

Compose 里的 volumes 本质上和前面学过的:

bash 复制代码
docker run -v 宿主机路径:容器路径

是一回事,只是改成了声明式写法。

例如:

yaml 复制代码
services:
  web:
    image: nginx:latest
    volumes:
      - ./site:/usr/share/nginx/html:ro

这和下面这条命令本质对应:

bash 复制代码
docker run -v "$(pwd)/site:/usr/share/nginx/html:ro" nginx

意思是:

  • 把宿主机当前目录下的 site/
  • 挂载到容器里的 /usr/share/nginx/html
  • :ro 表示只读

这样一来,本地修改 site/index.html,容器中的网页内容也会立即变化。

十四、COPYvolumes 的区别必须分清

COPY

表示:

构建镜像时把文件打进镜像。

volumes

表示:

运行容器时把外部目录或卷挂进容器。

所以这两者不是一个阶段的概念:

  • 构建阶段看 COPY
  • 运行阶段看 volumes

十五、bind mount 和 named volume 有什么区别

这两种都属于挂载,但来源不同。

1. bind mount

例如:

yaml 复制代码
volumes:
  - ./site:/usr/share/nginx/html:ro

左边是一个明确的宿主机路径,所以这是:

  • 本地目录挂载
  • 开发时改文件立刻生效

2. named volume

例如:

yaml 复制代码
services:
  cache:
    image: redis:7
    volumes:
      - redis_data:/data

volumes:
  redis_data:

这里的 redis_data 不是宿主机路径,而是一个 Docker 管理的命名卷。

它更适合:

  • Redis 数据
  • MySQL 数据
  • PostgreSQL 数据
  • 其他服务持久化数据

十六、为什么 named volume 要在最下面单独声明

这是 Compose 里另一个常见困惑。

例如:

yaml 复制代码
services:
  cache:
    volumes:
      - redis_data:/data

volumes:
  redis_data:

这里有两层含义:

服务里的 volumes

表示:

这个服务要挂载什么。

顶层的 volumes

表示:

这个 Compose 项目里定义了哪些命名卷。

所以可以记成:

  • 服务里的 volumes 是"使用它"
  • 顶层的 volumes 是"定义它"

而像下面这种 bind mount:

yaml 复制代码
- ./site:/usr/share/nginx/html:ro

因为左边已经是一个明确的宿主机路径,所以不需要在最下面再声明一次。

十七、docker compose up --build 到底会做什么

如果服务里写了:

yaml 复制代码
app:
  build: ./app

执行:

bash 复制代码
docker compose up --build

通常可以理解成两段:

  1. 先处理构建镜像
  2. 再按依赖关系启动容器

所以在一个包含 builddepends_on 的项目里,更合理的流程理解是:

  1. 先构建 app 镜像
  2. 再启动 cache
  3. cache healthy
  4. 再启动 app

要特别记住:

build 是准备镜像,depends_on 是启动容器。

十八、docker compose up --build 每次都会重新 build 吗

更准确地说:

  • 它每次都会进入 build 流程
  • 但不一定每次都从头重建所有层
  • Docker 会尽量复用构建缓存

例如:

  • Dockerfilerequirements.txt、源码没变时,很多层会显示 CACHED
  • 如果改了被 COPY 进去的代码或依赖文件,受影响的层就会重建

所以:

  • docker compose up --build:启动前执行构建检查,并按缓存规则重建
  • docker compose up:默认直接使用已有镜像,不主动重新构建

这也是为什么有时明明改了代码,但只执行 docker compose up 仍然看到旧逻辑,因为此时 Compose 可能直接用了旧镜像。

十九、一个可完整复现的 Compose 综合案例

下面这个案例把前面学过的内容串起来:

  • build
  • environment
  • depends_on
  • healthcheck
  • named volume

目标是:

一个 Python 应用启动后连接 Redis,写入一条数据,再读出来。

1. 目录结构

text 复制代码
compose-demo/
  compose.yaml
  app/
    Dockerfile
    requirements.txt
    main.py

2. compose.yaml

yaml 复制代码
services:
  cache:
    image: redis:7
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 3s
      timeout: 2s
      retries: 5

  app:
    build: ./app
    environment:
      REDIS_HOST: cache
      REDIS_PORT: "6379"
    depends_on:
      cache:
        condition: service_healthy

volumes:
  redis_data:

3. app/requirements.txt

txt 复制代码
redis==5.0.4

4. app/main.py

python 复制代码
import os
import time

import redis


redis_host = os.getenv("REDIS_HOST", "localhost")
redis_port = int(os.getenv("REDIS_PORT", "6379"))

print(f"Connecting to Redis at {redis_host}:{redis_port}")

client = redis.Redis(host=redis_host, port=redis_port, decode_responses=True)

for i in range(10):
    try:
        client.ping()
        print("Redis ping succeeded.")
        break
    except Exception as e:
        print(f"Redis not ready yet: {e}")
        time.sleep(1)
else:
    raise RuntimeError("Redis did not become ready in time.")

client.set("demo_key", "hello-compose")
value = client.get("demo_key")

print(f"Read from Redis: demo_key={value}")
print("App finished successfully.")

5. app/Dockerfile

dockerfile 复制代码
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY main.py .

CMD ["python", "main.py"]

二十、这个综合案例怎么运行

进入目录后执行:

bash 复制代码
docker compose up --build

如果一切正常,通常能看到:

  • cache 先启动
  • cache 变成 healthy
  • app 再启动
  • app 打印成功连接 Redis 的日志

可能看到类似输出:

text 复制代码
Connecting to Redis at cache:6379
Redis ping succeeded.
Read from Redis: demo_key=hello-compose
App finished successfully.

这条输出说明:

  • app 通过 environment 拿到了 Redis 地址
  • Redis 地址不是 localhost,而是服务名 cache
  • Compose 内部网络和依赖控制都生效了

二十一、为什么 docker compose ps 有时只看到 cache

在这个综合案例里:

  • cache 是长期运行服务
  • app 是一次性任务型服务

所以常见情况是:

  • cache 仍然运行中
  • app 执行完脚本后退出

这时执行:

bash 复制代码
docker compose ps

可能只明显看到:

  • cache

如果想看 app 的完整记录,更适合执行:

bash 复制代码
docker compose ps -a
docker compose logs app

二十二、这一篇学完后应该真正掌握什么

如果这一篇真正掌握了,应该能清楚说出这些内容:

  • compose.yaml 是多服务的声明式配置文件
  • docker compose 是操作这一组服务的命令
  • 同一 Compose 项目中的服务默认可以通过服务名互相访问
  • 容器内的 localhost 只表示当前容器自己
  • environment 用来把连接配置传进容器
  • depends_on 主要表达依赖顺序
  • healthcheck 用来判断服务是否真正健康
  • condition: service_healthy 可以让依赖方等待健康后再启动
  • bind mount 和 named volume 是两类不同来源的挂载方式
  • docker compose up --build 会执行构建流程,但通常会复用缓存

二十三、这一篇最值得记住的一句话

如果只记一句话,最值得记住的是:

Docker Compose 的核心不是"少敲几条命令",而是把多服务的镜像、网络、环境变量、依赖关系和存储方式统一写进一份声明式配置。

二十四、下一步要学什么

理解了 Docker Compose 之后,下一步最自然的延伸就是:

Kubernetes

因为到这里为止,已经学会了:

  • 单个容器怎么运行
  • 自己的镜像怎么构建
  • 多个服务怎么在一台机器上统一组织

接下来要进入的新问题就是:

如果这些服务不再只跑在一台机器上,而是要跑在一个集群里,应该怎么部署、扩缩容和恢复?

本专栏文章导航

相关推荐
孤影过客2 小时前
Linux下的PostgreSQL集群演进指南
linux·运维·postgresql
Arvin6272 小时前
Jenkins 任务执行完成后会kill掉的衍生进程
运维·servlet·jenkins
chh5632 小时前
从零开始学C++--类和对象
java·开发语言·c++·学习·算法
雄哥0072 小时前
linux redis升级⼿册-源码部署版
linux·运维·redis
總鑽風2 小时前
数据一致性springcloud+rabbitmq+mysql+redis
mysql·spring cloud·rabbitmq
HyperAI超神经2 小时前
【TVM教程】理解 Relax 抽象层
人工智能·深度学习·学习·机器学习·gpu·tvm·vllm
炽烈小老头2 小时前
【每天学习一点算法 2026/04/07】快乐数
学习·算法
水云桐程序员2 小时前
电子自动化技术(EDA技术)FPGA概述
运维·fpga开发·自动化
计算机安禾2 小时前
【数据结构与算法】第31篇:排序概述与插入排序
c语言·开发语言·数据结构·学习·算法·重构·排序算法