Docker 网络先搞懂网桥,再看 Compose

奥黛丽·逐爱者
2026-07-02 / 0 评论 / 2 阅读 / 正在检测是否收录...

Docker 网络要先分清两件事:容器之间怎么通信,宿主机怎么访问容器。

最常见的问题,是在容器里把数据库地址写成 localhost⁠:

DATABASE_URL=postgresql://postgres:secret@localhost:5432/app

如果数据库就在宿主机本机,这行没问题;放进容器里就不对了:容器里的 localhost 指的是容器自己,不是数据库容器,也不是宿主机。

这篇我先讲 Docker 网络本身,然后再看这些概念在 Docker Compose 里怎么配置的。

▎先分清三种流量

看 Docker 网络,先分清流量方向:

容器访问外网容器访问容器宿主机访问容器

容器访问外网,比如容器里 curl https://example.com⁠。默认情况下,Docker 会让容器通过宿主机出去,外面的服务看到的通常是宿主机地址。

容器访问容器,比如应用连数据库。这个时候应该让两个容器在同一张 Docker 网络里。更准确一点:在用户自定义 bridge 网络或 Compose 默认网络里,通常用容器名或服务名访问。

宿主机访问容器,比如浏览器打开本地应用。这个时候看的是端口发布,也就是 -p 或 Compose 里的 ports⁠。

▎bridge 是日常开发的主角

装好 Docker 后看一下网络:

docker network ls

常见输出会有:

bridgehostnone

别把它理解成 Docker 只有三种网络。这里看到的是已经存在的网络。Docker 真正说“类型”时,看的是网络驱动,也就是 driver。

日常开发最常用的是 bridge⁠。可以把它想成宿主机里的一座软件网桥,容器接到这座桥上,形成一个小局域网。

宿主机  |  | 端口发布  vbridge 网络  |---- app 容器  |---- db 容器

同一张 bridge 网络里的容器可以互相通信。不同 bridge 网络之间默认隔开。外部要访问容器服务,通常要把容器端口发布到宿主机。

▎bridge 原理,知道这点就够用

不用一上来就啃 Linux 网络命名空间,但最好知道 Docker 大概做了什么。

Docker bridge 网络原理示意

容器启动后,会有自己的网络空间。你在容器里看到的网卡,并不是宿主机那张真实网卡。Docker 会给容器创建一对虚拟网卡,常见说法叫 veth pair⁠:一头放进容器里,另一头接到宿主机上的 bridge。

可以粗略想成这样:

app 容器 eth0   |veth pair   |Docker bridge   |宿主机网络

同一张 bridge 上的容器,像接在同一个小交换机上,所以它们能互相访问。Docker 还会给这张网络分配子网,比如 172.18.0.0/16⁠,每个容器拿到一个内部 IP。

容器访问外网时,Docker 会做一层地址伪装,也就是常说的 NAT。外面的服务通常看不到容器自己的内部 IP,只看到宿主机出去的地址。

外部访问容器时,逻辑反过来。你写了:

-p 8080:80

Docker 就把宿主机 8080 上的流量转发到容器 80⁠。端口发布可以理解成 Docker 在中间加了一条转发规则,容器自己并没有真的变成宿主机端口。

▎端口发布只管宿主机访问

跑一个 Nginx:

docker run -d --name web -p 8080:80 nginx

这句里的 -p 8080:80⁠,左边是宿主机端口,右边是容器端口。

宿主机 localhost:8080 -> web 容器 80

浏览器访问 http://localhost:8080⁠,Docker 会把流量转到容器里的 80⁠。

但另一个容器访问它时,不应该写 localhost:8080⁠。如果它们在用户自定义 bridge 网络或 Compose 网络里,应该写:

web:80

这就是很多人最容易混的地方:ports 是给宿主机进容器用的,不是给容器之间互相找服务用的。

还可以把端口只绑到本机:

docker run -d --name web -p 127.0.0.1:8080:80 nginx

这样通常只有宿主机本机能访问。开发数据库、Redis、管理后台时,更建议这样绑定,少开一个对局域网暴露的口子。

Dockerfile 里的 EXPOSE 也顺手提一下:

EXPOSE 8080

它更像说明书,告诉别人这个镜像里的程序预期监听 8080⁠。它不会自动把端口发布到宿主机。真正发布端口,还是靠 -p⁠。

▎默认 bridge 和自定义 bridge 的差别

默认 bridge 适合临时实验。

正式一点的项目,更建议创建用户自定义网络。原因有两个。

一个是隔离清楚。这个项目的容器放 app-net⁠,另一个项目放别的网络,互相别掺。

另一个是名字访问更自然。应用连数据库写 db:5432⁠,连 Redis 写 redis:6379⁠,不用追着容器 IP 跑。

