Docker Compose

多容器协同编排——用一个 YAML 描述整个应用栈:服务、网络、数据卷、环境配置

阶段一 · 07 · 返回阶段大纲

前置回顾

第 05 篇讲过:容器之间默认可以通过 docker0 网桥互通,但 docker run 只能管一个容器。第 01 篇讲过:veth pair + bridge 组网是需要手动配置的。Compose 的设计目标就是把这些手动操作自动化——你只需要声明"有哪些服务、它们怎么关联",Compose 替你创建网络、启动容器、配置 DNS。

为什么需要 Compose

一个典型的 Web 应用通常不止一个容器:

一个博客系统需要:
  ├─ web 容器(处理 HTTP 请求)
  ├─ api 容器(业务逻辑)
  ├─ db 容器(PostgreSQL)
  └─ redis 容器(缓存)

如果用纯 docker run:
  # 1. 创建网络
  docker network create blog-net

  # 2. 启动 db(记住各种参数)
  docker run -d --name db --network blog-net \
    -e POSTGRES_PASSWORD=xxx \
    -v blog-pgdata:/var/lib/postgresql/data \
    postgres:16

  # 3. 启动 redis
  docker run -d --name redis --network blog-net \
    -v blog-redis:/data \
    redis:7-alpine

  # 4. 启动 api
  docker run -d --name api --network blog-net \
    -e DB_HOST=db -e REDIS_HOST=redis \
    myapp-api:latest

  # 5. 启动 web
  docker run -d --name web --network blog-net \
    -p 80:80 \
    myapp-web:latest

  # 停止时还要按顺序一个个关
  # 每次启动都要记住所有参数
  # 换一台机器要重敲所有命令

用 Compose,上面所有操作变成一个文件 + 一条命令:

# compose.yml —— 全部描述在这个文件里
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: xxx
    volumes:
      - pgdata:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    volumes:
      - redisdata:/data

  api:
    build: ./api             # 也可以用 Dockerfile 构建
    environment:
      DB_HOST: db            # 服务名直接当域名用
      REDIS_HOST: redis

  web:
    build: ./web
    ports:
      - "80:80"

volumes:                     # 声明命名卷
  pgdata:
  redisdata:

# 启动:docker compose up -d
# 停止:docker compose down
# 重建:docker compose up -d --build

docker run 是逐个拧螺丝——每启动一个容器你都要亲手指定网络、端口、环境变量、卷。Compose 是流水线——你把设计图纸(compose.yml)画好,一条命令整个生产线跑起来。图纸可以放进 git 版本控制,任何人拉下来一键启动。

核心概念与 YAML 结构

最小的 compose.yml 长什么样

# compose.yml 的顶层三大块
services:          # ← 容器 = 服务。一个 service = 一组相同的容器(可以扩缩容)
  web:             #   这个服务叫 "web"
    image: nginx   #   用什么镜像
    ports:         #   端口映射
      - "8080:80"

  api:             #   这个服务叫 "api"
    build: .       #   从当前目录的 Dockerfile 构建
    environment:   #   环境变量
      - PORT=3000

networks:          # ← 网络。不声明的话 Compose 自动创建一个 default 网络
  backend:

volumes:           # ← 数据卷。不声明的话 Compose 自动创建命名卷
  db-data:

docker compose vs docker-compose(V2 vs V1)

你可能会在教程里同时看到两种写法——它们不是两个等效命令,而是同一产品的两个大版本

V1 docker-compose(横杠)V2 docker compose(空格)
本质独立 Python 脚本,需 pip install 单独安装Docker CLI 内置插件(Go 重写),装 Docker Desktop 就自带
命令格式docker-compose up -ddocker compose up -d
维护状态2023 年 7 月停止维护活跃维护,当前标准
version 字段需要 version: "3.8"不再需要,可以删掉
depends_on condition部分支持完整支持 service_healthy
--profile不支持支持

本教程全部使用 V2(空格)。如果你的 docker-compose(横杠)报 command not found,说明机器上只有 V2,把横杠换成空格就行。目前最新版 Docker Desktop 会把两个命令都指向同一个插件,两者都能用,但习惯上用空格版。

docker compose up 背后做了什么

回顾第 05 篇的 docker run 全流程(dockerd → containerd → runc → kernel)。Compose 不是取代这个链——它是这个链的"批量调用管理器":

docker compose up -d 背后:

