目录

Docker-07 Compose 从入门到实战:多容器、网络、宿主机访问与生产排障

目录

1. Compose 解决什么问题

前面用 docker run 启动一个容器时,所有参数都写在命令行里:镜像、端口、环境变量、卷、网络、重启策略。一个服务还可以接受这种方式,三个或四个服务就会变成一串难以复现的命令:

先创建网络
再启动数据库
再启动缓存
再启动 API
再记住每个端口、卷和环境变量

Compose 把这组服务写进 YAML 文件,并根据文件创建和更新容器、网络和卷:

compose.yaml
  ├── services   服务和容器参数
  ├── networks   服务之间如何连通
  ├── volumes    数据如何持久化
  ├── configs    非秘密配置
  └── secrets    秘密输入

它解决的是一组容器在一台 Docker 主机上的可复现管理。Compose 适合本地开发、测试、CI 集成测试和简单单机部署;它不提供 Kubernetes 那样的跨节点调度、自动扩容和完整高可用。

2. 安装和版本确认

现代 Docker 推荐 Compose v2 插件,命令写成 docker compose,中间有空格。旧的 docker-compose 是独立 Python 工具,语法和行为可能不同。

docker version
docker compose version
docker buildx version
docker context show

在 macOS/Windows 上,Compose CLI 通常连接 Docker Desktop 里的 Linux daemon;在 Linux 服务器上,通常直接连接 Docker Engine。先确认 docker context show,再执行任何会删除容器、卷或网络的命令。

3. 第一个 Compose 项目

创建目录:

mkdir compose-first
cd compose-first

写入 compose.yaml

services:
  web:
    image: nginx:1.27-alpine
    ports:
      - "127.0.0.1:8080:80"

启动和查看:

docker compose config
docker compose up -d
docker compose ps
curl -fsS http://127.0.0.1:8080
docker compose logs web
docker compose down

第一次阅读 Compose 文件时只看三件事:services 下的服务名、image 使用什么镜像、ports 把哪个宿主端口映射到哪个容器端口。

3.1 up 发生了什么

docker compose up -d 大致会:

  1. 读取 Compose 文件和变量。
  2. 创建项目默认网络。
  3. 拉取本地不存在的镜像。
  4. 创建服务容器并注入网络、端口、环境和卷。
  5. 按依赖关系启动服务。
  6. 后台运行并记录容器状态。

Compose 不是把 YAML 每一行机械翻译成一条 docker run。它会把文件渲染成项目模型,再把模型和当前 Docker 对象比较,决定创建、更新或重建哪些服务。

3.2 常用命令地图

命令作用初学者容易误解的地方
config渲染并校验最终配置不启动容器,适合先发现变量/YAML 错误
up -d创建或更新服务配置/镜像改变时可能重建容器
ps查看项目服务状态默认只显示项目容器,排障可加 --all
logs -f跟踪服务日志只能看到 stdout/stderr 或日志驱动提供的内容
exec在运行容器内执行命令不创建新容器,主进程退出后不能用
run --rm创建一次性服务容器默认不发布服务端口,适合迁移和管理命令
restart停止后重新启动现有容器不读取新 Dockerfile,不应用新挂载/环境
stop停止服务容器默认保留容器、卷和网络
down删除项目容器和网络默认保留 named volume
down -v还删除项目卷可能删除数据库数据,需明确确认

查看项目最终模型:

docker compose config > rendered.compose.yaml
docker compose config --services
docker compose config --volumes

4. YAML 中最重要的对象

4.1 service 名和 container 名

services:
  api:
    image: example/api:1.0.0
  db:
    image: postgres:16-alpine

apidb 是 Compose 服务名,也是同一项目网络中的 DNS 名。服务名不是镜像名,也不要求和最终容器名相同。

默认情况下,Compose 会生成带项目名的容器名,例如 demo-api-1。不要随意写 container_name:它会阻止同一个服务方便地扩展多个副本,也可能与其他项目发生名称冲突。

4.2 imagebuild

使用现成镜像:

services:
  web:
    image: nginx:1.27-alpine

从本地 Dockerfile 构建:

services:
  api:
    image: demo/api:dev
    build:
      context: .
      dockerfile: Dockerfile

