容器与编排

Docker Compose 网络与数据卷实战

容器间用 localhost 互访失败、named volume 与 bind mount 到底选哪个,都是 Compose 里的高频困惑。本文讲清默认 bridge 与自定义网络的 DNS 差异、服务名解析、端口发布与暴露的区别,以及两种挂载方式在权限、备份与迁移上的取舍。

作者:巧匠团队·8 分钟阅读·更新于 2026-09-30

为什么默认网络下服务名解析不了

Compose 默认给项目创建一个用户自定义的 bridge 网络,项目内所有服务都接在上面,容器之间可以用服务名互相解析。所以应用连数据库写 `db` 或 `redis` 即可,不需要 IP,也不需要暴露端口到宿主机。

问题出在显式声明 `network_mode: bridge` 或把服务接到 default bridge 网络时。default bridge 没有内嵌 DNS,只能靠 --link 这种已废弃的机制或手写 IP。这两种写法在多机部署时也直接失效,因为容器 IP 每次重建都会变。

因此推荐的做法是:需要互访的服务不要加 ports;只在需要从宿主机或外部访问的服务上声明 ports。需要额外别名时用 networks 下的 aliases,它会在该网络内注册多个名字。

services:
  api:
    image: acme/api:1.4
    networks: [app]
  db:
    image: postgres:16
    networks:
      app:
        aliases: [database, pg]
    volumes: [pgdata:/var/lib/postgresql/data]

networks:
  app:
    driver: bridge

volumes:
  pgdata:

expose 与 ports 的区别

expose 只是声明「这个容器会用到这个端口」,供文档和同网络内客户端参考,它不会创建任何到宿主机的映射,也不会让默认网络下的服务获得 DNS 解析能力。它本质上是个元信息。

ports 才会做端口发布:短格式 "8080:80" 表示宿主机 8080 映射到容器 80,长格式可以指定协议和监听地址,如 "127.0.0.1:8080:80/tcp" 只绑定回环地址,外部完全不可达,适合只想本机调试的场景。

注意 -p 与 -P 的区别:-p 显式指定映射,-P 会把镜像 EXPOSE 声明的所有端口随机映射到宿主机高位端口。只用 -P 会让服务意外暴露在所有网卡上,安全上应始终写明 -p。

services:
  web:
    image: acme/web:2.0
    expose:
      - "3000"
    ports:
      - "127.0.0.1:8080:3000"
  metrics:
    image: prom/node-exporter:v1.8.1
    ports:
      - "9100:9100"

named volume 与 bind mount 的取舍

bind mount 映射宿主机目录,路径由你指定。适合需要宿主机直接编辑配置、挂载已有数据目录、或用 `--mount` 设置 uid/gid 与 ro 权限的场景。缺点是路径依赖宿主机布局,迁移和在 CI 里跑都不方便。

named volume 由 Docker 管理存储位置,跨宿主机和 CI 中表现一致,也更容易设置属主与权限。首次挂载到空目录时,Docker 会把镜像内该路径的原有内容复制进卷,bind mount 则默认遮住镜像内容,需要显式 `nocopy` 才复制。

运维取舍:需要备份和迁移时用 named volume 配合 `docker run --rm -v name:/data -v "$PWD":/backup alpine tar czf /backup/x.tar.gz -C /data .`;需要人工进去改文件、或把配置从仓库挂进去时用 bind mount。两者在同一服务里混用很常见,例如数据用卷、配置用挂载。

services:
  db:
    image: postgres:16
    volumes:
      - pgdata:/var/lib/postgresql/data     # named volume:数据
      - ./conf/postgresql.conf:/etc/postgresql/postgresql.conf:ro   # bind mount:配置

# 备份 named volume
docker run --rm -v pgdata:/data -v "$PWD":/backup alpine tar czf /backup/pgdata.tar.gz -C /data .

启动顺序与排障

depends_on 只控制启动顺序和运行期依赖下线顺序,不保证依赖已经可用。数据库容器起来了不代表能接受连接,因此应用反复报 connection refused 是常见现象,而不是配置错误。

需要真正等待就绪时,用长语法加 condition: service_healthy,并在依赖服务上定义 healthcheck。healthcheck 里的检查必须轻量且确定,不要用依赖应用自身的复杂逻辑,否则会形成死锁式的等待。

排查网络问题的顺序是:先 `docker compose ps` 确认容器状态与端口,再进容器用服务名测试连通性(如 `getent hosts db` 或 `curl`),然后用 `docker network inspect` 查看同一网络上的成员与别名。若服务名解析失败,先检查该服务是否真的声明了 networks。

services:
  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 10
  api:
    depends_on:
      db:
        condition: service_healthy

多 Compose 文件与环境隔离

Compose 支持 -f 叠加多个文件,后面的文件覆盖前面的,这是把基础配置与开发/生产覆盖层分开的最省事做法。基础文件放服务定义与镜像名,覆盖文件只放 ports、environment 和 volumes 的差异。

每个项目默认创建以目录名命名的网络与卷,因此同名项目在同机并行会冲突。用 `docker compose -p <name>` 显式指定项目名,或者在文件里用 name 字段固定,都能保证隔离。生产环境尤其要避免两个环境共享同一个 named volume。

查网络细节时 `docker network inspect` 最直接:关注 Containers 列表里的服务名与别名,以及 IPAMConfig 的子网划分。确认同一个外部服务被多个项目复用时,可以把它声明为 external 网络,由外部统一管理,避免每个项目各建一个网络导致互访失败。

# 基础 + 覆盖
docker compose -f compose.yaml -f compose.prod.yaml up -d
docker compose -f compose.yaml -f compose.prod.yaml config   # 校验合并结果

# 固定项目名,避免同名目录互相污染
docker compose -p acme-api up -d

networks:
  shared:
    external: true
    name: acme-shared

官方参考来源

下方为命令对应的官方权威文档,供你核对最新用法与深入查阅。