1. 解析 compose.yml → 整理出 services / networks / volumes
2. 创建 network(等于 docker network create xxx)
3. 创建 volume(等于 docker volume create xxx)
4. 按依赖顺序启动容器(depends_on 决定先后)
5. 每个容器走完整的 docker run 流程(dockerd → containerd → runc → kernel)

Compose 不是"容器运行时",它是"容器运行的编排器"。
它调用 Docker API 来管理多个容器,但它自己不创建任何 Namespace/Cgroup。

depends_on 的陷阱:启动顺序 ≠ 服务就绪

Compose 用 depends_on 控制容器启动的先后顺序:

services:
  api:
    depends_on:
      - db          # db 先启,api 后启
      - redis       # redis 先启,api 后启

但这里有一个很容易踩的坑:depends_on 只保证容器进程先启动,不保证容器内的服务已经 ready。来看一个典型故障场景:

docker compose up -d

时间线:
  T+0s:  db 容器启动 → postgres 进程开始运行
  T+1s:  api 容器启动(depends_on 已满足——db 进程在跑)
  T+1s:  api 连 db:5432 → 连接成功!执行 SQL:CREATE TABLE ...
  T+2s:  ❌ SQL 失败!

原因:
  postgres 进程虽然起来了,但数据库内部还在:
  • 从 WAL(预写日志)恢复数据
  • 回放上次未完成的 checkpoint
  • 初始化共享内存缓冲区
  → 这个过程可能持续 5-30 秒
  → 在此期间数据库不接受查询连接
  → api 以为 db 已经 ready,实际上还没有

解决方案一:healthcheck + depends_on condition(Compose v3.9+)

healthcheck 是 Docker 容器级别的健康检查机制。它定期在容器内执行一条命令,根据命令的返回值判断容器是否"真的就绪":

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: secret
    healthcheck:                          # ← 定义健康检查
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      #     │             │
      #     │             └── 执行这条命令,返回 0 = 健康,非 0 = 不健康
      #     └── 执行方式:CMD(直接执行)或 CMD-SHELL(通过 shell 执行)
      interval: 5s       # 每 5 秒检查一次
      timeout: 3s        # 单次检查超过 3 秒视为失败
      retries: 5         # 连续失败 5 次后标记为 unhealthy
      start_period: 10s  # 容器启动后 10 秒内不检查(给初始化留时间)

  api:
    depends_on:
      db:
        condition: service_healthy   # ← 等 db 的 healthcheck 通过后才启动 api
        #          │
        #          └── 可选值:
        #              service_started  (默认)容器启动就算
        #              service_healthy  容器的 healthcheck 通过才算
        #              service_completed_successfully (一次性任务成功退出才算)

healthcheck 的状态变化过程:

容器启动 → starting(start_period 内不检查)
         → 第一次检查 pg_isready 返回 1(还没好)→ 状态仍是 starting
         → 第二次检查 pg_isready 返回 1 → 仍是 starting
         → 第三次检查 pg_isready 返回 0 → healthy!← depends_on 条件满足,api 启动
# 查看容器的健康状态
docker compose ps
# NAME      STATUS
# db        Up 12s (healthy)       ← 通过了 healthcheck
# api       Up 3s

# 如果一直 unhealthy,查看原因
docker inspect <container> --format '{{json .State.Health}}' | python3 -m json.tool
# {
#   "Status": "unhealthy",
#   "FailingStreak": 5,
#   "Log": [
#     {"Output": "pg_isready: connection refused", "ExitCode": 2},
#     ...
#   ]
# }

常见服务的 healthcheck 写法

服务healthcheck 命令说明
PostgreSQL pg_isready -U postgres PostgreSQL 自带的就绪检测工具
MySQL mysqladmin ping -h localhost MySQL 自带的管理工具
Redis redis-cli ping 返回 PONG 说明就绪
HTTP 服务 curl -f http://localhost:8080/health 应用暴露 /health 端点,返回 200 即健康
通用 TCP nc -z localhost 8080 仅检查端口是否监听,不保证业务逻辑正常

解决方案二:应用层 retry(更健壮)

即使有 healthcheck,数据库也可能在运行中途挂了。所以 healthcheck + retry 是双保险,不是二选一

// 应用启动时,连接失败不要立刻崩溃
// 等几秒重试——无论 healthcheck 过没过,这层保护都值得加

func connectDB(dsn string) *sql.DB {
    for i := 0; i < 10; i++ {
        db, err := sql.Open("postgres", dsn)
        if err == nil && db.Ping() == nil {
            return db
        }
        time.Sleep(2 * time.Second)
    }
    panic("database not reachable after 10 retries")
}

