Docker Compose部署多服务时网络模式选bridge还是host

Docker Compose里部署多服务,网络模式最常见的纠结就是 bridge 和 host。实际使用中发现,大多数业务并不是卡在“容器能不能联网”,而是卡在端口暴露、服务互访、性能损耗、故障排查这几件事上。

默认情况下,Compose创建的是 user-defined bridge network。这个和 Docker 默认的 bridge 不是一回事,Compose里的服务可以直接用 service name 互相访问,比如 app 访问 mysql:3306、redis:6379,不需要写容器IP。容器重启、IP变化都不影响服务名解析,这一点对多服务编排很关键。

bridge模式更适合多数业务系统

Web应用、API服务、MySQL、Redis、RabbitMQ、Nginx、后台任务这些放在一组 Compose 里,bridge 通常是更稳的选择。原因很直接:容器之间隔离清楚,对外只暴露需要暴露的端口,服务之间靠内部网络通信。

比如一个常见结构:nginx 对外暴露 80/443,app 不对公网开放,只给 nginx 访问;mysql、redis 只在 bridge 网络里跑,宿主机外部访问不到。这样即使服务器公网IP被扫端口,外面也只能看到 nginx,数据库不会裸露出去。

Compose配置大概是这种思路:

services:
  nginx:
    image: nginx
    ports:
      - "80:80"
      - "443:443"
    depends_on:
      - app

  app:
    image: your-app
    environment:
      MYSQL_HOST: mysql
      REDIS_HOST: redis

  mysql:
    image: mysql:8
    environment:
      MYSQL_ROOT_PASSWORD: password

  redis:
    image: redis:7

这里 app 访问 mysql,不是访问 127.0.0.1,也不是访问宿主机IP,而是访问 mysql:3306。这个地方很多人刚用 Compose 时会踩坑。容器里的 127.0.0.1 指的是容器自己,不是宿主机,也不是其他容器。

host模式的特点很直接:容器共用宿主机网络

host network 下,容器不再有独立的 network namespace,容器进程直接使用宿主机网络栈。容器监听 8080,宿主机就直接监听 8080,不需要 ports 映射,也没有 NAT 转发。

Compose里写法一般是:

services:
  gateway:
    image: your-gateway
    network_mode: host

这里补充一点,使用 network_mode: host 之后,ports 配置就没有意义了。因为容器已经直接占用宿主机端口,再写 ports Docker 也不会按 bridge 那套逻辑做端口映射。

host模式在 Linux 服务器上比较常用,Docker Desktop 上表现不完全一样,尤其是 Mac 和 Windows,本身 Docker 跑在虚拟机里,host 不是你本机真正的 host。生产环境如果是 Linux 云服务器,这个问题不大。

性能差异:别神化host,但高并发网关确实会有收益

bridge模式有一层 veth、iptables/nftables、NAT 转发。正常 Web业务里,这点开销一般不明显。实际跑 API、后台管理、普通企业站,瓶颈更多在应用逻辑、数据库、磁盘IO、外部接口,不在 Docker bridge。

但在高连接数、高 PPS、高吞吐的场景,host 的优势会变得明显一些。比如游戏网关、UDP服务、实时通信入口、代理转发服务、L4转发、采集 Agent,这类服务对网络路径更敏感。

经验值可以这样看:

场景 | bridge表现 | host表现 | 备注
普通HTTP API,几百到几千 QPS | 足够用 | 差异不明显 | 应用和数据库更容易成为瓶颈
Nginx反向代理,1Gbps以内 | 通常没问题 | CPU占用略低 | 具体看连接数和TLS开销
UDP游戏网关,高 PPS | 可能更容易遇到抖动 | 更直接 | NAT和conntrack压力要关注
node_exporter、node-local Agent | 不方便拿宿主机网络视角 | 更合适 | 监控类服务常用host
MySQL、Redis这类内部服务 | 不建议直接host | 风险更高 | 容易暴露端口

多说一句,host不是性能魔法。云服务器网络质量、CPU型号、内核参数、conntrack、应用线程模型、DDoS清洗链路都会影响结果。比如同样跑网关服务,普通海外线路和优质线路在跨境延迟、丢包率上的差别,比 bridge 和 host 的差别更明显。