context 是构建上下文,Dockerfile 的 COPY 不能越过它访问文件。Compose 文件所在目录不一定就是上下文目录;路径必须结合 docker compose config 和实际目录确认。

开发时可以 docker compose up -d --build,生产发布通常先在 CI 构建、扫描、签名并推送镜像,服务器只 pull 已验证的 digest,不在生产主机现场编译。

4.3 commandentrypointworking_dir

services:
  worker:
    image: example/worker:1.0.0
    working_dir: /app
    entrypoint: ["/app/worker"]
    command: ["--config", "/etc/worker/config.yaml"]

Compose 中的 command 会覆盖镜像默认 CMDentrypoint 会覆盖镜像 ENTRYPOINT。这两个字段会改变最终 PID 1,可能影响信号转发和优雅退出。修改启动命令后,要用 docker compose configdocker inspect 验证最终值。

5. 从单容器到 API + PostgreSQL + Redis

下面是一份适合本地开发的完整骨架:

name: demo

services:
  api:
    image: demo/api:dev
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      APP_ENV: ${APP_ENV:-dev}
      DATABASE_URL: postgres://app:${POSTGRES_PASSWORD}@db:5432/app?sslmode=disable
      REDIS_ADDR: redis:6379
    ports:
      - "127.0.0.1:8080:8080"
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks: [app]

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 5s
      timeout: 3s
      retries: 10
    networks: [app]

  redis:
    image: redis:7-alpine
    command: ["redis-server", "--appendonly", "yes"]
    volumes:
      - redisdata:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 10
    networks: [app]

volumes:
  pgdata:
  redisdata:

networks:
  app:
    driver: bridge

运行:

printf 'POSTGRES_PASSWORD=dev-password\n' > .env
docker compose config
docker compose up -d --build
docker compose ps
docker compose logs -f api

这里的关键关系是:

  • API 通过 db:5432 访问 PostgreSQL,通过 redis:6379 访问 Redis。
  • API 端口发布到宿主 127.0.0.1:8080,浏览器可以访问;数据库和 Redis 没有 ports,只能在 Compose 网络内访问。
  • pgdataredisdata 是 named volume,容器重建后数据仍可保留。
  • depends_on 配合 healthcheck 只能改善启动顺序,API 仍要实现连接重试。

6. 网络:容器之间如何通信

Compose 默认创建一个项目网络,例如 demo_default。同一网络内的服务通过服务名解析:

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

不要硬编码容器 IP。容器被重建后 IP 可能改变,服务名仍然稳定。

6.1 portsexposeEXPOSE

services:
  api:
    ports:
      - "127.0.0.1:8080:8080"
    expose:
      - "8080"
  • ports:发布到宿主机,格式是 宿主地址:宿主端口:容器端口
  • expose:声明内部服务端口,不发布到宿主机,主要用于文档和某些编排场景。
  • Dockerfile 的 EXPOSE:镜像元数据,不会自动打开端口。

ports: ["8080:8080"] 通常会监听宿主机所有地址;本地开发和管理端口优先写 127.0.0.1,避免意外暴露公网。

6.2 localhost 到底指谁

在 API 容器里:

docker compose exec api curl http://localhost:8080

这里的 localhost 是 API 容器自己,不是宿主机,也不是 db 容器。访问数据库要写 db:5432;访问另一个服务要写它的服务名和容器端口。

下面这些地址含义不同:

地址从容器内看代表什么
127.0.0.1 / localhost当前容器的 network namespace
db:5432Compose 网络中的 db 服务容器端口
host.docker.internal:8080Docker Desktop 的宿主机特殊 DNS;Linux 需显式配置
172.17.0.1:8080常见默认 bridge 网关,不能盲目硬编码
宿主机局域网 IP宿主网卡地址,受监听地址和防火墙影响

7. 容器内如何访问宿主机端口

这是初学者最容易踩坑的地方。假设宿主机上有一个 API 监听 8080,容器需要访问它。

7.1 错误写法:访问 localhost

docker compose exec api curl http://127.0.0.1:8080

这会访问 API 容器自己的 8080 端口。如果 API 容器没有监听它,通常得到 connection refused;它不会自动绕到宿主机。

7.2 Docker Desktop:使用 host.docker.internal

