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

Docker Compose 完整指南:服务、网络、卷、依赖、Profile 和生产边界

Docker Compose 是一种使用 YAML 描述多容器应用的工具和规范。它解决的核心问题不是“把多个 docker run 命令写短”,而是把应用的容器、网络、存储、配置、启动关系和生命周期放进一个可复现的声明式模型中。

本文以现代 Docker Engine、BuildKit 和 Compose Specification 为范围,示例默认运行 Linux 容器。Windows 容器、Docker Desktop 的虚拟化实现、Swarm 的 Stack 文件和 Kubernetes manifest 与 Compose 有交集,但不应混为同一个运行模型。


1. Compose 的对象模型:项目、服务、容器和资源

Compose 文件描述的是一个 project。项目通常由以下对象组成:

  • Service(服务):一个或多个同类容器的定义,例如 webdb
  • Container(容器):服务定义被实际启动后产生的运行实例。
  • Network(网络):服务之间通信的二层/三层隔离边界。
  • Volume(卷):独立于容器生命周期的持久化存储。
  • Config 和 Secret:向容器注入的配置文件或敏感信息。
  • Profile:选择性启用服务的条件分组。

例如:

services:
  api:
    image: example/api:1.2
  db:
    image: postgres:16

这里的 apidb 是服务,不是容器名。执行:

docker compose up -d

Compose 通常会创建:

项目_default       一个默认网络
项目_api_1         api 服务的一个容器
项目_db_1          db 服务的一个容器

实际容器名会受到项目名、服务名和实例序号影响。不要在应用中依赖自动生成的容器名;服务之间应使用服务名进行 DNS 解析。

项目名的来源通常可以通过以下方式控制:

docker compose -p demo up -d

也可以设置:

export COMPOSE_PROJECT_NAME=demo
docker compose up -d

项目名会影响默认网络名、命名卷名和容器名,因此 CI 环境应明确设置项目名,避免不同工作目录中的 Compose 项目互相冲突。


2. 一个可运行的完整示例

下面的示例包含:

  • db:PostgreSQL 数据库;
  • migrate:一次性数据库迁移任务;
  • adminer:数据库管理界面,仅在 tools Profile 中启用;
  • 命名卷 pgdata
  • 自定义网络 backendfrontend
  • 健康检查;
  • 服务依赖;
  • 环境变量和 Secret 文件注入。

目录结构:

compose-demo/
├── compose.yaml
├── .env
└── secrets/
    └── postgres_password.txt

compose.yaml

name: compose-demo

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: app
      POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
    secrets:
      - postgres_password
    volumes:
      - pgdata:/var/lib/postgresql/data
    networks:
      - backend
    healthcheck:
      test:
        [
          "CMD-SHELL",
          "pg_isready -U app -d app"
        ]
      interval: 5s
      timeout: 3s
      retries: 20
      start_period: 10s
    restart: unless-stopped

  migrate:
    image: postgres:16
    depends_on:
      db:
        condition: service_healthy
    environment:
      PGPASSWORD_FILE: /run/secrets/postgres_password
    secrets:
      - postgres_password
    volumes:
      - ./migrations:/migrations:ro
    networks:
      - backend
    entrypoint: ["/bin/sh", "-ec"]
    command: |
      export PGPASSWORD="$$(cat "$$PGPASSWORD_FILE")"
      psql \
        --host=db \
        --username=app \
        --dbname=app \
        --file=/migrations/001_init.sql

  adminer:
    image: adminer:4
    profiles: ["tools"]
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "${ADMINER_PORT:-8080}:8080"
    networks:
      - frontend
      - backend

volumes:
  pgdata:

networks:
  frontend:
  backend:

secrets:
  postgres_password:
    file: ./secrets/postgres_password.txt

.env

ADMINER_PORT=8080

初始化文件:

mkdir -p migrations secrets
printf 'change-me\n' > secrets/postgres_password.txt

