Docker 基础体系 · 第 53/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。

Compose 服务发现与依赖:DNS、Healthcheck、启动顺序和重连

在 Compose 中,“应用能否连接数据库”至少包含四个彼此独立的问题:

  1. 应用是否能通过服务名解析出数据库地址;
  2. 解析出的地址是否对应当前仍在运行的容器;
  3. 数据库进程是否已经启动并能够接受连接;
  4. 应用在连接断开、容器重建或地址变化后,是否会重新解析并建立连接。

depends_on 主要处理第 2、3 个问题中的启动协调,DNS 处理第 1 个问题,应用自身的连接管理处理第 4 个问题。它们不能相互替代。


一、先建立模型:Compose 中的服务发现链路

假设 Compose 项目包含两个服务:

services:
  api:
    image: example/api
    depends_on:
      - db

  db:
    image: postgres:16

如果没有额外配置,Compose 通常会创建一个项目级默认网络,并把 apidb 连接到这个网络。此时 api 容器内部可以使用:

db:5432

访问数据库。

这里的 db 不是宿主机上的 DNS 记录,也不是数据库容器的固定 IP,而是 Compose 在同一用户定义网络中提供的服务名解析。

一次典型连接过程可以抽象为:

应用调用 getaddrinfo("db", 5432)
        │
        ▼
容器内 /etc/resolv.conf 指向 Docker 嵌入式 DNS
        │
        ▼
Docker 根据当前网络成员关系返回 db 对应的容器 IP
        │
        ▼
应用向该 IP 的 5432 端口建立 TCP 连接
        │
        ▼
数据库进程完成协议握手并接受认证

其中每一步都可能单独失败:

  • DNS 失败:db 无法解析;
  • 网络失败:解析成功,但目标 IP 不可达;
  • 监听失败:数据库容器存在,但 PostgreSQL 尚未监听 5432;
  • 协议或认证失败:TCP 已连通,但数据库尚未完成初始化,或账号密码错误;
  • 运行时失效:连接建立后数据库重启,原有连接断开。

因此,“容器已启动”不是“服务已可用”的同义词。


二、服务发现:服务名、网络别名和容器 IP

1. 服务名是 Compose 内部连接的稳定入口

在同一个 Compose 网络中,服务可以通过服务名互相访问:

services:
  api:
    image: example/api

  db:
    image: postgres:16

应用应配置:

DB_HOST=db
DB_PORT=5432

而不是:

DB_HOST=127.0.0.1
DB_PORT=5432

容器中的 127.0.0.1 只表示当前容器自身的 loopback 接口。api 容器中的 127.0.0.1:5432 指向 api 自己,而不是 db

也不应把宿主机映射端口用于容器间通信:

services:
  db:
    image: postgres:16
    ports:
      - "15432:5432"

这里:

  • 宿主机访问数据库使用 localhost:15432
  • 同一 Compose 网络中的其他容器仍应使用 db:5432

ports 是宿主机与容器之间的发布规则,不是服务发现配置。容器间通信直接走 Docker 网络和容器端口,通常不经过宿主机发布端口。

2. 服务名解析到网络地址,而不是永久 IP

Compose 不保证某个服务名永久对应某个 IP。容器被重建后,IP 可能发生变化:

第一次:
db -> 172.20.0.3

db 容器重建后:
db -> 172.20.0.5

服务名仍然是 db,但底层地址可能变化。因此应用代码应保存服务名或主机名配置,而不是在配置文件中写死容器 IP。

错误示例:

environment:
  DB_HOST: 172.20.0.3

正确示例:

environment:
  DB_HOST: db

服务名稳定的是“发现入口”,不是“解析结果”。

3. 网络范围决定服务名是否可用

服务名只能在连接到相同网络、且允许彼此发现的容器之间使用。例如:

services:
  api:
    image: example/api
    networks:
      - backend

  db:
    image: postgres:16
    networks:
      - backend

networks:
  backend:

此时 api 可以解析 db

如果改成:

services:
  api:
    image: example/api
    networks:
      - frontend

  db:
    image: postgres:16
    networks:
      - backend

networks:
  frontend:
  backend:

两个容器没有共同网络,api 中的 db 通常无法解析或无法连通。