macOS、Windows 的 Docker Desktop 通常提供:

docker run --rm curlimages/curl:8.10.1 \
  http://host.docker.internal:8080/healthz

Compose 写法:

services:
  api:
    image: example/api:dev
    extra_hosts:
      - "host.docker.internal:host-gateway"

Docker Desktop 会把 host.docker.internal 解析到 Desktop 宿主机入口。宿主服务如果只监听某些地址,仍要检查它是否接受来自 Docker VM 的连接;不要把这条特殊 DNS 当成所有平台都自动存在。

7.3 Linux:显式添加 host-gateway

Linux Docker Engine 通常不会自动提供 host.docker.internal。Compose 中显式添加:

services:
  api:
    image: example/api:dev
    extra_hosts:
      - "host.docker.internal:host-gateway"

然后容器内访问:

docker compose exec api getent hosts host.docker.internal
docker compose exec api curl -v http://host.docker.internal:8080/healthz

host-gateway 是 Docker 提供的特殊值,daemon 会把它替换成当前网络可到达的宿主网关地址。它比把 172.17.0.1 写死更适合跨机器 Compose 文件,但仍受 Docker 版本、网络模式和防火墙影响。

也可以单次运行时添加:

docker run --rm \
  --add-host=host.docker.internal:host-gateway \
  curlimages/curl:8.10.1 \
  http://host.docker.internal:8080/healthz

7.4 Linux:使用 bridge 网关地址

查看网络网关:

docker network inspect demo_default | jq '.[0].IPAM.Config'
ip addr show docker0
ip route

默认 bridge 常见网关是 172.17.0.1,自定义 Compose 网络可能是 172.18.0.1172.20.0.1 或其他地址。正确做法是查询当前网络,而不是假设所有机器都是 172.17.0.1

gateway=$(docker network inspect demo_default \
  -f '{{(index .IPAM.Config 0).Gateway}}')
docker compose exec api sh -c "curl -v http://${gateway}:8080/healthz"

脚本使用网关地址时要考虑 shell 转义、网络不存在和多个 IPAM 配置;对应用配置而言,host.docker.internal + host-gateway 通常更易读。

7.5 宿主服务必须监听可达地址

即使容器找到了宿主地址,宿主服务只监听 127.0.0.1 时,Linux bridge 容器通常仍然无法通过宿主 bridge IP 访问它:

ss -lntp | rg ':8080'

监听状态的含义:

监听地址容器访问宿主 bridge 地址
127.0.0.1:8080通常不可达,服务只接受宿主 loopback
0.0.0.0:8080通常可达,但会暴露到所有宿主接口,需防火墙
宿主 bridge IP:8080可达范围更窄,需确保地址稳定
Unix socketTCP 容器不能直接访问,需显式挂载 socket 且评估权限

开发服务可以配置监听 0.0.0.0,再用宿主防火墙限制来源;生产不要为了容器访问就把管理端口无条件暴露到公网。很多开发框架默认绑定 127.0.0.1,需要显式设置 --host 0.0.0.0 或对应配置。

7.6 network_mode: host:能用,但代价很大

Linux 可以让容器共享宿主 network namespace:

services:
  debug:
    image: nicolaka/netshoot:latest
    network_mode: host

此时容器内的 127.0.0.1 就是宿主机 loopback,也不需要 ports;但容器失去独立网络隔离,端口冲突、监听范围和安全风险直接与宿主绑定。network_mode: hostports 不能同时使用,Compose 通常会直接报错;删除 ports 后,服务监听的就是宿主机端口。Docker Desktop 的 host networking 还受 Desktop 版本和设置限制,不要把 Linux 行为直接推断到 Desktop。

7.7 宿主机端口访问排障

按这个顺序排查:

# 1. 宿主服务是否监听
ss -lntp | rg ':8080'

# 2. 容器是否能解析特殊主机名
docker compose exec api getent hosts host.docker.internal

# 3. 容器到目标端口是否建立 TCP
docker compose exec api sh -c 'nc -vz -w 3 host.docker.internal 8080'

# 4. HTTP/TLS/应用层是否正常
docker compose exec api curl -v --max-time 5 \
  http://host.docker.internal:8080/healthz

