Docker 网络管理

内容纲要

查看「Docker & Kubernetes 专题」获取更多相关内容


端口转发

有时候需要从外部访问容器内部的应用,如 NGINX 容器默认使用 80 端口提供的服务

那么在运行容器时可以使用 -p 选项:

# -d, --detach
# -p, --publish
# -p <宿主机端口>:<容器端口>
docker run -d -p 8080:80 --name webserver nginx

这个功能是由 iptables 实现的:

iptables -t nat -nL
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
DOCKER     all  --  0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
DOCKER     all  --  0.0.0.0/0           !127.0.0.0/8          ADDRTYPE match dst-type LOCAL

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
MASQUERADE  all  --  172.17.0.0/16        0.0.0.0/0
MASQUERADE  tcp  --  172.17.0.2           172.17.0.2           tcp dpt:80

Chain DOCKER (2 references)
target     prot opt source               destination
RETURN     all  --  0.0.0.0/0            0.0.0.0/0
DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:80 to:172.17.0.2:80

注意最后一行的 DNAT

除了 -p 进行手动指定端口也可以使用 -P:

# 运行一个 NGINX 容器使用 -P 选项
docker run -d -P --name webserver nginx

# docker port <容器名称或 ID>
docker port webserver
# 或
docker ps
167e17b8655c   nginx     "/docker-entrypoint.…"   19 seconds ago   Up 18 seconds   0.0.0.0:55000->80/tcp   webserver

如上 0.0.0.0:55000->80/tcp,此时浏览器访问 http://localhost:55000 就可以看到 NGINX 欢迎页面,与 -p 手动指定不同的是 -P 会随机端口映射到容器

那么 Docker 是怎么知道容器用到 80 端口的?答案是映像的 Dockerfile 的一行指令:

EXPOSE 80

网络 Driver

Docker 的网络子系统使用驱动程序是可插拔的。默认情况下存在几个驱动程序,它们提供核心网络功能。

Driver 类型

  • bridge:默认使用的网络驱动。当应用程序在需要通信的独立容器中运行时,通常会使用桥接网络。请参阅桥接网络

    当需要多个容器在同一台 Docker 主机上进行通信时,用户定义的桥接网络是最佳选择;

  • host:对于独立容器,去掉容器和 Docker 宿主机之间的网络隔离,直接使用宿主机的网络。请参阅使用主机网络

    当网络堆栈不应该与 Docker 主机隔离,但希望容器的其他方面被隔离时,Host 网络是最佳选择;

  • overlay:覆盖网络将多个 Docker 守护进程连接在一起,并使 swarm 服务能够相互通信。您还可以使用覆盖网络来促进 swarm 服务和独立容器之间,或不同 Docker 守护进程上的两个独立容器之间的通信。这种策略消除了在这些容器之间进行操作系统级路由的需要。请参阅覆盖网络

    当需要在不同 Docker 主机上运行的容器进行通信时,或者当多个应用程序使用 swarm 服务协同工作时,Overlay 网络是最佳选择;

  • ipvlan:IPvlan 网络让用户可以完全控制 IPv4 和 IPv6 寻址。 VLAN 驱动程序建立在其之上,使运营商能够完全控制第 2 层 VLAN 标记,甚至为对底层网络集成感兴趣的用户提供 IPvlan L3 路由。请参阅 IPvlan 网络
  • macvlan:Macvlan 网络允许您为容器分配 MAC 地址,使其在您的网络上显示为物理设备。 Docker 守护进程通过 MAC 地址将流量路由到容器。在处理希望直接连接到物理网络而不是通过 Docker 主机的网络堆栈路由的遗留应用程序时,使用 macvlan 驱动程序有时是最佳选择。请参阅 Macvlan 网络

    当从 VM 设置迁移或需要容器看起来像网络上的物理主机时,Macvlan 网络是最好的,每个主机都有一个唯一的 MAC 地址;

  • none:对于这个容器,禁用所有网络。通常与自定义网络驱动程序结合使用。 none 不适用于 swarm 服务。请参阅禁用容器网络
  • 网络插件:可以通过 Docker 安装和使用第三方网络插件,将 Docker 与专用网络堆栈集成。这些插件可从 Docker Hub 或第三方供应商处获得。请参阅供应商的文档以安装和使用给定的网络插件;

查看网络 Driver

使用命令查看 Docker 的网络,默认情况下有 3 种 DRIVER:

docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
80120126ce42   bridge    bridge    local
2c2829aa204e   host      host      local
ec57da5f9f8d   none      null      local

以容器默认使用的桥接网络为例,使用命令 docker inspect