一个服务可以加入多个网络:

services:
  api:
    image: example/api
    networks:
      - frontend
      - backend

  db:
    image: postgres:16
    networks:
      - backend

networks:
  frontend:
  backend:

这样 api 可以通过 backend 访问 db,但不应因此假设 frontend 上也存在 db

4. 网络别名是网络范围内的额外名称

可以为服务设置网络别名:

services:
  db:
    image: postgres:16
    networks:
      backend:
        aliases:
          - database
          - postgres-primary

networks:
  backend:

同一 backend 网络中的容器可以使用:

db
database
postgres-primary

这些名称指向同一个服务的网络端点。别名的可见范围由网络决定,不是全局 DNS 记录。

别名适合兼容既有配置,但不宜随意为多个服务使用同一个别名。多个网络端点或多个容器共享同一名称时,解析可能返回多个地址,应用必须能够处理多地址结果;不能把别名当作必然唯一的主机名。


三、Docker 容器内 DNS:解析请求实际经过什么路径

在 Linux 容器中,可以检查解析配置:

docker compose exec api cat /etc/resolv.conf

在 Docker 管理的用户定义网络中,常见结果包含:

nameserver 127.0.0.11
options ndots:0

127.0.0.11 是 Docker 为容器提供的嵌入式 DNS 地址。它负责:

  • 解析 Docker 网络中的服务名、容器名和网络别名;
  • 将无法由 Docker 网络回答的外部域名请求转发给上游 DNS;
  • 根据当前网络连接关系返回地址。

具体 resolv.conf 内容可能受 Docker 配置、宿主机 DNS、网络模式和运行时版本影响,因此应以容器内实际检查结果为准。

可以在容器中验证:

docker compose exec api getent hosts db

可能得到:

172.20.0.3    db

还可以验证端口连通性:

docker compose exec api sh -c 'nc -vz db 5432'

这两个命令验证的是不同层次:

  • getent hosts db:名称解析是否成功;
  • nc -vz db 5432:解析之后的 TCP 端口是否能建立连接。

如果镜像没有 getentnc,不能据此判断网络一定有问题。调试工具是否包含在业务镜像中是独立问题,可以临时使用带工具的调试容器加入同一网络。

ping 不是完整的服务可用性测试

下面的测试只能说明 ICMP 或名称解析相关路径:

ping db

它不能证明:

  • 数据库端口正在监听;
  • 数据库协议握手成功;
  • 数据库接受当前账号;
  • 应用执行查询时不会失败。

对于数据库,至少应验证实际服务端口,最好由数据库官方客户端执行一个轻量查询。


四、启动顺序:depends_on 到底保证什么

1. 没有依赖时,服务可以并行启动

Compose 会根据服务依赖构造有向图。若 apidb 没有依赖关系,Compose 可以并行创建和启动它们。

即使 YAML 中 db 写在 api 前面,也不能把文件顺序当作应用层启动顺序。YAML 的书写顺序不是可靠的依赖声明。

2. 短语法只表达“先启动依赖服务”

services:
  api:
    image: example/api
    depends_on:
      - db

  db:
    image: postgres:16

短语法表达的是:

创建并启动 db
        ↓
创建并启动 api

它通常不表达:

等待 db 完成初始化
等待 db 接受连接
等待 db 健康检查通过

因此,下列情况都可能发生:

  1. db 容器已经处于 running;
  2. PostgreSQL 进程仍在初始化数据目录;
  3. api 启动并立即连接;
  4. api 收到 connection refused、认证暂不可用或数据库未完成初始化;
  5. api 因一次失败直接退出。

depends_on 解决的是 Compose 的容器操作顺序,不会自动修改应用的连接重试策略。

3. 长语法可以声明健康条件

services:
  api:
    image: example/api
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 5s
      timeout: 3s
      retries: 12
      start_period: 10s
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: app-password
      POSTGRES_DB: app

这里的启动路径变为:

创建 db
  ↓
启动 db 容器
  ↓
执行 db 的 healthcheck
  ↓
状态从 starting 变为 healthy
  ↓
Compose 启动 api

condition: service_healthy 的前提是依赖服务确实配置了可工作的 healthcheck。没有健康检查时,Compose 没有可等待的健康状态。