# 5. Linux 上看路由和防火墙
docker compose exec api ip route
sudo iptables -S DOCKER-USER 2>/dev/null || true
sudo nft list ruleset 2>/dev/null | sed -n '1,160p'

如果 DNS 成功但 TCP 超时,优先看监听地址、防火墙、VPN 和宿主路由;如果 TCP 成功但 HTTP 失败,再看协议、TLS、Host header、认证和应用日志。

8. 网络隔离和多网络设计

把入口、应用和数据分到不同网络:

services:
  proxy:
    image: nginx:1.27-alpine
    networks: [edge, app]
  api:
    image: example/api:1.0.0
    networks: [app, data]
  db:
    image: postgres:16-alpine
    networks: [data]

networks:
  edge: {}
  app: {}
  data:
    internal: true

这里 API 可以访问 DB,DB 不会自动出现在 edge 网络,外部请求只能到 proxy。internal: true 是网络层面的限制,不替代数据库认证、宿主防火墙和应用授权。

连接已有网络:

networks:
  shared:
    external: true
    name: company-shared

external 表示 Compose 不创建、不删除这个网络;启动前必须确认它存在,网络名称和权限由外部系统负责。

9. 卷、bind mount 和配置文件

9.1 named volume

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

volumes:
  pgdata:

Compose 会创建带项目名前缀的卷,例如 demo_pgdatadocker compose down 默认保留它,docker compose down -v 会删除项目声明的卷。数据库卷必须有原生备份、恢复验证和权限规划。

9.2 bind mount

services:
  api:
    volumes:
      - type: bind
        source: ./config
        target: /app/config
        read_only: true

相对路径相对于 Compose 项目目录解析。宿主目录会遮住镜像中同一路径的默认文件,不会自动合并;挂载一个空 ./config 可能让应用看不到镜像内的默认配置。

开发源码热加载适合 bind mount,生产要警惕宿主路径权限、SELinux 标签、误覆盖和敏感文件泄露。

9.3 tmpfs

services:
  api:
    tmpfs:
      - /tmp:rw,noexec,nosuid,nodev,size=64m

tmpfs 存在于内存,容器停止后消失,适合临时文件和短期缓存;它不是数据库持久化方案。

10. 环境变量、.env 和秘密

10.1 .env 用于替换 Compose 文件

.env 常用于变量替换:

APP_ENV=dev
POSTGRES_PASSWORD=dev-password
services:
  db:
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}

启动前查看变量替换结果:

docker compose config
docker compose --env-file .env.test config

${NAME:-default} 表示没有值时使用默认值;${NAME:?message} 表示没有值时直接报错。生产密码不应提交到 Git,仓库可以只提交 .env.example

10.2 environmentenv_file

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

这些配置会把变量注入容器,运行时可从 docker inspect 看到,不能当作加密秘密存储。命令行环境变量也可能出现在 shell 历史、CI 日志或进程信息中。

10.3 Compose secrets

services:
  api:
    secrets:
      - db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt

容器内通常从 /run/secrets/db_password 读取。开发 Compose 的 file 仍依赖宿主文件权限;生产应使用部署系统或外部 secret manager。不要把密码写进 Dockerfile、镜像 layer 或日志。

11. 启动顺序、健康检查和重试

11.1 depends_on 的三种理解

短写法:

depends_on:
  - db

只表达启动顺序,不保证 DB 已经接受连接。健康条件:

depends_on:
  db:
    condition: service_healthy

这会让 Compose 等待 healthcheck 通过,但不代表迁移完成、账号正确或业务可写。应用仍然需要连接超时、指数退避、断线重连和幂等初始化。

11.2 healthcheck 应该测什么

healthcheck:
  test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/healthz >/dev/null"]
  interval: 10s
  timeout: 3s
  start_period: 30s
  retries: 3

健康检查要轻量、快速、幂等。区分三类状态:

  • liveness:进程是否还能工作,失败可以重启。
  • readiness:实例是否应该接收流量,失败应摘流,不一定重启。
  • dependency:数据库、缓存或外部 API 是否可达,不应因为一次短暂超时就杀死全部副本。

Docker Engine 将失败标为 unhealthy,不会因为健康检查失败自动触发普通 restart policy。要自动替换不健康实例,交给编排平台、外部 watchdog,或者让应用在不可恢复时退出。

