Skip to content

容器网络与数据卷:服务之间怎么互相访问

更新: 8/15/2026 字数: 0 字 时长: 0 分钟

从你熟悉的场景说起:一次接口调用

作为前端开发者,你一定写过这样的代码:

javascript
fetch('http://localhost:8080/api/users')
  .then(res => res.json())

前端跑在 3000 端口,后端跑在 8080 端口,你通过一个地址 + 端口就能把请求发过去。如果跨域了,你会配个 proxy 代理;如果后端换了地址,你改个 baseURL 就行。这套「谁在哪、怎么找到它」的逻辑,你早已烂熟于心。

容器世界里的服务通信,本质上就是同一套逻辑,只不过"谁在哪"这件事变复杂了。 当后端被打包进 Docker 容器,它不再是"跑在你电脑 8080 端口的那个进程",而是"住在一个隔离小盒子里的服务"。盒子之间怎么互相喊话?这就是本文要讲的事。

从前后端接口调用到容器通信

一、容器网络基础:每个容器都是一台"独立小电脑"

先建立一个核心心智模型:每启动一个容器,就相当于开了一台全新的、干净的虚拟小电脑。

这台"小电脑"有:

  • 自己的文件系统(所以它看不到你宿主机的文件)
  • 自己的进程空间(它以为自己是世界上唯一的机器)
  • 自己独立的 IP 地址

最后一点最关键。这就解释了新手最容易踩的坑:

你在容器 A 里写 fetch('http://localhost:8080') 去调容器 B,结果连不上

为什么?因为 localhost 在容器 A 里指的是容器 A 自己,不是宿主机,更不是容器 B。每个容器的 localhost 都是它自己的"本机"。这就像你在两台不同的电脑上,各自的 localhost 当然指向各自。

那容器之间怎么通信?Docker 默认会创建一个叫 bridge(网桥) 的虚拟局域网。你可以把它想象成一台虚拟交换机:所有连到这台交换机上的容器,会各自分到一个内网 IP(比如 172.17.0.2172.17.0.3),它们之间可以通过这些 IP 互相访问。

容器网络:同一宿主机上的虚拟局域网

类比一下:这就像你连上了办公室的 Wi-Fi,路由器给你和同事的电脑各分了一个 192.168.x.x 的地址,你们在同一个局域网里能互访;但外网的人访问不到你,除非路由器做了端口映射。Docker 的 bridge 网络,就是这台"路由器"。

二、服务互访的三种常见方式

方式一:用 IP 直连(不推荐,但帮你理解原理)

最原始的方式:查出容器 B 的内网 IP,然后直接访问。

javascript
fetch('http://172.17.0.3:3000/api/users')

问题在哪? 容器的 IP 是 Docker 动态分配的,每次重启都可能变。这就像你把接口地址硬编码成了一个每天都在变的 IP——今天能跑,明天重启就 404。前端同学对这种"写死配置导致上线炸掉"的痛应该深有体会。所以这种方式只用来理解原理,生产中几乎不用。

方式二:用服务名访问(推荐,容器界的 DNS)

这是最优雅的方式,而且你会瞬间理解——它就是容器版的"域名解析"

当你用 Docker Compose 编排多个服务时(比如一个 web、一个 api、一个 db),Docker 会内置一个 DNS 服务。你不用管 IP 是多少,直接用服务名当主机名去访问就行:

javascript
// 在 web 容器里调用 api 服务,直接用服务名 "api"
fetch('http://api:3000/users')

Docker 的内置 DNS 会自动把 api 这个名字解析成它当前真实的 IP,哪怕重启后 IP 变了也无所谓,名字永远指向正确的容器。

用服务名代替 IP:容器界的 DNS

类比一下:你在浏览器输 github.com 而不是它的 IP,因为 DNS 帮你做了名字→IP 的翻译,而且 IP 变了你也无感。容器的服务名访问,就是同一个道理——用一个稳定的名字,屏蔽掉底层多变的 IP。

对应的 docker-compose.yml 大致长这样:

yaml
services:
  web:
    build: ./frontend
    ports:
      - "3000:3000"     # 宿主机端口:容器端口
  api:
    build: ./backend    # web 容器里可直接用 http://api:3000 访问它
  db:
    image: postgres

只要写在同一个 compose 文件里,它们默认就在同一个网络内,彼此可以用服务名互相访问。

方式三:端口映射(让"外面的人"进来)

前两种方式解决的是容器之间的内部通信。但还有个问题:你的浏览器(跑在宿主机上,不在容器网络里)怎么访问容器里的服务?

答案是端口映射,就是上面 YAML 里的这行:

yaml
ports:
  - "3000:3000"   # 把宿主机的 3000 端口,映射到容器的 3000 端口