Compose 规范还定义了其他条件:

depends_on:
  migration:
    condition: service_completed_successfully

它表示依赖服务应成功退出,适合一次性迁移任务。其退出码必须为 0;迁移失败时,依赖它的服务不应继续启动。

service_started 表示服务已启动,效果接近短语法;service_healthy 需要健康状态;service_completed_successfully 需要依赖容器成功结束。

4. 依赖图必须是可满足的有向图

如果定义:

services:
  api:
    depends_on:
      - db
  db:
    depends_on:
      - api

就形成了环:

api → db → api

Compose 无法对这样的依赖进行拓扑排序,启动过程会失败。实际系统应把依赖关系建模为有向无环图,或者把相互协作改成“双方均可重试”的运行时关系,而不是用循环启动依赖强行解决。

5. 停止顺序与启动顺序相反

对于:

api depends_on db

Compose 启动时通常先启动 db,停止时通常先停止 api,再停止 db。这样可以减少应用仍在使用数据库时数据库先被关闭的情况。

但这不等价于优雅关闭已经完成。应用仍需处理 SIGTERM,停止接收新请求、释放连接池,并在超时前退出。Compose 的停止协调不能替代进程自身的关闭逻辑。


五、Healthcheck:健康状态如何产生,又不能说明什么

1. 健康检查是容器内执行的命令

示例:

healthcheck:
  test: ["CMD-SHELL", "pg_isready -U app -d app"]
  interval: 5s
  timeout: 3s
  retries: 12
  start_period: 10s

健康检查命令在目标容器内部执行,而不是在宿主机上执行。因此:

  • pg_isready 必须存在于 PostgreSQL 镜像中;
  • 命令访问的 localhost 表示数据库容器自身;
  • 检查命令应使用容器内实际监听的端口和路径。

健康检查命令的退出码通常按以下方式解释:

  • 0:本次检查成功;
  • 1:本次检查失败;
  • 其他退出码:保留给运行时解释,通常也应视为失败,不应依赖未定义的特殊含义。

容器状态会经历类似过程:

starting
   │
   ├─ 检查成功
   │
   ├─ 连续失败达到 retries
   ▼
unhealthy

start_period 给服务预留启动宽限期。宽限期内的失败通常不会立即计入失败次数;一旦进入正常检查阶段,连续失败达到 retries,状态才会变为 unhealthy

2. 健康检查必须匹配真实就绪条件

对于 PostgreSQL:

test: ["CMD-SHELL", "pg_isready -U app -d app"]

主要检查数据库是否能够接受连接请求,但它不一定验证业务初始化是否完成。

如果服务启动还依赖迁移、表结构或特定配置,可以把检查设计为更接近应用需要的条件,例如:

healthcheck:
  test:
    [
      "CMD-SHELL",
      "pg_isready -U app -d app && psql -U app -d app -tAc 'SELECT 1' | grep -q 1"
    ]
  interval: 5s
  timeout: 5s
  retries: 12
  start_period: 15s

这仍然不是万能证明。一个 SELECT 1 成功,只说明当前数据库实例接受该连接和查询,不代表:

  • 所有业务表已存在;
  • 外部依赖可用;
  • 查询性能满足要求;
  • 应用执行的复杂事务不会失败。

健康检查的正确含义应是:“该服务满足某个明确、有限的就绪条件”,而不是“服务永远健康”。

3. unhealthy 不会自动重启容器

下面的配置:

healthcheck:
  test: ["CMD-SHELL", "some-check"]
  interval: 10s
  timeout: 3s
  retries: 3

只会更新容器的健康状态。它不会仅因为状态变为 unhealthy 就自动重启容器。

restart: unless-stopped 主要处理容器进程退出,不等价于“健康检查失败后重启”。如果确实需要根据健康状态执行自动修复,必须使用明确的外部控制器、监控系统或编排平台机制,并评估重启是否会掩盖配置和数据问题。

4. Compose 等待健康状态,不代表运行时持续编排

api 使用:

depends_on:
  db:
    condition: service_healthy

Compose 在本次启动流程中会等待 db 变为 healthy,再启动 api

但如果之后 db 变为 unhealthy,Compose 不会因此自动按依赖关系重启 api。应用必须能够处理运行时数据库不可用;健康检查也不应被误当作持续故障转移机制。