12. restart、资源限制和生命周期

Compose 的 restart 是容器级 restart policy:

services:
  api:
    restart: unless-stopped
  worker:
    restart: "on-failure:5"

它只处理主进程退出,不负责健康检查、数据库迁移、零停机发布和跨节点故障转移。四种策略的差异见 Docker-03 重启策略

Compose 中还可能出现相似但不同的字段:

services:
  api:
    depends_on:
      db:
        condition: service_healthy
        restart: true

这里的 depends_on.restart: true 指 Compose 显式操作 DB 时联动重启 API,不等于 DB 每次崩溃都由 Compose 负责重启。

资源限制示例:

services:
  api:
    cpus: 1.5
    mem_limit: 768m
    mem_reservation: 512m
    pids_limit: 512

这些字段最终会影响容器 HostConfig 和 cgroup,但具体支持依 Docker、Compose、内核和平台而定。生产修改前用 docker compose configdocker inspect/sys/fs/cgroup 验证。

13. 配置覆盖和多环境

推荐布局:

compose.yaml             # 通用服务
compose.dev.yaml         # 开发覆盖
compose.prod.yaml        # 生产覆盖
.env.example
docker compose -f compose.yaml -f compose.dev.yaml config
docker compose -f compose.yaml -f compose.dev.yaml up -d

后面的文件会覆盖前面的模型,但列表字段的合并规则容易出乎意料,启动前必须审阅 config 输出。不要只凭文件片段推断最终的 portsvolumesenvironmentnetworks

13.1 开发覆盖示例

基础文件:

services:
  api:
    image: demo/api:1.0.0
    networks: [app]

开发覆盖:

services:
  api:
    build: .
    volumes:
      - .:/src
    environment:
      LOG_LEVEL: debug
    ports:
      - "127.0.0.1:8080:8080"

生产覆盖应去掉源码挂载、调试端口和本地构建,使用 registry digest、外部 secret、资源限制和正式日志驱动。

14. Profiles:可选服务

开发工具不应该每次都启动:

services:
  adminer:
    image: adminer:4
    profiles: [tools]
    ports:
      - "127.0.0.1:8081:8080"
docker compose up -d
docker compose --profile tools up -d

profiles 适合 Adminer、调试代理、一次性迁移工具和本地监控组件。生产配置中要确认 profile 没有意外暴露管理端口。

15. up、重建与配置漂移

Compose 通过服务配置生成容器。以下命令的语义不同:

docker compose restart api
docker compose up -d api
docker compose up -d --build api
docker compose up -d --force-recreate api
  • restart:停止/启动旧容器,不应用新镜像、挂载或环境定义。
  • up -d:比较配置和镜像,必要时更新/重建。
  • --build:需要时重新构建本地镜像。
  • --force-recreate:即使 Compose 判断配置没变,也强制重建。

如果手工执行:

docker update --memory 1g demo-api-1

当前容器会改变,但 Compose 文件没有改变;下次重建时手工值可能消失,这叫配置漂移。紧急止损可以先 update,随后必须把最终配置写回 Compose 文件并重新 config/up 验证。

16. 横向扩展和项目名

docker compose -p demo-dev up -d --scale api=3

扩展前确认:

  • API 是否无状态,session 是否外置。
  • 入口代理是否能把流量分发到多个副本。
  • 数据库连接池是否按副本数规划。
  • 健康检查和 readiness 是否能摘除坏副本。
  • 没有固定 container_name
  • volume 是否需要每副本独占。

项目名决定容器、网络和卷的默认前缀。显式项目名可以避免不同目录的项目互相冲突:

docker compose -p demo-dev up -d
docker compose -p demo-test up -d
docker compose -p demo-dev ps

Compose 的 scale 仍然是单机能力,不等于跨节点调度和高可用。

17. 现实问题一:宿主端口被占用

现象:

Bind for 0.0.0.0:8080 failed: port is already allocated

排查:

ss -lntp | rg ':8080'
docker ps --format 'table {{.Names}}\t{{.Ports}}'
docker compose ps

