VPS上的Docker网络:bridge、host和macvlan详解
Docker的bridge、host和macvlan网络在单台VPS上如何工作。涵盖自定义bridge的DNS解析、端口发布、IPv6配置以及Docker Compose网络隔离。
你有一台VPS。多个容器需要互相通信,也需要与外部世界通信。Docker提供了三种相关的网络驱动(network driver):bridge、host和macvlan。每种驱动在隔离性、性能和便利性之间做出不同的权衡。
本文说明何时选择哪种驱动,如何通过自定义bridge网络和Docker Compose连接容器,以及如何避免那些导致服务不可达或意外暴露的常见错误。
前提条件: 基本的Docker CLI知识以及对Docker Compose的了解。
Docker bridge和host网络有什么区别?
Bridge网络为每个容器提供独立的网络命名空间(network namespace),通过虚拟以太网对(veth)将其连接到宿主机上的软件bridge。容器获得私有IP(通常在172.17.0.0/16范围内),通过iptables管理的NAT访问外部世界。Host网络完全移除网络命名空间:容器共享宿主机的网络栈,直接绑定宿主机端口,无需地址转换。
快速对比:
| 标准 | 自定义Bridge | Host | Macvlan |
|---|---|---|---|
| 网络隔离 | 完全(独立namespace) | 无(共享宿主机) | 完全(独立MAC地址) |
| DNS解析 | 按容器名自动解析 | 使用宿主机DNS | 无内置DNS |
| 端口映射 | 需要(-p) |
不需要(直接绑定) | 不需要(真实IP) |
| 性能开销 | 较小(NAT + veth) | 无 | 无 |
| 安全风险 | 低(默认隔离) | 高(无网络边界) | 中(需要混杂模式) |
| VPS适用性 | 首选 | 监控、高吞吐场景 | 极少可用 |
为什么要用自定义bridge网络而不是默认bridge?
默认bridge网络(名为docker0)不提供容器间的DNS解析。容器只能通过IP地址互相访问,而IP地址在每次重启时都会变化。自定义bridge网络提供自动DNS、更好的隔离,以及运行时的动态连接/断开。所有场景都使用自定义bridge。把默认bridge当作历史遗留。
DNS问题
Docker启动时会创建一个默认bridge网络。没有显式--network标志的容器都会加入该网络。但默认bridge上的容器无法通过名称互相解析:
# Start two containers on the default bridge
docker run -d --name web nginx:alpine
docker run -d --name client alpine sleep 3600
# Try name resolution - this fails
docker exec client ping -c1 web
# ping: bad address 'web'
用自定义bridge试试:
# Create a custom bridge network
docker network create app-net
# Start containers on it
docker run -d --name web2 --network app-net nginx:alpine
docker run -d --name client2 --network app-net alpine sleep 3600
# Name resolution works
docker exec client2 ping -c1 web2
# PING web2 (172.18.0.2): 56 data bytes
# 64 bytes from 172.18.0.2: seq=0 ttl=64 time=0.089 ms
Docker内嵌DNS的工作原理
用户自定义网络上的每个容器都会获得127.0.0.11作为DNS服务器。这是Docker的内嵌DNS服务器,负责将容器名称和服务别名解析为当前IP地址。如果名称不是容器名,则将查询转发到宿主机配置的DNS服务器。
docker exec client2 cat /etc/resolv.conf
# nameserver 127.0.0.11
DNS服务器运行在容器的网络命名空间内,但实际解析发生在宿主机上的Docker守护进程中。为了避免与容器内可能使用53端口的服务冲突,Docker的DNS监听器在内部使用随机高端口,通过iptables规则重定向查询。
没有等效的IPv6地址。127.0.0.11地址在纯IPv6容器中也能正常工作。
内嵌DNS服务器为容器名称记录返回600秒(10分钟)的TTL。这对蓝绿部署很重要:如果你替换了一个容器,其他容器可能在10分钟内仍然解析到旧IP。积极缓存DNS的应用(Java默认无限期缓存)会保持过期地址更长时间。
同一自定义bridge网络上的容器无需任何-p标志就能互相访问所有端口。端口发布只控制来自网络外部(宿主机或互联网)的访问。app-net上的两个容器可以在任何端口上自由通信。
默认bridge与自定义bridge对比
| 特性 | 默认bridge(docker0) |
自定义bridge |
|---|---|---|
| 按容器名DNS解析 | 否 | 是 |
| 动态连接/断开 | 否(需要重启容器) | 是 |
| 按网络可配置 | 否(需编辑daemon.json并重启) |
是 |
| 与其他栈隔离 | 否(所有未分配容器共享) | 是 |
| 推荐用于生产 | 否 | 是 |
什么时候在VPS上使用host网络?
当容器需要原始网络性能或必须动态绑定大量端口时,使用host网络。容器直接共享宿主机的网络栈,绕过NAT和虚拟以太网bridge。这消除了高吞吐负载的可测量开销。
VPS上的典型使用场景:
- 监控代理如Prometheus node-exporter或Netdata,需要查看所有宿主机接口和流量
- DNS服务器需要在宿主机实际IP上监听
- 性能敏感服务,NAT开销影响显著(高包率、UDP密集型负载)
# Run a container with host networking
docker run -d --name node-exporter --network host \
prom/node-exporter:latest
使用host网络时--publish标志会被忽略。容器直接绑定到宿主机端口。如果宿主机上9100端口已被占用,容器将无法启动。
安全权衡
Host网络提供的网络隔离与直接在宿主机上运行进程相同:没有隔离。容器可以看到所有网络接口、绑定任何端口、嗅探流量。只在有明确理由时使用。
# Verify which ports a host-networked container opened
ss -tlnp | grep node_exporter
# LISTEN 0 4096 *:9100 *:* users:(("node_exporter",pid=12345,fd=3))
注意观察:输出显示进程监听在*:9100,即所有接口。使用bridge网络时,你通过-p标志控制这一点。使用host网络时,由应用本身决定绑定什么。
什么是macvlan网络,VPS上需要它吗?
Macvlan为每个容器分配独立的MAC地址,使其在网络上显示为独立的物理设备。容器从你的LAN子网获得真实IP,无需端口映射即可直接访问。
在VPS上,你几乎不需要macvlan。原因如下:
- 大多数云厂商会阻止它。 Macvlan要求物理网卡在混杂模式下运行。VPS虚拟化平台通常在虚拟交换机层面阻止此功能。
- 没有可加入的LAN。 你的VPS只有一个公网接口。Macvlan是为需要容器在现有LAN上拥有独立地址的环境设计的,比如有IoT设备的家庭实验室或需要专用IP的老旧应用。
- 宿主机与容器通信中断。 Linux内核设计上阻止macvlan容器与其运行的宿主机通信。需要变通方案,如附加第二个bridge网络。
如果你的VPS提供商给你一台拥有完整NIC访问权限和多个IP的独立服务器,macvlan才变得可行。对于标准VPS部署,使用自定义bridge网络。
作为参考,创建macvlan网络的方式如下(你在VPS上大概率不需要这个):
docker network create -d macvlan \
--subnet=192.168.1.0/24 \
--gateway=192.168.1.1 \
-o parent=eth0 \
macnet
parent选项指定要附加的宿主机接口。容器从子网中获得独立IP和独立MAC地址。LAN上的其他设备可以直接访问它。
Docker端口的expose和publish有什么区别?
Dockerfile中的EXPOSE是文档说明。它声明应用监听的端口,但不会向宿主机或外部开放该端口。运行时的--publish(或-p)才会创建从宿主机到容器的实际端口映射。没有-p,外部流量无法到达容器。
# In your Dockerfile - this is documentation only
EXPOSE 8080
# This actually maps host:8080 -> container:8080
docker run -d -p 8080:8080 myapp
# This binds to localhost only - external traffic cannot reach it
docker run -d -p 127.0.0.1:8080:8080 myapp
127.0.0.1绑定模式
当你在Nginx、Traefik或Caddy等反向代理后面运行服务时,只将容器端口发布到127.0.0.1。这样可以阻止来自互联网的直接访问,强制所有流量通过反向代理进行TLS终止、速率限制和访问控制。
# docker-compose.yml - binding to localhost only
services:
app:
image: myapp:latest
ports:
- "127.0.0.1:3000:3000" # Only reachable from localhost
验证绑定:
ss -tlnp | grep 3000
# LISTEN 0 4096 127.0.0.1:3000 0.0.0.0:* users:(("docker-proxy",...))
注意观察:输出显示127.0.0.1:3000而不是0.0.0.0:3000。这确认VPS公网IP上的外部流量无法直接访问3000端口。
不加127.0.0.1前缀时,Docker默认在所有接口上发布。在有公网IP的VPS上,这意味着你的服务暴露在互联网上,绕过了反向代理,可能也绕过了防火墙。
端口发布快速参考
| 语法 | 绑定到 | 可从何处访问 |
|---|---|---|
-p 8080:80 |
0.0.0.0:8080 + [::]:8080 |
所有地方(IPv4 + IPv6) |
-p 127.0.0.1:8080:80 |
127.0.0.1:8080 |
仅本地 |
-p 0.0.0.0:8080:80 |
0.0.0.0:8080 |
所有IPv4接口 |
-p [::1]:8080:80 |
[::1]:8080 |
仅IPv6本地 |
如何在单台VPS上隔离容器网络?
为每个应用栈创建独立的bridge网络。不同网络上的容器无法通信,除非你显式将它们连接到共享网络。对于数据库和内部服务,使用internal: true标志阻止所有出站互联网访问。
多网络架构
VPS上典型的生产配置如下:
Internet
|
[Reverse Proxy]
/ \
[frontend] [api-net]
| |
webapp api-server
|
[db-net: internal]
|
postgres
反向代理连接到frontend和api-net。API服务器连接到api-net和db-net。PostgreSQL仅在db-net上,没有通往互联网的路由。
以下是该配置的Docker Compose文件:
services:
proxy:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
networks:
- frontend
- api-net
webapp:
image: mywebapp:latest
networks:
- frontend
api:
image: myapi:latest
environment:
- DATABASE_URL=postgresql://appuser:${DB_PASSWORD}@db:5432/appdb
networks:
- api-net
- db-net
db:
image: postgres:16-alpine
environment:
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- db-net
networks:
frontend:
driver: bridge
api-net:
driver: bridge
db-net:
driver: bridge
internal: true # No internet access for the database network
volumes:
pgdata:
db-net上的internal: true意味着PostgreSQL无法访问互联网。它不能下载更新、不能与外部服务器通信,也不能被利用作为出站连接的跳板。API服务器可以访问数据库,因为它同时连接了api-net和db-net。
验证网络隔离
启动整个栈后,确认隔离生效:
# List networks created by Compose
docker network ls --filter "name=myproject"
# Inspect a network to see which containers are connected
docker network inspect myproject_db-net --format '{{range .Containers}}{{.Name}} {{end}}'
# myproject-api-1 myproject-db-1
# Confirm the database cannot reach the internet
docker exec myproject-db-1 ping -c1 -W2 8.8.8.8
# ping: sendto: Network unreachable
注意观察:"Network unreachable"确认internal: true标志生效。数据库容器没有默认网关。
如何为Docker容器启用IPv6?
在/etc/docker/daemon.json中全局启用IPv6,然后在每个网络上通过子网启用。Docker在Linux上原生支持双栈(IPv4 + IPv6)。ip6tables参数默认启用,负责管理容器的IPv6防火墙规则。
第1步:配置daemon
编辑/etc/docker/daemon.json:
{
"ipv6": true,
"fixed-cidr-v6": "fd00:dead:beef::/64",
"ip6tables": true,
"default-address-pools": [
{ "base": "172.17.0.0/16", "size": 24 },
{ "base": "fd00:dead:beef::/48", "size": 64 }
]
}
fixed-cidr-v6为默认bridge分配一个IPv6 /64子网。default-address-pools告诉Docker如何为新网络分配子网:IPv4范围中的/24块和IPv6范围中的/64块。
使用ULA前缀(fd00::/8)进行容器间私有通信。如果你的VPS有提供商分配的公网IPv6范围,可以使用该范围的子网给需要公网IPv6地址的容器。
重启Docker使配置生效:
sudo systemctl restart docker
验证IPv6已启用:
docker network inspect bridge --format '{{.EnableIPv6}}'
# true
第2步:创建双栈网络
docker network create --ipv6 --subnet 172.20.0.0/24 --subnet fd00:dead:beef:1::/64 app-v6
或在Docker Compose中:
networks:
app-v6:
enable_ipv6: true
ipam:
config:
- subnet: 172.20.0.0/24
- subnet: fd00:dead:beef:1::/64
第3步:验证双栈连通性
docker run --rm --network app-v6 alpine ip -6 addr show eth0
# inet6 fd00:dead:beef:1::2/64 scope global
IPv6仅在Linux上运行的Docker守护进程中受支持。在较旧的系统上,可能需要在创建IPv6网络之前加载ip6_tables内核模块:
sudo modprobe ip6_tables
如何在Docker Compose中定义网络?
Docker Compose自动为每个项目创建默认网络。Compose文件中的每个服务都加入该网络,可以通过服务名解析其他服务。对于大多数单栈应用,这个默认行为就够了。
需要多个网络进行隔离时,在networks部分显式定义:
services:
web:
image: nginx:alpine
ports:
- "127.0.0.1:8080:80"
networks:
- public
app:
image: node:20-alpine
networks:
- public
- private
redis:
image: redis:7-alpine
networks:
- private
networks:
public:
driver: bridge
private:
driver: bridge
internal: true
在这个文件中,web可以通过public网络访问app。app可以访问web和redis。redis只能通过private网络访问app,没有互联网访问权限。
在Compose中使用host网络
services:
node-exporter:
image: prom/node-exporter:latest
network_mode: host
pid: host
restart: unless-stopped
network_mode: host替代所有networks定义。同一服务不能同时使用host模式和自定义网络。
连接外部网络
如果网络在Compose之外创建(由另一个栈或手动创建),将其声明为外部网络:
networks:
shared-proxy:
external: true
这让多个Compose项目共享一个网络。常见模式:一个Compose栈运行反向代理并创建网络,其他栈将其声明为外部并将服务连接到它。
Docker网络命令快速参考
| 命令 | 功能 |
|---|---|
docker network ls |
列出所有网络 |
docker network create mynet |
创建bridge网络 |
docker network create --ipv6 --subnet fd00::/64 mynet |
创建双栈网络 |
docker network inspect mynet |
显示网络详情(子网、容器、选项) |
docker network connect mynet container1 |
将运行中的容器连接到网络 |
docker network disconnect mynet container1 |
将容器从网络断开 |
docker network prune |
删除所有未使用的网络 |
docker network rm mynet |
删除指定网络 |
扩展限制
当超过1000个容器连接到单个网络时,bridge网络会变得不稳定。这是Linux内核对bridge设备的限制。如果你运行大量容器,按功能将它们分配到多个网络,而不是全部放在一个bridge上。
遇到问题?
容器无法通过名称互相解析。
你可能在默认bridge上。创建自定义网络并将两个容器都连接上去。用docker inspect <container> --format '{{json .NetworkSettings.Networks}}'检查。
端口已发布但从外部无法访问。
检查是否绑定到127.0.0.1而不是0.0.0.0。在宿主机上运行ss -tlnp | grep <port>。同时检查防火墙规则。
容器无法访问互联网。
网络可能设置了internal: true。用docker network inspect <network> --format '{{.Internal}}'检查。如果返回true,网络设计上就会阻止出站流量。
IPv6在容器中不工作。
验证daemon.json中ip6tables是否启用。在较旧的内核上,加载模块:sudo modprobe ip6_tables。检查网络是否启用了IPv6:docker network inspect <network> --format '{{.EnableIPv6}}'。
使用host网络时出现"Address already in use"。
另一个进程(或容器)已经占用了该端口。用ss -tlnp | grep <port>找到它。Host网络没有端口映射,所以冲突是直接的。
下一步
容器网络已配置完成。接下来可以做:
- 添加反向代理处理TLS和路由
- 修复Docker的防火墙绕过问题,让发布的端口遵守UFW/nftables规则
- 为Compose服务添加资源限制和健康检查
- 返回VPS上的Docker总览查看完整生产检查清单
版权所有 2026 Virtua.Cloud。保留所有权利。 本内容为 Virtua.Cloud 团队原创作品。 未经书面许可,禁止复制、转载或再分发。