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)
- [1. `docker compose`](#1.
- [三、`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)
- [1. 没有 `depends_on`](#1. 没有
- [十二、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)
- [服务里的 `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) - 二十二、这一篇学完后应该真正掌握什么
- 二十三、这一篇最值得记住的一句话
- 二十四、下一步要学什么
本专栏文章导航
- 第 01 篇:Docker 入门学习笔记 01:它到底解决了什么问题,镜像和容器又是什么
- 第 02 篇:Docker 入门学习笔记 02:基础命令、前后台运行,以及 attach、logs、exec 的区别
- 第 03 篇:Docker 入门学习笔记 03:端口映射到底是什么,为什么容器启动了却访问不到
- 第 04 篇:Docker 入门学习笔记 04:环境变量到底在做什么,为什么很多容器都依赖它
- 第 05 篇:Docker 入门学习笔记 05:卷到底是什么,为什么容器删了数据却还能保留
- 第 06 篇:Docker 入门学习笔记 06:用一个可复现的 Python 项目真正理解 Dockerfile
- 第 07 篇:Docker 入门学习笔记 07:用一个多服务案例真正理解 Docker Compose
这一篇进入 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 里的服务,如果都在这个网络里,就可以:
- 直接通过服务名互相访问
- 把服务名当成内部网络里的主机名使用
例如:
webcache
不只是配置里的名字,也可以作为网络里的可解析地址。
所以在 Compose 中,常见连接配置会写成:
REDIS_HOST=cacheDB_HOST=dbAPI_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
通常可以理解成两段:
- 先处理构建镜像
- 再按依赖关系启动容器
所以在一个包含 build 和 depends_on 的项目里,更合理的流程理解是:
- 先构建
app镜像 - 再启动
cache - 等
cache healthy - 再启动
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 综合案例
下面这个案例把前面学过的内容串起来:
buildenvironmentdepends_onhealthchecknamed 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变成healthyapp再启动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
因为到这里为止,已经学会了:
- 单个容器怎么运行
- 自己的镜像怎么构建
- 多个服务怎么在一台机器上统一组织
接下来要进入的新问题就是:
如果这些服务不再只跑在一台机器上,而是要跑在一个集群里,应该怎么部署、扩缩容和恢复?
本专栏文章导航
- 第 01 篇:Docker 入门学习笔记 01:它到底解决了什么问题,镜像和容器又是什么
- 第 02 篇:Docker 入门学习笔记 02:基础命令、前后台运行,以及 attach、logs、exec 的区别
- 第 03 篇:Docker 入门学习笔记 03:端口映射到底是什么,为什么容器启动了却访问不到
- 第 04 篇:Docker 入门学习笔记 04:环境变量到底在做什么,为什么很多容器都依赖它
- 第 05 篇:Docker 入门学习笔记 05:卷到底是什么,为什么容器删了数据却还能保留
- 第 06 篇:Docker 入门学习笔记 06:用一个可复现的 Python 项目真正理解 Dockerfile
- 第 07 篇:Docker 入门学习笔记 07:用一个多服务案例真正理解 Docker Compose