服务间网络与 DNS

服务名就是域名——这是 Compose 最有价值的功能

回顾第 05 篇:Docker 用 docker0 网桥 + iptables 实现容器间通信。但容器之间怎么知道彼此的 IP?手动查 IP 再写到配置文件里?容器重启 IP 就变了。

Compose 的解决方案:内置 DNS 服务器。同一个 compose 文件里的所有容器自动加入一个默认网络,且 服务名自动成为 DNS 域名

services:
  api:
    build: ./api
    environment:
      DB_HOST: db           # ← 直接写服务名!
      DB_PORT: "5432"       # ← 不需要查 IP
      REDIS_HOST: redis     # ← redis 的 IP 也不需要

  db:
    image: postgres:16

  redis:
    image: redis:7

# api 容器内的进程访问 db:直接连接 db:5432
# DNS 解析过程:
#   api → 发起连接 "db:5432"
#   → 内置 DNS 查 "db" → 解析为 db 容器的内网 IP(如 172.18.0.3)
#   → 连接成功
#
# db 容器重启后 IP 变了(变成 172.18.0.4)?
#   → DNS 自动更新 → api 仍然连 "db:5432",无感知!

# 对比不用 Compose:
#   docker run --name db ...  → IP 可能每次不同
#   docker run --name api -e DB_HOST=172.17.0.3 ...  → IP 写死了,重启就挂

自定义网络:隔离不同业务组

services:
  api:
    networks:
      - backend          # ← api 只连接 backend 网
      - frontend         # ← 和 frontend 网

  db:
    networks:
      - backend          # ← db 只连 backend 网
                         #   前端流量永远到不了 db

  web:
    networks:
      - frontend         # ← web 只连 frontend 网

  # 用 docker compose exec api ping db     → 通(同 backend 网)
  # 用 docker compose exec api ping web    → 通(api 也接了 frontend)
  # 用 docker compose exec db ping web     → 不通(db 不在 frontend)

networks:
  frontend:
  backend:

Volume 与数据持久化

回顾第 04/05 篇:容器的可写层(upperdir)在容器删除后就被销毁。数据库的数据不能存在可写层——容器一删数据就没了。Volume 是独立于容器生命周期的存储,数据存在宿主机上,挂载到容器内的指定路径。

命名卷的完整生命周期

先看一个最简单的命名卷配置,然后跟踪它从创建到销毁的全过程:

services:
  db:
    image: postgres:16
    volumes:
      - pgdata:/var/lib/postgresql/data   # ← 命名卷
      # │      │
      # └── 卷名 └── 容器内路径

这个配置背后实际发生了什么?完整的生命周期:

1. docker compose up -d

   Compose 看到你引用了一个叫 pgdata 的卷
   → 检查宿主机上是否已存在此卷
   → 没有 → 自动 docker volume create
          → 在宿主机上创建目录:/var/lib/docker/volumes/项目名_pgdata/_data/
          → 注意:实际卷名会加上项目名前缀,避免不同项目冲突

2. db 容器启动时,Docker 做了一次 bind mount:

   mount --bind /var/lib/docker/volumes/项目名_pgdata/_data \
                /var/lib/postgresql/data

   → 容器内 /var/lib/postgresql/data 指向宿主机的 _data 目录
   → postgres 写入的所有数据,实际落到了宿主机上
   → 这些数据不在容器的可写层(upperdir)里!

3. docker compose down

   容器删除 → 可写层销毁
   ️但 /var/lib/docker/volumes/项目名_pgdata/_data 完好
   → 数据没丢!

4. docker compose up -d(重新启动)

   卷 pgdata 已存在 → 直接复用,不再创建
   → postgres 看到上次的数据全在
   → 这就是"持久化"的本质

5. docker compose down -v

   容器删除 + 卷也删除
   → /var/lib/docker/volumes/项目名_pgdata/ 整个目录被销毁
   → 数据永久丢失 ⚠️

用一张图理解关键:卷的数据在宿主机文件系统上,不在容器的 OverlayFS 里

宿主机                               容器内(merged 视图)

/var/lib/docker/volumes/             /
  myapp_pgdata/                      ├─ var/
    _data/  ← 数据真实物理位置        │  └─ lib/
      ├─ PG_VERSION                  │     └─ postgresql/
      ├─ base/                       │        └─ data/  ← 指向宿主机 _data
      └─ pg_wal/                     │           不是 upperdir!
                                     │
                              容器删了 → 这部分没了
                              但宿主机 _data 还在   → 数据完好