如果业务是游戏、海外访问、企业站或高防场景,选机器时网络线路要一起看。比如需要香港方向抗攻击入口,可以看 129云 的香港高防-C型,8C 8G、250Mbps峰值、200Gbps单机防御,适合把高防IP前置到网关或反代层;如果是新加坡企业建站,新加坡-A型 2C 2G、10Mbps峰值更偏轻量业务;美国精品网-B型 4C 4G、75Mbps峰值适合三网精品线路访问。需要按业务流量和防护强度确认,也可以直接打客服热线 400-9177118 问线路和防御细节。

多服务部署时,bridge的服务发现更省心

Compose的 user-defined bridge 有内置 DNS。服务名就是域名,这个能力在多服务部署里非常好用。比如 app 连 redis,写 redis:6379;app 连 mysql,写 mysql:3306;nginx 转发 app,写 http://app:8080。

host模式下就没有这套隔离网络里的服务发现了。大家都跑在宿主机网络里,服务之间通常靠 127.0.0.1 加端口,或者宿主机IP加端口。这样看起来简单,但多服务一多,端口管理就会变成负担。

比如两个服务都想监听 8080,bridge 下没问题,因为每个容器都有自己的网络空间;host 下不行,宿主机只有一个 8080,谁先占谁赢,另一个直接启动失败。

这也是为什么业务系统里,app、worker、db、cache 放 bridge 更舒服;真正需要贴近宿主机网络的入口服务,再单独考虑 host。

端口暴露方式差异很大

bridge模式对外暴露端口靠 ports:

services:
  app:
    image: your-app
    ports:
      - "8080:8080"

这个意思是宿主机 8080 转发到容器 8080。也可以写成 "127.0.0.1:8080:8080",只允许宿主机本地访问,公网访问不到。这个写法在部署管理后台、内部 API 时很有用。

host模式则是应用监听什么地址,就直接决定宿主机暴露什么地址。应用如果监听 0.0.0.0:8080,公网网卡也会暴露 8080;如果只监听 127.0.0.1:8080,那只能本机访问。

实际排障时,bridge 看 docker compose ps、docker inspect、iptables/nftables;host 直接看 ss -lntup、宿主机防火墙、应用监听地址。host 少了一层 Docker NAT,但安全边界也少了一层。

数据库和中间件不建议随便用host

MySQL、PostgreSQL、Redis、MongoDB、Elasticsearch 这类服务,除非有明确原因,不建议直接 network_mode: host。不是说不能跑,而是容易误暴露。

很多事故不是黑客技术多复杂,就是 Redis 6379、MongoDB 27017、Elasticsearch 9200 暴在公网,弱口令或无认证,扫到就出事。bridge 模式下只要不写 ports,外部根本连不到,安全基线会好很多。

如果确实需要宿主机访问数据库,可以只绑定本地地址:

services:
  mysql:
    image: mysql:8
    ports:
      - "127.0.0.1:3306:3306"

这样宿主机上的备份脚本、迁移工具可以连 127.0.0.1:3306,公网不能直接访问。云服务器安全组再把 3306 禁掉,双层限制更稳。

host适合放在入口层、采集层、网络敏感服务

host比较适合这些服务:高并发网关、UDP服务、DNS服务、node_exporter、cAdvisor某些宿主机采集需求、需要读取宿主机网络状态的 Agent、需要广播或组播的服务、L4代理、VPN类服务。

比如 node_exporter 用 host 网络就很自然:

services:
  node-exporter:
    image: prom/node-exporter
    network_mode: host
    pid: host
    volumes:
      - /:/host:ro,rslave

它本来就是看宿主机状态的,用 bridge 反而需要绕。网关类服务也是类似,少一层端口映射,监听和排查都更贴近宿主机。

但如果一个 Compose 文件里十几个服务全写 host,后面维护会比较难。端口冲突、服务名不可用、暴露面扩大、环境迁移不一致,这些问题会慢慢冒出来。

bridge和host混用时要注意通信方式

Compose里可以部分服务 bridge,部分服务 host。比如 nginx 或 gateway 用 host,app、mysql、redis 继续 bridge。这个架构在一些高并发入口场景里会用到。

但这里有个细节:host网络里的容器,不能直接通过 app 这个 Compose service name 访问 bridge 网络里的 app。因为它不在那个 bridge network 里。处理方式通常有两种。

一种是 app 通过 ports 绑定到 127.0.0.1,比如 127.0.0.1:18080:8080,然后 host 模式的 gateway 访问 127.0.0.1:18080。