cat > migrations/001_init.sql <<'SQL'
CREATE TABLE IF NOT EXISTS users (
    id BIGSERIAL PRIMARY KEY,
    email TEXT NOT NULL UNIQUE,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
SQL

先检查 Compose 最终解析结果:

docker compose config

这个命令会展示变量替换、合并和规范化后的配置。它适合在启动前发现缩进错误、变量为空、服务依赖写错以及挂载路径解析错误。

启动数据库和迁移任务:

docker compose up -d db
docker compose run --rm migrate

如果使用:

docker compose --profile tools up -d

Compose 会额外启动 adminer。浏览器访问:

http://localhost:8080

Adminer 中可以使用:

系统:PostgreSQL
服务器:db
用户名:app
密码:secret 文件中的内容
数据库:app

这里服务器地址必须写 db,不能写 localhost。因为 Adminer 容器中的 localhost 指向 Adminer 自己,而不是 PostgreSQL 容器。


3. 服务定义:镜像、构建和容器生命周期

3.1 imagebuild

使用现成镜像:

services:
  api:
    image: ghcr.io/example/api:1.4.0

使用本地 Dockerfile 构建:

services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        GO_VERSION: "1.22"
    image: example/api:local

context 是发送给 BuildKit 的构建上下文。构建上下文过大时,会增加构建时间并可能把无关文件、密钥或本地构建产物发送给构建器,因此应配合 .dockerignore

.git
.env
secrets/
node_modules/
dist/

build.args 只用于构建阶段的参数,例如选择基础镜像版本。它们不是运行时 Secret;把密码放进 ARG 会使密码进入构建记录、缓存或镜像历史的风险显著增加。

构建并启动:

docker compose build
docker compose up -d

现代 Docker Compose 通常使用 BuildKit,但具体构建能力仍取决于 Docker Engine、Buildx 和构建器配置。需要缓存、跨平台构建或私有仓库认证时,应单独验证构建器环境,而不能仅凭 Compose 文件推断。

3.2 commandentrypoint 和参数展开

services:
  worker:
    image: alpine:3.20
    entrypoint: ["/bin/sh", "-c"]
    command: ["echo worker started"]

entrypoint 决定容器的主进程入口,command 通常覆盖镜像默认 CMD。数组形式避免额外 shell 解析;字符串形式可能经过 shell 处理,变量、管道和信号行为也会随之变化。

Compose 自己会先进行 ${VAR} 插值。例如:

command: ["sh", "-c", "echo ${MESSAGE}"]

如果希望变量由容器内的 shell 展开,而不是由 Compose 在宿主机上展开,需要写成:

command: ["sh", "-c", "echo $${MESSAGE}"]

这是两个不同阶段:

Compose 解析阶段:${MESSAGE}
容器内 shell 阶段:$${MESSAGE} -> ${MESSAGE}

可用以下命令检查最终结果:

docker compose config

3.3 环境变量来源和泄漏边界

常见写法:

services:
  api:
    environment:
      LOG_LEVEL: "${LOG_LEVEL:-info}"
    env_file:
      - .env.api

environment 是显式声明的容器环境变量。env_file 是从文件批量加载环境变量。.env 还可能参与 Compose 文件的插值,但它不等于自动把所有变量注入容器。

例如:

LOG_LEVEL=debug
environment:
  LOG_LEVEL: "${LOG_LEVEL}"

这里 .env 的值先被 Compose 用于替换,最终 LOG_LEVEL=debug 才进入容器。

使用以下命令确认变量是否进入容器:

docker compose exec api printenv LOG_LEVEL

但不要对含敏感信息的完整配置执行:

docker compose config

因为某些 Secret 或环境变量可能出现在输出中。环境变量也可能被进程诊断、错误日志、容器检查信息或子进程继承,因此密码、令牌和私钥不应默认放在 environment 中。


4. Compose 网络:服务发现、端口发布和隔离

4.1 默认网络

如果没有声明 networks,Compose 会创建一个项目级默认网络:

<project>_default

同一个 Compose 项目中的服务默认加入该网络。Docker 内置 DNS 会把服务名解析为对应容器的内部 IP:

adminer -> db

因此连接数据库时:

主机:db
端口:5432

不需要使用数据库容器的 IP。容器 IP 可能因重建而变化,服务名是稳定的逻辑地址。

服务名解析的基本数据流是:

应用进程
  │ 查询 db
  ▼
Docker 内置 DNS
  │ 返回 db 容器在共享网络中的地址
  ▼
容器网络栈 -> db:5432

localhost 的含义始终是当前网络命名空间:

Adminer 容器中的 localhost  -> Adminer 自己
数据库容器中的 localhost    -> PostgreSQL 自己
宿主机中的 localhost         -> 宿主机

4.2 自定义网络和分层隔离

示例中的网络定义:

services:
  api:
    networks:
      - frontend
      - backend

  db:
    networks:
      - backend

networks:
  frontend:
  backend:

拓扑关系是:

flowchart LR
    Client[宿主机或外部客户端] -->|发布端口| API[api]
    API --> Frontend[frontend 网络]
    API --> Backend[backend 网络]
    Adminer[adminer] --> Frontend
    Adminer --> Backend
    DB[db] --> Backend

db 没有加入 frontend,因此前端网络上的服务不能直接通过 Docker 网络访问它。这里的隔离是网络连接边界,不是应用认证替代品;拥有宿主机 Docker 控制权的主体通常仍可改变网络配置或进入容器。

同一网络中的服务可以使用别名:

services:
  db:
    networks:
      backend:
        aliases:
          - postgres

此时同一网络中的服务可以用 dbpostgres 访问该容器。别名应保持唯一且有明确语义,否则多个容器共享别名时解析结果可能不符合预期。

4.3 portsexpose 和内部端口

services:
  adminer:
    ports:
      - "127.0.0.1:8080:8080"

格式为:

[宿主机 IP:]宿主机端口:容器端口

该配置表示:

仅宿主机回环地址:8080 -> adminer 容器:8080

如果写成:

ports:
  - "8080:8080"

通常会监听宿主机所有适用地址,实际暴露范围更大。端口映射只影响从宿主机或外部到容器的访问;同一 Docker 网络中的服务直接访问 adminer:8080,不需要经过宿主机映射。

expose 主要表达容器端口供其他容器使用的意图:

expose:
  - "8080"

它不会把端口发布到宿主机。Docker 网络中的实际可达性还取决于网络连接和应用自身监听地址。

常见反例:

services:
  db:
    ports:
      - "5432:5432"

如果只有 api 需要数据库,发布数据库端口会不必要地扩大攻击面。内部服务通常只加入后端网络,不发布端口。

4.4 网络故障排查

查看网络和容器:

docker compose ps
docker network ls
docker network inspect compose-demo_backend

检查 DNS:

docker compose exec adminer getent hosts db

检查 TCP 连通性:

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

如果镜像没有 getentnc,应使用镜像自带工具,或临时启动调试容器加入同一网络:

docker run --rm -it --network compose-demo_backend alpine:3.20 sh

网络故障应按顺序区分:

  1. 服务是否正在运行;
  2. 两个容器是否加入同一网络;
  3. DNS 是否把服务名解析到地址;
  4. 目标端口是否监听;
  5. 应用协议、认证和 TLS 是否正确;
  6. 是否误用了宿主机端口或 localhost

“能解析”只说明 DNS 成功,“能建立 TCP”也不说明数据库认证或应用协议成功。


5. 卷和挂载:容器生命周期之外的数据

容器的可写层属于容器本身。删除容器后,写入可写层的数据通常随容器消失。需要独立保存的数据必须使用挂载。

5.1 Named Volume:由 Docker 管理的卷

volumes:
  pgdata:

services:
  db:
    volumes:
      - pgdata:/var/lib/postgresql/data

pgdata 是命名卷,Docker 管理其实际存储位置。它适合数据库数据、上传文件和需要跨容器重建保留的数据。

查看:

docker volume ls
docker volume inspect compose-demo_pgdata

执行:

docker compose down

默认不会删除命名卷。执行:

docker compose down -v

会删除该 Compose 文件声明的命名卷,数据库数据也可能随之丢失。-v 不是普通清理参数,而是破坏性操作。

恢复前应先确认:

docker compose ps
docker volume ls
docker volume inspect compose-demo_pgdata

5.2 Bind Mount:宿主机路径直接映射

services:
  app:
    volumes:
      - ./src:/app/src:ro

Bind mount 的源是宿主机目录,适合开发时热加载代码、注入配置或导出结果。它与宿主机强耦合:

  • 路径必须存在或可被创建;
  • 权限由宿主机文件系统和容器 UID/GID 共同决定;
  • 容器可能修改宿主机文件;
  • 容器内原路径已有文件会被挂载内容遮蔽。

推荐对只读内容显式加 :ro

- ./config:/etc/app:ro

Linux 上还必须考虑 UID/GID:

stat -c '%u:%g %a %n' ./data
docker compose exec app id

如果容器进程以 UID 1000 运行,而宿主机目录由 UID 0 拥有且没有写权限,就会出现 permission denied。简单执行 chmod 777 会削弱权限边界,通常应调整目录所有者、组权限或容器运行 UID。

5.3 tmpfs:内存中的临时文件系统

services:
  app:
    tmpfs:
      - /tmp

tmpfs 中的数据不写入持久磁盘,容器停止或删除后消失,适合临时文件、缓存和不应落盘的中间数据。它不是绝对的“秘密保险箱”:内存可能被交换,且拥有足够权限的主机管理员仍能观察运行环境。

5.4 挂载遮蔽和初始化行为

如果镜像中已有:

/var/lib/app/default.conf

然后挂载一个目录:

volumes:
  - ./config:/var/lib/app

容器看到的是宿主机 ./config 的内容,镜像原有目录内容被遮蔽,而不是自动合并。这个机制经常导致“镜像里明明有默认文件,启动后却找不到”。

数据库镜像常在首次启动空数据目录时执行初始化脚本;一旦命名卷已有数据,后续重启通常不会再次初始化。更换环境变量或初始化 SQL 后,旧卷不会自动重置。

5.5 备份和恢复的正确边界

备份数据库时,优先使用数据库自己的逻辑备份工具:

docker compose exec -T db \
  pg_dump -U app -d app > backup.sql

恢复到空数据库:

cat backup.sql | docker compose exec -T db \
  psql -U app -d app

直接复制正在写入的数据库卷目录可能得到不一致快照,除非数据库和存储系统明确支持这种备份方式。卷备份不是数据库一致性备份的同义词。


6. 服务依赖:启动顺序不等于可用性

6.1 依赖图和三种条件

Compose 服务依赖形成有向图。若 api 依赖 db,可表示为:

db -> api

但“先启动 db”不等于“数据库已经能够接受连接”。Compose 长语法支持条件:

services:
  api:
    depends_on:
      db:
        condition: service_healthy

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

常见条件:

  • service_started:容器已经启动。这是短语法的基本语义,不能证明服务已就绪。
  • service_healthy:依赖服务的健康检查状态为 healthy。
  • service_completed_successfully:依赖的一次性任务以退出码 0 完成。

迁移任务可写成:

services:
  migrate:
    depends_on:
      db:
        condition: service_healthy

  api:
    depends_on:
      migrate:
        condition: service_completed_successfully

启动路径为:

创建并启动 db
  -> healthcheck 成功
  -> 运行 migrate
  -> migrate 退出码为 0
  -> 启动 api

如果迁移退出码非零,api 不应被认为满足该依赖条件。修复迁移文件后,重新执行迁移任务:

docker compose run --rm migrate
docker compose up -d api

6.2 健康检查必须测试“业务可用条件”

错误示例:

healthcheck:
  test: ["CMD", "true"]

它只说明容器内命令能执行,不说明数据库可连接。

更有意义的检查:

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

各字段的含义是:

  • interval:检查间隔;
  • timeout:单次检查超时;
  • retries:连续失败次数达到该值后标记不健康;
  • start_period:启动初期的宽限期。

健康检查命令必须存在于镜像中。精简镜像可能没有 curlwgetnc 或 shell,因此应选择镜像实际提供的客户端,或者在镜像中安装专用检查工具。

6.3 启动顺序仍不能替代重试

Compose 只在编排阶段等待条件满足。运行过程中数据库可能重启、网络短暂中断或连接池失效,因此应用仍应实现:

  • 初始连接重试;
  • 指数退避;
  • 请求超时;
  • 连接池重建;
  • 可恢复错误与不可恢复错误区分。

反例是把所有可用性逻辑都放在:

depends_on:
  db:
    condition: service_healthy

这只能减少初始启动竞态,不能解决整个运行期的故障恢复。

6.4 依赖环和关闭顺序

以下结构存在依赖环:

api -> db -> api

Compose 无法按拓扑顺序启动它。应将初始化逻辑移出循环,例如让数据库独立启动,应用在连接失败时重试。

停止时,Compose 通常会按依赖关系的反向顺序停止服务,以减少上游仍在使用已关闭下游的情况。但应用仍应正确处理 SIGTERM,完成连接关闭和未完成请求排空。强制 SIGKILL 会截断清理逻辑。


7. Profile:选择性启用服务

Profile 用于把调试工具、管理界面、性能测试或本地专用服务从默认启动集合中分离出来。

services:
  db:
    image: postgres:16

  adminer:
    image: adminer:4
    profiles: ["tools"]

没有 Profile 的服务默认启用;带 Profile 的服务只有在该 Profile 激活时才启用:

docker compose up -d

只启动默认服务。

docker compose --profile tools up -d

启动默认服务和 tools 服务。

也可以通过环境变量:

COMPOSE_PROFILES=tools docker compose up -d

Profile 影响的是 Compose 的服务选择,不是镜像构建标签,也不是网络隔离机制。

如果显式指定带 Profile 的服务:

docker compose up -d adminer

Compose 会把被指定的服务作为目标启动,并满足其依赖;这不等同于自动启动同一 Profile 下的所有服务。因此不能把“显式启动一个服务”理解为“激活整个 Profile”。

Profile 常见用途:

services:
  app:
    image: example/app

  debug-shell:
    image: alpine:3.20
    profiles: ["debug"]
    command: ["sleep", "infinity"]

  db-ui:
    image: adminer:4
    profiles: ["tools"]

开发时:

docker compose --profile debug --profile tools up -d

生产配置中不应仅依赖 Profile 隐藏高风险服务。生产部署还应使用独立配置文件、CI 校验和访问控制,避免某个命令参数误启用调试端口。


8. 配置组合、覆盖和最终配置验证

Compose 支持通过多个文件组合配置:

docker compose \
  -f compose.yaml \
  -f compose.dev.yaml \
  config

常见模式是:

compose.yaml       基础配置
compose.dev.yaml   本地开发覆盖
compose.prod.yaml  生产环境覆盖

覆盖规则不是简单的文本拼接,而是针对 Compose 模型合并。列表、映射和标量字段的合并行为应以 Compose Specification 为准;不要仅凭 YAML 文件视觉结构推测最终结果。

每次组合后都应检查:

docker compose -f compose.yaml -f compose.dev.yaml config

还可以检查服务实际配置:

docker compose ps
docker inspect compose-demo-db-1

使用相对路径时,多个 Compose 文件的路径通常按第一个文件所在项目目录解释,而不是按当前任意覆盖文件解释。为了避免不同工作目录导致挂载路径变化,建议从项目根目录执行命令,并在 CI 中明确 -f 文件和工作目录。


9. Compose 生命周期和常用命令

9.1 创建与启动

docker compose up -d

Compose 会根据配置创建网络、卷和容器,然后启动服务。配置或镜像变化时,它可能重建受影响的容器。

查看状态:

docker compose ps

查看日志:

docker compose logs -f db
docker compose logs --tail=100

进入运行中的容器:

docker compose exec db psql -U app -d app

exec 要求容器已经运行。

9.2 一次性任务

docker compose run --rm migrate

run 创建一次性容器,适合迁移、管理命令和脚本。它默认不会发布服务的 ports;如果确实需要发布端口,可使用:

docker compose run --rm --service-ports some-service

一次性任务是否启动依赖服务,受依赖配置影响。任务命令应保证退出码准确传递,否则 CI 可能把失败任务误判为成功。

9.3 停止、删除和清理

docker compose stop

停止容器但保留容器、网络和卷。

docker compose down

停止并删除由该项目创建的容器和网络,默认保留命名卷。

docker compose down -v

额外删除命名卷。执行前必须确认数据是否已备份。

docker compose down --remove-orphans

删除不再出现在当前配置中的孤儿容器。共享项目名或手工创建容器时,这个选项可能影响不属于当前部署意图的容器,因此要谨慎使用。

9.4 重建和强制重建

docker compose up -d --build

需要时重新构建镜像。

docker compose up -d --force-recreate

即使配置看起来未变,也重新创建容器。它不会自动删除卷。

使用:

docker compose config
docker compose ps
docker compose logs
docker volume inspect ...
docker network inspect ...

比直接执行 down -v 更适合排查问题;删除资源往往会消除现场证据。


10. 扩缩容、固定端口和实例身份

可以使用:

docker compose up -d --scale worker=3

这会为 worker 创建多个实例。服务内部通过 DNS 访问时,名称可能对应多个地址,客户端是否正确处理多地址取决于应用和解析行为。

以下配置会限制扩容:

services:
  worker:
    container_name: fixed-worker

固定 container_name 只允许一个同名容器,通常不应与服务扩容一起使用。

固定宿主机端口也会造成冲突:

services:
  api:
    ports:
      - "8080:8080"

扩展为多个实例时,每个实例都试图占用宿主机 8080,因此无法直接扩容。可以不发布固定端口,由反向代理或外部负载均衡访问内部服务;或者使用动态宿主机端口,但这需要额外的服务发现机制。

Compose 的服务扩容不是完整的高可用方案。单台 Docker 主机发生故障时,所有实例都会同时失效;多个实例也不自动提供跨主机调度、滚动升级、故障域隔离或持久卷编排。


11. 重启策略、健康状态和故障恢复

服务可以配置:

services:
  api:
    restart: unless-stopped

常见重启策略:

  • no:不自动重启;
  • always:退出后始终重启;
  • on-failure:非零退出时重启;
  • unless-stopped:除非被明确停止,否则退出后重启。

重启策略只处理“容器进程退出”这一类故障。进程卡死但仍然存在、应用返回错误、数据库连接全部失败但进程未退出,都不一定触发重启。

健康检查的 unhealthy 状态也不会自动等同于容器重启。要实现恢复,需要应用自身退出、外部监控处理,或使用能够根据健康状态执行调度决策的平台。

depends_on 也不会在运行期间持续重排整个应用。数据库重启后,Compose 不会因为依赖关系自动重启所有上游服务;应用必须自行重连,或由运维系统执行有意识的重启。


12. Secret、Config 和敏感数据边界

12.1 Secret 文件注入

Compose Secret 示例:

secrets:
  api_token:
    file: ./secrets/api_token.txt

services:
  api:
    secrets:
      - api_token

容器内通常以文件形式出现:

/run/secrets/api_token

应用读取:

token="$(cat /run/secrets/api_token)"

Secret 文件注入避免了把值直接写入环境变量,但在普通 Docker Compose 单机模式下,它不等于 Swarm Secret 那种集群级加密存储和轮换机制。源文件仍存在宿主机上,Docker 守护进程和拥有相应主机权限的主体仍可能读取它。

12.2 Config

非敏感配置可以使用 Config:

configs:
  app_config:
    file: ./config/app.yaml

services:
  api:
    configs:
      - source: app_config
        target: /etc/app/config.yaml
        mode: 0444

Config 适合配置文件,不适合密码。不同 Compose 实现对 configs 的底层处理可能不同,部署前应通过容器内路径和权限验证。

12.3 Secret 轮换

Secret 文件发生变化后,应用是否重新读取取决于应用实现。很多程序只在启动时读取一次,因此仅替换宿主机文件并不会让现有进程获得新值。

一种明确的轮换流程是:

写入新 Secret
  -> 校验文件权限和内容
  -> 重建或重启相关服务
  -> 验证新连接/新签名成功
  -> 撤销旧 Secret

不要把完整 Secret 输出到:

docker compose config
docker inspect
docker compose logs

也不要把 Secret 写入 Dockerfile 的 ARG、镜像层、Git 仓库或调试日志。


13. Compose 与生产环境的边界

Compose 可以用于单台主机上的生产部署,但“能运行生产服务”不等于“具备生产编排平台的全部能力”。

13.1 Compose 适合什么

Compose 适合:

  • 本地开发环境;
  • CI 中的集成测试;
  • 单台主机上的小型服务集合;
  • 明确由外部系统负责监控、备份、TLS、发布和故障处理的部署。

它能描述单机上的服务关系、网络、卷、环境和启动流程。

13.2 Compose 不自动提供什么

Compose 本身不自动提供:

  • 多主机调度;
  • 节点故障转移;
  • 滚动升级;
  • 跨故障域复制;
  • 集群级 Secret 加密和轮换;
  • 自动负载均衡;
  • 服务级 SLA;
  • 数据库高可用;
  • 一致性备份;
  • 完整的观测和告警系统。

例如:

deploy:
  replicas: 3

deploy 属于 Compose 规范中的部署相关模型,但具体字段是否由当前运行方式实现,取决于部署器。不要假设直接执行 docker compose up 就会获得 Swarm 或 Kubernetes 的完整副本调度、滚动升级和故障转移能力。对于本地 Compose CLI 支持的行为,应以当前版本文档和实际验证为准;跨平台交付则应使用目标平台原生配置。

13.3 生产部署至少要补齐的外部能力

生产边界通常需要:

Compose
  + 镜像版本固定和签名/扫描
  + 反向代理与 TLS
  + 日志收集和轮转
  + 指标、健康检查和告警
  + 数据库逻辑备份与恢复演练
  + 主机补丁和权限控制
  + Secret 管理与轮换
  + 发布、回滚和变更审计

镜像标签不应只写:

image: postgres:latest

latest 会随时间变化,导致同一 Compose 文件在不同时间解析到不同镜像内容。生产环境应至少固定明确版本,必要时固定到不可变摘要:

image: postgres:16@sha256:<digest>

摘要必须使用实际从可信仓库获取的值,不能手工猜测。


14. 常见误解与对应诊断

误解一:depends_on 保证数据库可用

默认短语法:

depends_on:
  - db

主要表达启动依赖,不保证数据库已经完成初始化或接受连接。应增加有效的 healthcheckservice_healthy,应用仍需实现重试。

误解二:容器可以通过 localhost 访问另一个容器

不可以。容器有独立网络命名空间。服务之间使用:

服务名:容器端口

例如:

db:5432

误解三:ports 是服务之间通信所必需的

不需要。共享 Docker 网络中的容器直接访问容器端口。ports 只在需要从宿主机或外部进入时使用。

误解四:docker compose down 会删除数据库

默认不会删除命名卷,但以下命令可能删除数据:

docker compose down -v
docker volume rm compose-demo_pgdata

Bind mount 则直接对应宿主机目录,删除容器通常不会删除源目录,但应用仍可能已经修改其中的数据。

误解五:健康检查失败会自动重启容器

不一定。健康状态和进程退出是两个不同状态。查看实际状态:

docker inspect \
  --format '{{json .State.Health}}' \
  compose-demo-db-1

查看进程是否退出:

docker compose ps

误解六:Profile 是安全边界

Profile 只是服务选择机制。被隐藏的 Adminer、调试端口或管理接口仍可能被显式启动。安全边界应由网络、认证、主机权限、防火墙和部署流程共同提供。

误解七:更新 Compose 文件会修改现有容器内部文件

Compose 会根据配置决定是否重建或更新容器,但挂载卷中的数据不会自动按新镜像迁移。数据库 schema、文件格式和应用版本之间的迁移必须由显式迁移任务或应用流程完成。


15. 一套可重复的排障顺序

当服务无法启动或无法通信时,按资源生命周期逐层检查。

第一步:验证解析后的配置

docker compose config

检查:

  • 变量是否为空;
  • 端口是否冲突;
  • 挂载源路径是否正确;
  • Profile 是否导致服务未启用;
  • 依赖条件是否引用了真实服务名。

第二步:查看服务和退出码

docker compose ps -a
docker compose logs --tail=200 db

退出码为非零时,先分析主进程为何退出,不要马上删除容器。

第三步:检查网络

docker network inspect compose-demo_backend

确认客户端和服务端都在该网络中,然后在客户端容器内测试:

docker compose exec adminer getent hosts db
docker compose exec adminer sh -c 'nc -vz db 5432'

第四步:检查健康检查本身

docker inspect \
  --format '{{range .State.Health.Log}}{{println .ExitCode .Output}}{{end}}' \
  compose-demo-db-1

健康检查命令可能因为用户名、数据库名、路径、权限或工具不存在而失败。健康检查失败不应直接解释为“应用故障”。

第五步:检查卷和权限

docker volume inspect compose-demo_pgdata
docker compose exec db id
docker compose exec db sh -c 'ls -ld /var/lib/postgresql/data'

Bind mount 则在宿主机检查:

ls -ld ./data
stat ./data

第六步:只在确认数据安全后重建

docker compose up -d --force-recreate

如果必须清除卷:

docker compose down -v

执行前先完成逻辑备份,并确认删除的是目标项目卷,而不是共享或手工创建的卷。


16. 一份最小但可靠的检查表

提交或部署 Compose 文件前,可以验证以下事实:

docker compose config
docker compose pull
docker compose build
docker compose up -d
docker compose ps
docker compose logs --tail=100

然后确认:

  • 服务之间使用服务名和容器端口通信;
  • 只有需要外部访问的服务使用 ports
  • 数据目录使用命名卷或明确的 Bind mount;
  • downdown -v 的数据影响已被理解;
  • depends_on 没有被误当成运行期高可用;
  • 健康检查命令在目标镜像中确实存在;
  • Profile 没有意外开放管理接口;
  • 密码没有进入环境变量、镜像层、日志和仓库;
  • 扩容时没有 container_name 或固定宿主机端口冲突;
  • 生产部署所需的备份、监控、TLS、轮换和回滚由明确系统负责。

Compose 的价值在于把单机多容器应用的结构显式化:服务定义决定运行单元,网络决定通信边界,卷决定数据生命周期,依赖决定初始编排,Profile 决定选择性启用。理解这些对象之间的因果关系后,才能知道哪些问题 Compose 可以解决,哪些问题必须交给应用、主机或更高层的编排平台。


系列导航与关联阅读

官方资料

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