六、一个可运行的启动等待示例

下面的示例包含 PostgreSQL 和一个 Python 客户端。客户端只建立 TCP 连接,用于清楚展示 DNS、启动等待和运行时重连;它不是 PostgreSQL 协议客户端。

目录结构:

.
├── compose.yaml
└── app
    └── app.py

compose.yaml

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: app-password
      POSTGRES_DB: app
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 3s
      timeout: 2s
      retries: 20
      start_period: 5s

  api:
    image: python:3.12-alpine
    working_dir: /app
    volumes:
      - ./app:/app:ro
    command: ["python", "/app/app.py"]
    depends_on:
      db:
        condition: service_healthy

app/app.py

import socket
import time

HOST = "db"
PORT = 5432


def connect_once():
    # 每次尝试都重新解析 HOST,避免永久使用旧地址。
    addresses = socket.getaddrinfo(
        HOST,
        PORT,
        type=socket.SOCK_STREAM,
    )

    last_error = None

    for family, socktype, proto, _, sockaddr in addresses:
        sock = socket.socket(family, socktype, proto)
        sock.settimeout(3)

        try:
            sock.connect(sockaddr)
            print(f"connected to {HOST} at {sockaddr}", flush=True)
            return sock
        except OSError as exc:
            last_error = exc
            sock.close()

    raise last_error or OSError("no address succeeded")


delay = 1

while True:
    try:
        conn = connect_once()
        delay = 1

        # 示例只保持连接一段时间。
        # 真实应用应在这里执行协议握手、认证和业务请求。
        while True:
            time.sleep(5)
            print("connection is still open", flush=True)

    except OSError as exc:
        print(f"connection failed: {exc}; retry in {delay}s", flush=True)
        time.sleep(delay)
        delay = min(delay * 2, 30)

启动:

docker compose up

预期过程是:

  1. Compose 创建网络;
  2. 创建并启动 db
  3. PostgreSQL 初始化数据目录;
  4. 健康检查暂时失败或处于 starting
  5. PostgreSQL 就绪后,db 变为 healthy
  6. Compose 启动 api
  7. api 解析 db,连接 db:5432

查看状态:

docker compose ps

可能看到:

NAME        SERVICE   STATUS
project-db  db        Up ... (healthy)
project-api api       Up

查看健康检查的原始结果:

docker inspect \
  "$(docker compose ps -q db)" \
  --format '{{json .State.Health}}'

如果 api 没有启动,优先检查:

docker compose logs db
docker compose config

docker compose config 可以检查 Compose 文件展开后的结果,尤其适合发现环境变量替换、缩进和依赖条件错误。


七、为什么启动成功后仍然需要重连

1. 已建立的 TCP 连接不会随 DNS 自动迁移

假设应用第一次解析得到:

db -> 172.20.0.3

并建立 TCP 连接:

api:随机端口 → 172.20.0.3:5432

此时如果数据库容器被重建:

旧容器 172.20.0.3 被删除
新容器 172.20.0.5 创建
db -> 172.20.0.5

旧 TCP 连接仍然指向 172.20.0.3。DNS 记录变化不会修改已存在的 TCP 连接,也不会把连接自动转发到新容器。

应用必须:

  1. 检测连接已断开;
  2. 关闭失效连接;
  3. 重新解析 db
  4. 使用新地址建立连接;
  5. 重新认证、恢复连接池和必要的会话状态。

2. 只在进程启动时解析一次可能造成旧地址问题

下面这种实现存在风险:

DB_IP = socket.gethostbyname("db")

while True:
    connect(DB_IP, 5432)

如果 db 容器后来重建,DB_IP 仍然是旧地址。更合理的方式是:

每次建立新连接时解析服务名

或者使用支持服务名重新解析的连接池配置。但具体行为取决于语言运行时、DNS 缓存、数据库驱动和连接池实现,不能仅因为配置写成主机名,就假设驱动一定会在每次重连时重新解析。

3. 连接池可能隐藏部分失效状态

数据库连接池通常会保存多个长连接。数据库重启后可能出现:

  • 池中所有连接失效;
  • 部分连接因网络路径不同而仍可用;
  • 空闲连接直到下一次借用才暴露错误;
  • 一个事务中的连接断开,事务状态无法恢复。