比如这样建一张网络:

docker network create app-net

然后把容器挂进去:

docker run -d --name db --network app-net postgres:18docker run -d --name redis --network app-net redisdocker run -it --rm --network app-net alpine sh

进到 Alpine 容器里,访问数据库就用 db:5432⁠,访问 Redis 就用 redis:6379⁠。这里的 db 和 redis 来自容器名。用户自定义 bridge 网络会提供名字解析,IP 会变,名字稳得多。

查看网络详情:

docker network inspect app-net

这条命令很有用。它能看到网络的子网、网关、接入的容器、容器 IP。网络不通时,可以先看这里。

容器启动后也能接入网络:

docker network connect app-net some-container

也能断开:

docker network disconnect app-net some-container

调试时挺方便。比如起了一个临时工具容器,忘了接项目网络,补一下就行。

▎其他网络驱动什么时候用?

日常主力是 bridge⁠,但 Docker 不止它。

host 是让容器直接使用宿主机网络。少了一层隔离,也少了一层端口转发。某些代理、监控、性能测试场景会用它。普通 Web 服务不建议默认选它,因为端口冲突和边界问题会更直接。

docker run --rm --network host nginx

none 基本就是不给容器配网络,只剩 loopback。适合完全不需要网络的任务,或者你想自己手动配置网络。

docker run --rm --network none alpine ip link show

overlay 用来跨多个 Docker daemon 通信,常见于 Swarm。单机 Compose 开发基本碰不到。

macvlan 会让容器像局域网里一台独立机器,有自己的 MAC 地址。某些老系统迁移、局域网直连场景会用。这个就要懂宿主机网卡、交换机、网段和路由了。

ipvlan 和 macvlan 接近,但不额外分配独立 MAC。网络环境限制 MAC 数量时,它会更合适。

可以按场景这样选:

本机开发、多容器联调         bridge单机部署,大多数 Web 服务    bridge容器要贴宿主机网络           host容器完全不需要网络           none多台机器上的容器互通         overlay容器要像局域网独立设备       macvlan / ipvlan

没碰到对应场景,不用硬上复杂驱动。

▎Compose 只是把这些关系写下来

现在再看 Docker Compose,就顺了。

比如一个应用加数据库:

services:  app:    build: .    environment:      DATABASE_URL: postgresql://postgres:secret@db:5432/app    ports:      - "127.0.0.1:8080:8080"    depends_on:      - db  db:    image: postgres:18    environment:      POSTGRES_PASSWORD: secret      POSTGRES_DB: app

执行:

docker compose up -d

Compose 会自动创建一张默认网络,名字通常是:

项目名_default

app 和 db 都接到这张网络里。Docker 会提供内部 DNS,所以 app 能用 db 这个服务名访问数据库。

这就是前面那条规则在 Compose 里的落地:DATABASE_URL 里写 db⁠,ports 里写 127.0.0.1:8080:8080⁠。服务之间靠名字,宿主机访问靠端口发布。

▎Compose 里的 ports 和 expose

在 Compose 里,ports 会发布端口到宿主机:

services:  web:    image: nginx    ports:      - "8080:80"

更安全一点的本地写法,是只绑定本机地址:

services:  db:    image: postgres:18    ports:      - "127.0.0.1:5432:5432"

这样本机数据库客户端能连,局域网其他机器连不到。

expose 不会发布到宿主机:

services:  db:    image: postgres:18    expose:      - "5432"

它更像内部声明。实际项目里,同一张 Compose 网络里的服务本来就能访问监听端口,很多时候不需要专门写 expose⁠。

▎什么时候在 Compose 里自定义 networks?

小项目不需要。一个 app⁠、一个 db⁠、一个 redis⁠,默认网络通常够用:应用连数据库写 db:5432⁠,连 Redis 写 redis:6379⁠。

需要自定义网络,通常是为了表达边界。

Docker Compose 多网络拓扑示意

比如前端代理能访问应用,但不能访问数据库:

services:  proxy:    image: nginx    ports:      - "127.0.0.1:8080:80"    networks:      - frontend  app:    build: ./app    networks:      - frontend      - backend  db:    image: postgres:18    networks:      - backendnetworks:  frontend:    driver: bridge  backend:    driver: bridge

访问关系很清楚:

proxy -> app      可以app   -> db       可以proxy -> db       不通

这类配置的价值,是让架构边界落在文件里。后来别人看 compose.yaml⁠,不用猜数据库到底暴露给谁。

▎几个真正会用到的 Compose 网络配置

driver 用来指定网络驱动:

networks:  backend:    driver: bridge

本地开发基本就是 bridge⁠。

name 可以固定真实网络名:

networks:  backend:    name: myapp-backend    driver: bridge