解决路径:

  1. 找到占用端口的宿主进程或容器。
  2. 如果入口已由其他服务占用,修改 Compose 的宿主端口,例如 18080:8080
  3. 如果服务只供 Compose 内部访问,删除 ports,保留 expose 或直接让服务监听容器端口。
  4. 不要为了“先跑起来”直接停止不认识的生产进程。

注意 ports 左边是宿主端口,右边是容器端口。改变左边不会改变应用在容器内监听的端口。

18. 现实问题二:数据库启动了但 API 连接失败

不要只看 docker compose ps 显示 Up。进程活着不代表数据库已经 ready:

docker compose ps
docker compose logs --tail 200 db
docker compose exec db pg_isready -U app -d app
docker compose exec api getent hosts db
docker compose exec api sh -c 'nc -vz -w 3 db 5432'

按层定位:

  1. API 的连接地址是否写成 db:5432,而不是 localhost:5432
  2. API 与 DB 是否加入同一个 Compose 网络。
  3. DB healthcheck 是否使用了正确用户、数据库名和密码环境。
  4. API 是否有连接重试和退避;depends_on 不是应用重试机制。
  5. volume 中是否有旧版本数据,导致初始化环境变量不再生效。

PostgreSQL、MySQL 等官方镜像通常只在空数据目录第一次初始化时读取创建用户和密码变量。修改 Compose 中的密码,不会自动修改已有数据库用户密码。

19. 现实问题三:容器能访问网络,但访问不了宿主 API

一个完整实验:

# 宿主机启动一个测试服务;Linux 上要监听可达地址
python3 -m http.server 8080 --bind 0.0.0.0

# compose.yaml 中添加 extra_hosts 后,从容器访问
docker compose exec api curl -v \
  http://host.docker.internal:8080

失败时区分:

  • Could not resolve host:没有 Desktop 特殊 DNS,Linux 缺少 extra_hosts 或 daemon 不支持 host-gateway
  • Connection refused:宿主没有监听该地址/端口,或服务只监听宿主 loopback。
  • Connection timed out:防火墙、VPN、路由或安全组丢包。
  • HTTP 4xx/5xx:网络已经通,问题在 Host header、认证、路径或应用。

如果宿主服务本身也在 Compose 中,不要绕宿主机访问它,直接使用服务名和容器端口,例如 http://mock:9000。走宿主机端口会多一层 NAT 和平台差异,只有访问真正运行在宿主上的服务时才需要 host gateway。

20. 现实问题四:卷挂载后配置“消失”或权限错误

docker compose config
docker compose exec api id
docker inspect demo-api-1 | jq '.[0].Mounts'
docker compose exec api mount
docker compose exec api ls -la /app/config

常见根因:

  • 空宿主目录遮住了镜像内默认配置。
  • 容器以 UID 10001 运行,但宿主目录属于另一个 UID。
  • SELinux 标签阻止读写。
  • bind mount 使用了错误的相对路径。
  • read_only: true 或目标 volume 权限阻止写入。

解决时修正 UID/GID、挂载类型和配置来源,不要默认 chmod -R 777 或删除数据库卷。

21. 现实问题五:服务状态为 unhealthy

docker compose ps
docker inspect demo-api-1 | jq '.[0].State.Health'
docker compose logs --tail 200 api
docker compose exec api wget -S -O- http://127.0.0.1:8080/healthz

排查 healthcheck 自身:命令是否存在、路径是否正确、start_period 是否太短、探针是否依赖还未启动的下游。容器内没有 curl 不代表服务坏了,镜像应使用存在的工具,或在应用内提供专用探针程序。

健康检查持续失败时不要马上给服务加 restart: always。先判断是进程退出、依赖暂时不可达、探针写错,还是应用确实失去服务能力。

22. 现实问题六:Compose 看似修改了配置但容器没变

先看最终配置和容器创建时间:

docker compose config
docker compose ps
docker inspect demo-api-1 | jq '.[0] | {
  created: .Created,
  image: .Config.Image,
  env: .Config.Env,
  mounts: .Mounts,
  ports: .HostConfig.PortBindings
}'

可能原因:

  • 执行的是 docker compose restart,它不会重建容器。
  • 修改了错误的 Compose 文件,或当前目录不是预期项目。
  • 环境变量仍被 shell、.env--env-file 的值覆盖。
  • 当前容器是另一个项目名创建的。
  • 手工 docker update 造成了声明配置与实际配置不同。