冒号左边是宿主机端口,右边是容器端口。配了这行,你在浏览器访问 http://localhost:3000 就能打到容器里的服务了。

这里有个关键区分,帮你彻底理清:

场景用什么访问原因
浏览器 → 容器服务localhost:3000(宿主机映射端口)浏览器在容器网络外,只能走映射出来的端口
容器 → 容器服务http://api:3000(服务名 + 容器内端口)容器在同一内网,直接走服务名,不需要端口映射

一个常见误区:很多人以为容器之间互访也要配 ports 映射,其实不需要。端口映射只是为了让容器网络"之外"的访问者(比如你的浏览器)能进来;容器内部互访走的是内网,直接用服务名 + 容器端口即可。

三、数据卷:为什么它和"服务访问"有关系

讲完网络,再讲数据卷(Volume)。你可能会问:数据卷不是存数据的吗,跟服务通信有啥关系?

关系很直接。回到前面那个心智模型:容器是一台用完即弃的"临时电脑",容器一删,里面的文件全没了。这对无状态的前端/后端服务无所谓(代码在镜像里),但对数据库容器是致命的——数据库一重启,用户数据全丢了,那你的 API 还怎么正常提供服务?

数据卷就是为了解决这个问题:把需要持久保存的数据,存到容器"之外"的一块独立空间里。 容器可以随便删、随便重建,数据卷里的数据始终还在。

数据卷:容器之外的持久化仓库

yaml
services:
  db:
    image: postgres
    volumes:
      - db_data:/var/lib/postgresql/data   # 把数据库的数据目录挂到卷上

volumes:
  db_data:   # 声明一个独立于容器的持久化卷

它和服务访问的关联,是一条完整的链路:

你的 web 容器 → 通过服务名调用 api 容器 → api 容器读写 db 容器 → db 容器把数据存进数据卷

网络负责让服务"能互相找到对方",数据卷负责让服务"重启后数据不丢、状态可靠"。前者保证通信通,后者保证服务稳。两者配合,才是一个能长期运行的完整系统。

类比一下:数据卷就像你把重要文件存到了公司的共享网盘,而不是存在自己那台随时可能被回收重装的办公电脑本地硬盘上。电脑坏了、换了,文件还在网盘里。

四、技术拓展:再往前走几步

以上是核心。如果你想更进一步,下面这些概念能帮你在未来看懂更复杂的架构。

1. 数据卷 vs 绑定挂载(Bind Mount)——本地开发的热更新利器

除了上面那种由 Docker 管理的"命名卷",还有一种叫绑定挂载,它把你宿主机的某个目录直接挂进容器:

yaml
volumes:
  - ./src:/app/src   # 宿主机的 ./src 目录 ←→ 容器内的 /app/src

这对前端开发者极其有用:你在本机改代码,容器里的服务能实时看到变化,配合热更新(HMR)就不用每改一次都重新构建镜像了。简单记:命名卷用于"持久化生产数据",绑定挂载用于"本地开发同步代码"

2. 自定义网络与网络隔离——容器界的"安全分区"

你可以创建多个网络,让不同服务分组。比如让 db 只和 api 在一个网络里,web 根本无法直连数据库——这就实现了网络隔离,是一种安全实践。类比前端的思路:就像你不会把数据库凭证暴露到浏览器端一样,后端也不该让不相干的服务能直接摸到数据库。

3. 从单机到集群:Kubernetes 的 Service

当服务多到一台机器放不下,就需要 Kubernetes(K8s) 把容器调度到多台机器上。这时"用服务名访问"的理念被进一步放大成了 K8s 的 Service 机制:你依然用一个稳定的名字访问服务,但背后可能是几十个副本容器在做负载均衡。你熟悉的"域名 → 稳定入口 → 后面一堆机器"的模型,在这里完全成立——K8s Service 就是一个自带负载均衡的智能 DNS。

4. Nginx 反向代理——最像你"proxy 配置"的那个东西

生产环境里,通常会有一个 Nginx 容器作为统一入口,它接收所有外部请求,再根据路径转发给内部的不同容器(/api 转给后端,/ 转给前端)。这和你在 vite.config.js / webpack devServer 里写的 proxy 配置思路一模一样,只是从开发环境的代理,升级成了生产环境的网关。

一句话总结

如果只记一件事:容器化时代的服务通信,并没有推翻你熟悉的那套"地址 + 端口 + 代理 + 域名"逻辑,只是把它搬进了一个由虚拟网络、服务名 DNS 和持久化数据卷组成的新环境。 你原有的前端网络直觉,80% 都能平移过来——剩下的 20%,就是搞清楚"容器的 localhost 是它自己"、"内部互访用服务名不用映射端口"、"有状态服务的数据要交给数据卷"这几个关键差异。