# 查看卷的详细信息(包括物理路径)
docker volume inspect myapp_pgdata
# {
#   "Name": "myapp_pgdata",
#   "Driver": "local",
#   "Mountpoint": "/var/lib/docker/volumes/myapp_pgdata/_data"
# }

顶层 volumes: 为什么"可选"

教程前面的例子里有顶层的 volumes: 声明:

volumes:                     # ← 这是顶层声明
  pgdata:
  redisdata:

"可选"的意思不是"写了和没写不一样"——而是 Compose 会自动补。 当你在 service 里写了 - pgdata:/var/lib/...,Compose 发现顶层没声明这个卷,它不报错,自动帮你创建,效果等于隐式补了顶层的 volumes: pgdata:。所以日常使用中写不写顶层声明效果完全相同

顶层声明的真正用途有两个场景:

# 场景一:引用宿主机上已有的外部卷
volumes:
  shared-data:
    external: true    # ← 告诉 Compose:"别新建!用已经存在的那个!"
                      #    不加这句,Compose 会新建一个名为 项目名_shared-data 的卷
                      #    那就不是你手动建的 shared-data 了

# 场景二:指定卷的物理存储位置
volumes:
  pgdata:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /mnt/fast-ssd/pgdata    # ← 让这个卷存到指定磁盘路径
      #       └── 比如你有一块 SSD 专门放数据库

如果你不需要这两个场景,顶层声明写了纯粹是为了让 compose.yml 读起来更完整——一眼看清这个项目依赖哪些持久化资源。效果上写不写都一样。

macOS 上怎么配最方便

macOS 的特殊性:Docker Desktop 跑在一个隐藏的 Linux 虚拟机里。 你本机没有 /var/lib/docker/——它只存在于 VM 内部。命名卷默认路径在 VM 里,从 macOS 终端和 Finder 都看不到。

┌─ macOS ───────────────────────────────────────────────┐
│                                                         │
│  /Users/yutong/  ← 这是你能看到的                        │
│                                                         │
│  ┌─ Linux VM ────────────────────────────────────────┐ │
│  │  dockerd + 所有容器在这里运行                       │ │
│  │  /var/lib/docker/volumes/  ← 你看不到这个路径      │ │
│  └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘

如果你希望数据直接出现在 macOS 的 ~/docker_data/ 下,有 两种方式

# 方式一:bind mount 用 ~(最简单,macOS 开发首选)
services:
  db:
    image: postgres:16
    volumes:
      - ~/docker_data/pgdata:/var/lib/postgresql/data
      # └── macOS 上 ~ 自动展开为 /Users/yutong
      #     docker compose up 后数据直接出现在 ~/docker_data/pgdata/

  redis:
    image: redis:7
    volumes:
      - ~/docker_data/redisdata:/data

# 优点:零额外配置,Finder 能直接看到文件
# 缺点:不是"命名卷",docker volume ls 看不到;路径写死了,换机器要改
# 方式二:命名卷 + .env 指定路径(推荐给想保留命名卷心智模型的人)
# compose.yml
services:
  db:
    image: postgres:16
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: ${DATA_DIR:-~/docker_data}/pgdata

# .env(和 compose.yml 同级,Compose 自动读取)
DATA_DIR=~/docker_data

# 优点:docker volume ls 可见;通过 .env 换机器只改一行
# 缺点:比纯 bind mount 多几行;底层还是 bind mount,性能没额外提升

你在 macOS 上学,用方式一就够。 数据就在 ~/docker_data/ 下,Finder 能看到,删了重建也方便。等部署到 Linux 服务器上再把路径去掉,让 Docker 自动管理。

macOS bind mount 的路径限制

bind mount 的宿主机路径必须是 Docker Desktop 已共享的目录(Settings → Resources → File Sharing)。默认共享:/Users/tmp/Volumes/private。所以 ~/docker_data/ 能用(在 /Users 下),但 /opt/data 会报错。另外 macOS 上 bind mount 比 Linux 原生慢——开发热更新够用,数据库生产不推荐。

bind mount — 开发环境用

services:
  api:
    build: ./api
    volumes:
      - ./api/src:/app/src          # ← bind mount
      #   │          │
      #   │          └── 容器内路径
      #   └── 宿主机相对路径(相对于 compose.yml 所在目录)
      #
      # 改了宿主机上的代码 → 容器里立刻生效(同一份文件)
      # 适合开发环境:改代码即刷新,不需要重新 build

      - /app/node_modules            # ← 匿名卷:保持容器内的 node_modules
      #   不映射到宿主机,防止被宿主机的空 node_modules 覆盖