另一种是 gateway 不用 host,也放到 bridge 里,由 Docker 负责端口映射。除非 gateway 的网络性能压力确实明显,否则这种更简单。

示例:

services:
  gateway:
    image: your-gateway
    network_mode: host

  app:
    image: your-app
    ports:
      - "127.0.0.1:18080:8080"

gateway 访问 http://127.0.0.1:18080,流量再进入 app 容器。这比把 app 直接暴露到 0.0.0.0:8080 安全一些。

云服务器环境下还要看安全组和防火墙

Docker层面的 ports、host 只是服务器内部网络绑定方式。到了云服务器,还会经过安全组、防火墙、DDoS防护、WAF、负载均衡这些外层限制。

bridge模式里写了 ports,不代表公网一定能访问,还要看安全组有没有放行。host模式里应用监听 0.0.0.0:8080,也不代表外面一定通,安全组没开还是不通。

排查顺序建议按真实链路来:应用是否监听、容器网络是否正常、宿主机端口是否存在、本机 curl 是否通、安全组是否放行、外部网络是否可达、线路是否有丢包或清洗策略。

常用命令:

ss -lntup
docker compose ps
docker compose logs -f 服务名
docker inspect 容器名
curl -v http://127.0.0.1:端口
curl -v http://宿主机公网IP:端口
iptables -t nat -L -n
nft list ruleset

如果是高防服务器,还要确认业务端口是否接入高防策略。有些高防产品默认只放行常见端口,非标准端口要单独配置转发或策略。

实际选择时可以按服务角色拆开看

业务应用:优先 bridge。服务名互访、隔离清楚、迁移方便。

数据库和缓存:优先 bridge,不需要公网访问就不要 ports;需要宿主机访问就绑定 127.0.0.1。

反向代理:普通业务用 bridge 加 ports 就行;高并发入口、UDP、特殊网络需求再考虑 host。

监控 Agent:看采集对象。如果是采集宿主机网络、进程、端口,host 更合适;如果只是采集应用指标,bridge 也可以。

游戏网关、实时通信、代理转发:可以压测 bridge 和 host。重点看 CPU softirq、conntrack、PPS、延迟抖动,不要只看平均 QPS。

容易踩的坑

容器里访问 127.0.0.1,以为能访问宿主机服务。bridge模式下这通常是错的,127.0.0.1 是容器自己。需要访问宿主机可以用宿主机网关地址,或者 Docker Desktop 下用 host.docker.internal,Linux上可通过 extra_hosts 配置 host-gateway。

把 mysql、redis 端口直接映射到 0.0.0.0。测试阶段图方便,线上忘了收,风险很高。

host模式还写 ports,结果发现不生效。network_mode: host 下 ports 不按 bridge 逻辑工作。

多个 host 服务抢同一个端口。bridge里可以多个容器都监听 8080,只要宿主机映射端口不同;host里不行。

以为 host 一定比 bridge 快很多。普通HTTP业务里,差距经常被数据库、TLS、应用代码、磁盘IO掩盖。真要判断,用 wrk、iperf3、tcpdump、sar、pidstat、perf 这些工具看数据。

一个更贴近生产的Compose拆法

大多数线上服务可以这样拆:入口层只暴露 80/443,业务层和数据层都不暴露公网。需要高性能入口时,再单独评估入口层 host。

services:
  nginx:
    image: nginx
    ports:
      - "80:80"
      - "443:443"
    depends_on:
      - app

  app:
    image: your-app
    environment:
      DB_HOST: mysql
      REDIS_HOST: redis

  mysql:
    image: mysql:8
    volumes:
      - ./mysql:/var/lib/mysql

  redis:
    image: redis:7

networks:
  default:
    driver: bridge

如果入口层要改 host,可以把 nginx/gateway 拿出来单独处理,app 通过 127.0.0.1 绑定端口给它访问,数据库和缓存仍然留在 bridge 里:

services:
  gateway:
    image: your-gateway
    network_mode: host

  app:
    image: your-app
    ports:
      - "127.0.0.1:18080:8080"
    environment:
      DB_HOST: mysql
      REDIS_HOST: redis

  mysql:
    image: mysql:8

  redis:
    image: redis:7

这个写法的重点不是配置多复杂,而是入口、应用、数据三层的边界清楚。入口承接公网流量,应用只给入口访问,数据服务只给应用访问。后面要接 WAF、高防IP、负载均衡、备份脚本,也比较好改。