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` 的区别必须分清](#十四、COPY 和 volumes 的区别必须分清)
    • [十五、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:定义这一组服务
  • web、cache:服务名
  • 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,容器中的网页内容也会立即变化。

十四、COPY 和 volumes 的区别必须分清

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. 再按依赖关系启动容器

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

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

要特别记住:

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

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

更准确地说:

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

例如:

  • Dockerfile、requirements.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

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

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

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

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

本专栏文章导航

相关推荐
Peter-OK2 小时前
PostgreSQL学习笔记(入门-进阶-精通说明)
笔记·学习·postgresql
承渊政道3 小时前
Linux网络学习【Reactor反应堆模式】
linux·网络·学习·ubuntu·reactor
橙橙笔记4 小时前
具身智能的学习 - 完成一个机械臂 第1节
python·学习·机械臂·具身智能
成都蜀味缘餐饮技术培训学校4 小时前
手撕烤兔技术培训实战指南,秘制腌料配方比例与调制手法
大数据·经验分享·笔记·其他·微信公众平台
闲云自留地4 小时前
K8s 的记忆大脑!ETCD 集群部署、etcdctl 命令与备份恢复全通关
容器·kubernetes·etcd
讯捷蓝达5 小时前
戴尔R720xd服务器卡死在 “Configuring Memory...Done“,换了内存还是卡?问题根本不在内存
运维·服务器
希和20275 小时前
免费AI视频生成工具汇总(国内优先,适合做漫剧、短视频,Obsidian笔记版)
人工智能·笔记·音视频
蒸蒸yyyyzwd7 小时前
秋招学习笔记 day109
笔记·八股
小郎君6587 小时前
注解式脱敏:用 Jackson 的 ContextualSerializer 做字段级策略(一)
spring boot·spring cloud
爱和冰阔落7 小时前
【Linux网络】TCP 服务器如何并发处理多个客户端?从 fork、僵尸进程到双重 fork
linux·运维·服务器·网络