# ⚠ bind mount 风险:
#  • 容器进程以 root 运行 → 可以修改宿主机文件
#  • 路径硬编码 → 换机器可能路径不对
#  • 生产环境应避免——应用应通过 docker build 打包进镜像,不依赖外部目录

命名卷 vs bind mount 速查

命名卷bind mount
写法pgdata:/var/lib/...~/data/pgdata:/var/lib/...
数据存在哪Docker 管理(/var/lib/docker/volumes/)你指定的宿主机路径
docker volume ls 可见
可迁移到其他机器✅(docker volume 导出)❌(路径硬编码)
权限管理Docker 自动处理宿主机什么权限就什么权限
典型场景生产数据库开发热更新

环境分离:开发 vs 生产

多 compose 文件叠加

这是 Compose 最优雅的设计之一。不同环境的差异用"覆盖文件"表达,基础配置放在主干文件:

# compose.yml(基础配置,所有环境共享)
services:
  api:
    build: .
    environment:
      NODE_ENV: production

  db:
    image: postgres:16
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:
# compose.dev.yml(开发环境覆盖)
services:
  api:
    build:
      target: dev              # 用 Dockerfile 的 dev stage
    environment:
      NODE_ENV: development
    volumes:
      - ./src:/app/src         # 代码热更新
    ports:
      - "3000:3000"

  db:
    ports:
      - "5432:5432"            # 开发时可以本地直连数据库

  # 开发环境额外加一个调试工具
  adminer:
    image: adminer
    ports:
      - "8080:8080
# compose.prod.yml(生产环境覆盖)
services:
  api:
    restart: always            # 崩溃自动重启
    deploy:
      resources:
        limits:
          cpus: "2"
          memory: 512M
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

  db:
    restart: always
    deploy:
      resources:
        limits:
          memory: 1G
# 使用方式:多个 -f 参数叠加
docker compose -f compose.yml -f compose.dev.yml up -d
# Compose 按顺序叠加:后面的文件覆盖前面同名配置

# 开发环境
docker compose -f compose.yml -f compose.dev.yml up -d --build

# 生产环境
docker compose -f compose.yml -f compose.prod.yml up -d

# 停止
docker compose -f compose.yml -f compose.dev.yml down

profiles:按需启停服务

核心规则:三条就够

  1. profile 名称完全自定义——debugtoolsmonitoringheavy,叫什么都可以,只要 YAML 里写的和命令行传的一致就行
  2. 没写 profiles: 的服务永远启动——它们属于隐式的"默认组",docker compose up 就跑
  3. 写了 profiles: 的服务只有显式开启才启动——不传对应的 --profile 就不会启动

基本用法

services:
  api:
    build: .                     # ← 没写 profiles → 永远启动

  db:
    image: postgres:16           # ← 没写 profiles → 永远启动

  adminer:                       # 数据库 Web 管理界面(调试用)
    image: adminer
    ports: ["8080:8080"]
    profiles:
      - debug                    # ← 只在传了 --profile debug 时启动

  prometheus:                    # 监控(吃资源,平时不跑)
    image: prom/prometheus
    profiles:
      - monitoring               # ← 只在传了 --profile monitoring 时启动
# 日常运行——只启动 api + db
docker compose up -d
# adminer 和 prometheus 不会启动,因为它们被 profile 标记了

# 调试时——多带一个 profile
docker compose --profile debug up -d
# 启动:api + db + adminer

# 性能排查时——带另一个 profile
docker compose --profile monitoring up -d
# 启动:api + db + prometheus

# 一次性带多个 profile
docker compose --profile debug --profile monitoring up -d
# 启动:api + db + adminer + prometheus

# 启动所有带 profile 的服务(* 表示全部)
docker compose --profile "*" up -d

一个服务可以属于多个 profile

services:
  adminer:
    image: adminer
    profiles:
      - debug          # ← 传 --profile debug 也启动
      - tools          # ← 传 --profile tools 也启动
      # 任意一个 match 就启动
# 这两种方式都能启动 adminer:
docker compose --profile debug up -d
docker compose --profile tools up -d

用环境变量预设 profile

有些 profile 你希望每次都生效(比如开发环境的调试工具),可以在 .env 里预设,省去每次敲 --profile

# .env
COMPOSE_PROFILES=debug,tools