因此重连逻辑不只是:

connect() 失败 → 再 connect()

还需要定义:

  • 哪些错误可以重试;
  • 当前事务是否可以安全重放;
  • 连接池如何丢弃坏连接;
  • 重试期间是否继续接收请求;
  • 重试次数和最大等待时间;
  • 数据库恢复后如何重新初始化会话参数。

已经提交成功但客户端未收到响应的写操作,不能简单重试,否则可能造成重复写入。可重试写操作通常需要幂等键、唯一约束或业务层去重。


八、重试策略:处理启动竞态和运行时故障

1. 线性重试与指数退避

固定间隔重试:

第 1 次:1 秒后
第 2 次:1 秒后
第 3 次:1 秒后

在多个副本同时启动时,可能形成同步请求洪峰。

指数退避可以写成:

dn=min(dmax,d0×2n)d_n = \min(d_{\max}, d_0 \times 2^n)

其中:

  • d0d_0 是初始等待时间;
  • nn 是已经失败的次数;
  • dmaxd_{\max} 是最大等待时间;
  • dnd_n 是第 nn 次失败后的等待时间。

实际工程中通常还加入抖动:

dn=U(0,dn)d'_n = U(0, d_n)

其中 U(0,dn)U(0,d_n) 表示在 0 到 dnd_n 之间随机选择等待时间。这样多个客户端不会在同一时刻再次冲击依赖服务。

但重试不能无限延长请求生命周期。应同时设置:

  • 单次连接超时;
  • 整体启动或请求超时;
  • 最大退避时间;
  • 可观测的失败日志;
  • 停止信号处理。

2. 启动重试与运行时重试不是同一件事

启动阶段:

DNS 尚未可用或数据库仍在初始化
→ 等待并重试

运行阶段:

数据库已服务数小时后重启
→ 连接池失效
→ 中断请求或事务
→ 丢弃连接
→ 重新解析和连接

运行时重连还要考虑数据一致性和请求语义,不能只复制启动脚本中的无限循环。

3. Compose 的 restart 不是应用重连

services:
  api:
    restart: unless-stopped

这通常用于容器主进程退出后的重启。它不能替代应用内的:

  • DNS 重新解析;
  • TCP 重连;
  • 连接池恢复;
  • 请求重试;
  • 事务恢复。

如果应用进程一直存活,但数据库连接已经失效,容器重启策略不会介入。

Compose 长语法中还可以写:

depends_on:
  db:
    condition: service_healthy
    restart: true

这里的 restart: true 是 Compose 在显式执行相关重启操作时的依赖重启联动语义,不等于数据库因自身进程退出或健康状态变化后,Compose 自动持续重启所有依赖服务。不能用它代替应用的运行时容错。


九、常见失败表现与分层诊断

情况一:could not resolve host: db

优先检查服务是否位于同一网络:

docker compose config
docker compose ps
docker network ls
docker network inspect <project>_default

也可以直接在客户端容器中检查:

docker compose exec api getent hosts db

可能原因包括:

  • apidb 没有共同网络;
  • 服务名拼写错误;
  • 使用了错误的 Compose 项目或网络;
  • 容器尚未加入目标网络;
  • 应用运行在 network_mode: hostnone 或其他特殊网络模式;
  • 应用自身没有使用容器的系统解析配置。

情况二:db 能解析,但 connection refused

执行:

docker compose exec api sh -c 'nc -vz db 5432'

如果名称解析成功但端口拒绝,常见原因是:

  • 数据库进程尚未监听;
  • 数据库只监听了错误的地址;
  • 容器端口配置错误;
  • 数据库进程已经退出;
  • 健康检查使用了错误的端口或账号;
  • 应用启动早于数据库就绪,且没有重试。

此时应查看:

docker compose logs db
docker compose ps

depends_on: condition: service_healthy 可以降低启动竞态,但健康检查命令本身错误时,依赖服务会一直等不到 healthy。

情况三:TCP 已连接,但应用仍然失败

这说明故障已经越过 DNS 和基础 TCP 层,可能发生在:

  • 数据库认证;
  • TLS;
  • 协议版本;
  • 数据库初始化脚本;
  • 业务表或迁移;
  • 连接池配置;
  • 查询或事务。