必要时执行:

docker compose up -d --force-recreate api

强制重建前确认 volume、端口和秘密配置;无状态 API 通常安全,数据库服务不要未经评估强制重建。

23. Compose 调试工具箱

# 项目和服务
docker compose ls
docker compose ps --all
docker compose top api

# 配置和元数据
docker compose config
docker compose images
docker inspect "$(docker compose ps -q api)"

# 日志和事件
docker compose logs --timestamps --tail 200 api
docker events --since 30m

# 网络
docker network ls
docker network inspect demo_default
docker compose exec api ip addr
docker compose exec api ip route
docker compose exec api getent hosts db

# 存储
docker volume ls
docker volume inspect demo_pgdata
docker compose exec db df -h

# 资源和 cgroup
docker stats --no-stream
docker compose exec api cat /sys/fs/cgroup/memory.max
docker compose exec api cat /sys/fs/cgroup/cpu.stat

复制现场前先保存:

docker compose config > compose.rendered.yaml
docker compose ps --all > compose.ps.txt
docker compose logs --timestamps > compose.log
docker compose images > compose.images.txt

24. 生产环境如何使用 Compose

单机 Compose 可以用于小型内部服务,但要明确边界:

  • CI 构建并扫描镜像,服务器按 digest 拉取,不在生产现场 build
  • 配置、secret、数据卷和镜像版本分别管理。
  • 入口服务发布必要端口,数据库和缓存只放内部网络。
  • API 有 readiness、优雅退出、连接重试和合理 restart policy。
  • 日志有驱动、轮转和采集失败策略,资源有 CPU、内存、PIDs 限制。
  • 数据库有原生备份、恢复演练和迁移锁。
  • 发布前运行 docker compose config,发布后验证健康、错误率和延迟。
  • 回滚镜像时确认数据库 schema 向后兼容,不能只改一个 tag。

如果需要多节点调度、多个副本跨主机、自愈、滚动更新、服务发现和自动扩缩容,应该把同一批镜像交给 Kubernetes、Swarm、Nomad 或云编排服务。

25. 一个从零到排障的练习顺序

  1. 只启动 Nginx,理解 ports 和宿主端口。
  2. 加入 Redis,用服务名访问 redis:6379
  3. 加入 PostgreSQL,增加 named volume 和 healthcheck。
  4. 删除 API 容器并重建,验证无状态服务恢复。
  5. 删除 DB 容器但保留 volume,验证数据仍在。
  6. 在宿主机启动测试 API,用 host.docker.internal 访问它。
  7. 把宿主 API 改成只监听 127.0.0.1,观察 Linux 容器失败并解释原因。
  8. 修改宿主端口制造冲突,用 ssps 和 Compose 状态定位。
  9. localhost 错写进数据库地址,按 DNS/TCP/应用层排查。
  10. 手工 docker update 修改资源,再回写 Compose 消除配置漂移。
  11. 让 healthcheck 路径错误,区分 unhealthy 和主进程退出。
  12. 使用 downdown -v 前分别检查卷和数据,记录差异。

26. 初学者最应该记住的规则

  • 容器访问容器,用服务名 + 容器端口
  • 容器访问宿主,用 host.docker.internal;Linux Compose 添加 host-gateway
  • 容器里的 localhost 永远先理解成当前容器自己。
  • ports 左边是宿主端口,右边是容器端口。
  • depends_on 是启动依赖,healthcheck 是状态信号,应用重试仍需自己实现。
  • restart 只处理主进程退出,不处理所有 unhealthy,也不等于高可用。
  • downdown -v 的数据影响完全不同。
  • Compose 文件是声明状态,手工 docker update 是实际状态;两者不一致就是配置漂移。
  • 遇到问题先 configpslogsinspectevents,再修改配置。

27. 本篇小结

Compose 的学习顺序应该是:先学会启动一个服务,再理解服务名和容器端口,接着加入卷、环境变量、健康检查和多网络,最后处理宿主机访问、配置覆盖、资源限制和生产排障。真正掌握 Compose 的标志,是能解释每个地址、端口、挂载和状态变化,而不是只会复制一份 YAML。