默认情况下,Compose 会把项目名拼进网络名,比如 demo_backend⁠。你希望脚本或其他项目引用固定网络名时,可以写 name⁠。

external 用来接入已经存在的网络:

docker network create inter-project
services:  api:    build: ./api    networks:      - sharednetworks:  shared:    external: true    name: inter-project

这个适合多个 Compose 项目本地联调。比如认证服务一个项目,业务服务另一个项目,两边都接到 inter-project⁠,就能互相访问。

aliases 可以给服务加别名:

services:  app:    build: ./app    networks:      backend:        aliases:          - api.internalnetworks:  backend:

老项目里如果写死了 api.internal⁠,一时不想改代码,可以先用 alias 接住。别名太多会乱,迁移时用用还行。

ipam 用来自定义网段:

services:  db:    image: postgres:18    networks:      backend:        ipv4_address: 172.28.5.10networks:  backend:    driver: bridge    ipam:      config:        - subnet: 172.28.0.0/16          ip_range: 172.28.5.0/24          gateway: 172.28.5.254

能用服务名,就别固定 IP。固定 IP 适合 VPN 网段冲突、老系统白名单、网络实验这些场景。普通业务配置里,它会增加维护成本。

internal 可以创建内部网络:

networks:  backend:    internal: true

这个适合数据库、缓存、内部队列之间的封闭网络。下手前要想清楚应用是否需要访问外部 API、对象存储、公司代理。如果服务只接了这张 internal 网络,应用自己也可能出不去。

extra_hosts 可以往容器的 /etc/hosts 里塞主机名映射:

services:  app:    build: ./app    extra_hosts:      - "api.staging:192.168.1.100"      - "host.docker.internal:host-gateway"

这个最常见的用途,是让容器访问宿主机上的服务。比如本机跑了一个 Mock 服务、MySQL、Redis,容器里不能写 localhost⁠,因为那会指向容器自己。用 host.docker.internal 更清楚。Docker Desktop 上这个名字比较常见;Linux 环境里,可以显式加一行 host.docker.internal:host-gateway⁠,少一点环境差异。

network_mode 是另一条路:

services:  monitor:    image: my-monitor    network_mode: host

它会直接指定容器网络模式,绕开“接入某张 Compose 网络”这条常规路径。常见值有 host⁠、none⁠、bridge⁠,还有 service:xxx⁠。

比如 sidecar 场景:

services:  app:    image: my-app  debug:    image: nicolaka/netshoot    network_mode: "service:app"

debug 会复用 app 的网络命名空间,就像贴着 app 做网络调试。这个挺有用,但别乱写。Compose 里用了 network_mode⁠,就不能同时给这个服务写 networks⁠。普通业务服务优先用 networks⁠,只有明确需要贴宿主机网络、关闭网络、或者复用某个服务网络时,再碰 network_mode⁠。

dns 适合处理解析问题:

services:  app:    image: my-app    dns:      - 10.0.0.2      - 8.8.8.8

公司内网、自建域名、VPN 环境里,容器偶尔会解析不到内部域名。这时可以给服务指定 DNS。别一遇到访问失败就改 DNS,先用 getent hosts xxx 或 nslookup xxx 确认真是解析问题。

还有一个老配置叫 links⁠。现在基本不建议用了。Compose 默认网络和 service name 已经能解决服务发现,links 更多是历史包袱,看到老项目里有,知道它大概是干嘛的就行。

▎连不上时,按这个顺序查

先看服务状态:

docker compose ps

看网络:

docker network lsdocker network inspect 项目名_default

进应用容器:

docker compose exec app sh

看服务名能不能解析:

getent hosts db

看端口通不通:

nc -vz db 5432

镜像里没这些工具,就起一个调试容器接到同一张网:

docker run --rm -it --network 项目名_default nicolaka/netshoot

再测:

dig dbnc -vz db 5432curl -v http://app:8080

按这个顺序判断:

连接地址是不是写了 localhost服务名写对没两个容器是不是在同一张网络目标服务有没有启动完成目标服务监听的是 0.0.0.0 还是 127.0.0.1宿主机访问时有没有 portsports 绑定的是 127.0.0.1 还是 0.0.0.0是不是 VPN、代理、防火墙、IPv6 在影响

还有一点这里补充说明一下:depends_on 主要解决启动顺序,不保证数据库已经准备好接连接。网络通了,服务没就绪,应用一样会报错。应用侧最好有重试;如果想让 Compose 等数据库健康状态,还要给数据库配 healthcheck⁠,并使用 depends_on 的长语法配置 condition: service_healthy⁠。

这篇先讲到这里。后面有机会再单独聊聊 Docker 网络排查和线上部署里的坑。

· END ·

如果你愿意,欢迎关注。后面继续分享更多实用内容~

0

评论 (0)

取消