此时继续执行 ping 或反复检查 DNS 没有帮助,应使用数据库客户端或应用日志检查更高层协议。

情况四:容器重建后应用仍连接旧地址

常见原因是:

  • 应用缓存了 IP;
  • DNS 缓存时间过长;
  • 连接池继续借出旧连接;
  • 驱动没有在重连时重新解析;
  • 应用把解析结果写入了持久化配置。

验证新地址:

docker compose exec api getent hosts db
docker inspect "$(docker compose ps -q db)" \
  --format '{{json .NetworkSettings.Networks}}'

如果 getent 已返回新 IP,但应用仍失败,问题通常在应用的 DNS 缓存或连接池,而不是 Compose 服务发现本身。


十、不要把几个相似概念混为一谈

depends_on 不是服务注册中心

depends_on 只描述 Compose 项目内的启动和停止依赖,不提供:

  • 跨主机服务发现;
  • 负载均衡;
  • 故障转移;
  • 运行时健康实例摘除;
  • 全局服务注册。

Compose 的服务名解析适合单机 Docker 网络中的服务互联。跨主机、多节点调度、滚动发布和自动故障转移需要更强的编排或服务治理系统。

Healthcheck 不是监控系统

健康检查产生的是容器级状态。它可以帮助 Compose 判断是否满足启动条件,也可以被运维工具读取,但它不是完整的指标、日志、告警和追踪系统。

restart 不是重连机制

容器重启可能暂时掩盖应用错误,却不会保证:

  • 依赖服务已恢复;
  • 新 IP 已被重新解析;
  • 未完成事务得到正确处理;
  • 重复请求不会产生副作用。

服务名不是宿主机 DNS 名称

在宿主机上执行:

getent hosts db

不一定能解析 Compose 服务名。该名称的解析范围通常是连接到对应 Docker 网络的容器,而不是宿主机的普通 DNS 环境。

容器网络边界不是 Linux 主机网络的全部行为

本文讨论的是 Linux 容器使用 Docker 用户定义网络时的典型行为。Docker Desktop、Windows 容器、macOS 虚拟化层、host 网络、none 网络、手工配置 DNS 以及外部网络驱动可能改变具体结果。遇到版本或平台差异时,应结合:

docker version
docker compose version
docker inspect <container>
docker network inspect <network>

以实际运行环境为准。


十一、一个可靠连接模型的完整推导

对于依赖数据库的 api,较完整的正确性条件可以写成:

Ready(api)=Resolved(db)Reachable(db)Accepting(db)ProtocolReady(db)Retryable(api)Ready(api) = Resolved(db) \land Reachable(db) \land Accepting(db) \land ProtocolReady(db) \land Retryable(api)

其中:

  • Resolved(db):服务名 db 能解析到当前网络中的地址;
  • Reachable(db):该地址和端口可达;
  • Accepting(db):数据库进程正在监听并接受连接;
  • ProtocolReady(db):数据库已完成应用所需的协议、认证和初始化条件;
  • Retryable(api):应用能够在失败后重新解析、重新连接,并正确处理失败请求。

depends_on 最多直接帮助表达:

db 先于 api 启动
db 健康后 api 再启动

healthcheck 可以帮助近似表达:

ProtocolReady(db)

但前提是检查命令确实验证了这个条件。

Docker 嵌入式 DNS 帮助表达:

Resolved(db)

但它不保证目标进程正在监听。

最终的:

Retryable(api)

只能由应用自身实现。正因为这些条件属于不同层次,所以任何单独一个配置项都不能解决服务发现、启动竞态和运行时断连的全部问题。

在 Compose 中,合理的结构通常是:

services:
  api:
    depends_on:
      db:
        condition: service_healthy

  db:
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]

再配合应用侧:

使用服务名而非固定 IP
每次新建连接时允许重新解析
连接失败时使用有限的退避重试
失效连接从连接池中移除
谨慎重试可能产生副作用的请求

这样才能把 Compose 的静态启动协调、Docker 的网络服务发现和应用的动态故障恢复连接成一个完整闭环。


系列导航与关联阅读

官方资料

本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。