# docker inspect <NETWORK ID>
docker inspect 80120126ce42
[
  {
    "Name": "bridge",
    "Id": "80120126ce423327b52883f274339fe3395007a9de682e1833fd6670c0c5f4ec",
    "Created": "2023-01-07T10:51:29.926950571-05:00",
    "Scope": "local",
    "Driver": "bridge",
    "EnableIPv6": false,
    "IPAM": {
      "Driver": "default",
      "Options": null,
      "Config": [
        {
          "Subnet": "172.17.0.0/16"
        }
      ]
    },
    "Internal": false,
    "Attachable": false,
    "Ingress": false,
    "ConfigFrom": {
      "Network": ""
    },
    "ConfigOnly": false,
    "Containers": {},
    "Options": {
      "com.docker.network.bridge.default_bridge": "true",
      "com.docker.network.bridge.enable_icc": "true",
      "com.docker.network.bridge.enable_ip_masquerade": "true",
      "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
      "com.docker.network.bridge.name": "docker0",
      "com.docker.network.driver.mtu": "1500"
    },
    "Labels": {}
  }
]

可以看到子网范围为 "Subnet": "172.17.0.0/16",以及网口名称 "docker0"

然后在宿主机运行命令:

ip a
docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    link/ether 02:42:4f:59:6f:5c brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:4fff:fe59:6f5c/64 scope link
       valid_lft forever preferred_lft forever

可以看到当前系统有一个名为 docker0 的虚拟网卡,IP 为 172.17.0.1

如果你使用的是 Docker Desktop 将看不到 docker0,因为 Docker Desktop 里跑着一个虚拟机

然后运行并查看一个容器的网络配置:

docker run -it --rm busybox ip a
Unable to find image 'busybox:latest' locally
latest: Pulling from library/busybox
205dae5015e7: Pull complete
Digest: sha256:7b3ccabffc97de872a30dfd234fd972a66d247c8cfc69b0550f276481852627c
Status: Downloaded newer image for busybox:latest
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop qlen 1000
    link/ipip 0.0.0.0 brd 0.0.0.0
3: ip6tnl0@NONE: <NOARP> mtu 1452 qdisc noop qlen 1000
    link/tunnel6 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00 brd 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
16: eth0@if17: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

可以看到该容器的 IP 为 172.17.0.2

一些容器并没有网络相关的工具所以并不能用命令 ip,你可以在容器运行后使用命令 docker [container] inspect <容器名称或 ID> 也可以看到容器的网络配置,例如:

...
"Networks": {
  "bridge": {
    "IPAMConfig": null,
    "Links": null,
    "Aliases": null,
    "NetworkID": "8fec80cfb8880ddf10e80275f98a8429abcc489b5deb9c5dfef89d6ec11e47c2",
    "EndpointID": "f98241be0b0d9d40c98b5bbd995c65a7fb8adcc7c0a2c72287faa7d7cc213a23",
    "Gateway": "172.17.0.1",
    "IPAddress": "172.17.0.2",
    "IPPrefixLen": 16,
    "IPv6Gateway": "",
    "GlobalIPv6Address": "",
    "GlobalIPv6PrefixLen": 0,
    "MacAddress": "02:42:ac:11:00:02",
    "DriverOpts": null
  }
}
...

并且是能与网关或其他同样在桥接网络下的容器互通的,这就像家里同个路由器下的设备

你可以新建两个终端通过命令自己试验一下:

docker run -it --rm --name box1 busybox
docker run -it --rm --name box2 busybox

在运行容器后可以使用 ping 另一个容器的 IP 或容器名称,如在 box1 上使用命令:ping box2

创建网络 Driver 及连接

# -d, --driver
# docker network create -d <Driver 类型> <Driver 名称>
docker network create -d bridge bridge2

docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
8fec80cfb888   bridge    bridge    local
5d428bbad450   bridge2   bridge    local
2c2829aa204e   host      host      local
ec57da5f9f8d   none      null      local

可以看到在本地刚才新建的名为 bridge2 的 bridge 网络

那么要将容器使用这个新建的网络呢?

# 新建一个容器时使用指定的网络 Driver
# --network <NETWORK 名称>
# 可选使用 --gateway 和 --subnet 来制定网关和子网范围
docker run -it --rm --network bridge2 --name box3 busybox

# 或者将一个现有的容器加入到指定的网络 Driver
# docker network connect <NETWORK 名称> <容器名称或 ID>
docker network connect bridge2 box1

另外,手动新建的桥接网络可以使用容器名称进行一些操作,如:ping box3


小结

  • none 是最简单的模式,也就是禁用网络;
  • host 是直接使用宿主机网络,相当于去掉了容器的网络隔离,所有的容器会共享宿主机的 IP 地址和网卡,但容易导致端口冲突;
  • bridge 是桥接模式,它有点类似现实世界里的交换机、路由器,只不过是由软件虚拟出来的。和 host 模式相比,bridge 模式多了虚拟网桥和网卡,通信效率会低一些;