# 现在直接 up 就等于带了 --profile debug --profile tools
docker compose up -d

常见用法:按重量级拆分

services:
  api:
    build: .

  db:
    image: postgres:16

  redis:
    image: redis:7

  # —— 以下按需启动 ——

  adminer:
    image: adminer
    profiles: [debug]

  mailhog:                        # 邮件调试
    image: mailhog/mailhog
    profiles: [debug]

  prometheus:
    image: prom/prometheus
    profiles: [monitoring]

  grafana:
    image: grafana/grafana
    profiles: [monitoring]

  elasticsearch:                  # 日志搜索(很吃内存)
    image: elasticsearch:8
    profiles: [heavy]

  kafka:                          # 消息队列(不常用)
    image: confluentinc/cp-kafka
    profiles: [heavy]

然后三个场景三条命令:

# 日常开发
docker compose up -d                              # api + db + redis

# 调试数据库
docker compose --profile debug up -d               # + adminer + mailhog

# 排查性能问题
docker compose --profile monitoring up -d          # + prometheus + grafana

# 全套(包括吃资源的)
docker compose --profile "*" up -d                 # + 全部

profile 本质上是给服务打标签,然后按标签过滤。没打标签的服务属于"默认组",永远启动。打了标签的服务是"按需组",只有你显式选了对应标签才启动。这和 docker compose -f compose.yml -f compose.dev.yml 的多文件叠加并不冲突——二者可以组合使用:文件叠加负责"哪些服务的配置不同",profile 负责"哪些服务要不要启动"。

生产环境注意事项

Compose 能用于生产吗?可以,但要知道它的边界:

场景Compose 够用?说明
单机部署Compose 的核心场景——一台机器上跑多容器应用
多机 / 集群Compose 不会跨节点调度容器——这是 K8s 的领域
零停机更新⚠️ 有限docker compose up -d --no-deps api 可以逐个更新,但没有滚动更新策略
健康检查healthcheck + depends_on: condition: service_healthy
日志聚合⚠️每个容器的日志仍独立,需要额外工具(如 Loki + Promtail)
Secrets 管理Compose 支持 secrets: 顶层声明(但文件模式,不如 K8s Secrets 完善)

小结

本篇要点回顾

要点一句话概括
Compose 的定位批量管理 docker run 的编排器——用 YAML 描述多容器应用,一键启动/停止
服务名 = DNS 域名同一 compose 的容器通过服务名互相发现,重启后 IP 变了自动更新
自定义网络不同 service 可以接入不同 network,实现流量隔离(db 不暴露给前端网络)
命名卷数据存于宿主机 /var/lib/docker/volumes/(macOS 在 Linux VM 内),独立于容器生命周期,down 不删、down -v 才删
顶层 volumes 声明日常使用中可选(Compose 自动补),需要引用外部已有卷或指定物理存储路径时才必须写
macOS 卷配置~/docker_data/pgdata:/var/lib/... 最简单;命名卷 + driver_opts: device: 可保留命名卷心智模型;纯命名卷在 VM 内,日常不可见
bind mount宿主机目录直通容器——开发环境神器(改了代码立刻生效),生产禁用
环境分离多文件叠加(-f compose.yml -f compose.dev.yml)让基础配置和差异配置解耦
profiles给服务打自定义标签,没标签的永远启动,有标签的只有 --profile 显式开启才启动;一个服务可属于多个 profile;可在 .env 用 COMPOSE_PROFILES 预设
生产边界Compose 适合单机、不适合多机编排;没有滚动更新、自动扩缩容——那是 K8s 的领域

动手练习

  1. 把你之前写的那个简单 HTTP 服务(06 篇的练习)改造成一个 Compose 项目:api 服务 + redis 服务(缓存),验证服务间能通过服务名互相访问
  2. 在 compose 中加入 healthcheck,用 docker compose ps 观察健康状态的变化
  3. 创建 compose.dev.yml,用 bind mount 挂载源码目录,改代码后验证热更新;再创建 compose.prod.yml,加上 restart 策略和资源限制
  4. 把 06 篇的多阶段构建 Dockerfile 作为 compose 中某个 service 的 build 源,验证 docker compose up --build 会重新构建镜像
  5. docker compose exec <service> sh 进入运行中的容器,测试 ping 另一个服务名,验证 DNS 解析
  6. 思考题:如果 api 服务启动时需要数据库表已创建好,depends_on 能解决吗?如果不能,有哪些方案?(提示:回顾之前讨论过的"启动顺序 ≠ 服务就绪")