34.docker(二)

文章目录

第5章 网络

none和host网络的适用场景

本章开始讨论 Docker 网络。

我们会首先学习 Docker 提供的几种原生网络,以及如何创建自定义网络。然后探讨容器之间如何通信,以及容器与外界如何交互。

Docker 网络从覆盖范围可分为单个 host 上的容器网络和跨多个 host 的网络,本章重点讨论前一种。对于更为复杂的多 host 容器网络,我们会在后面进阶技术章节单独讨论。

Docker 安装时会自动在 host 上创建三个网络,我们可用 docker network ls 命令查看:

bash 复制代码
[root@docker ~ 19:25:57]# docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
9c1dc7a72f5a   bridge    bridge    local
45c508ace404   host      host      local
3e47ca22970e   none      null      local

下面我们分别讨论它们。

none 网络

none网络的driver类型是null,IPAM字段为空。挂在none网络上的容器只有lo,无法与外界通信。

故名思议,none 网络就是什么都没有的网络。挂在这个网络下的容器除了 lo,没有其他任何网卡。容器创建时,可以通过 --network=none 指定使用 none 网络。

bash 复制代码
[root@docker ~ 19:26:19]# docker run -it --network=none busybox 
Unable to find image 'busybox:latest' locally
latest: Pulling from library/busybox
e59838ecfec5: Pull complete 
Digest: sha256:e3652a00a2fabd16ce889f0aa32c38eec347b997e73bd09e69c962ec7f8732ee
Status: Downloaded newer image for busybox:latest
/ # ifconfig 
lo        Link encap:Local Loopback  
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1/128 Scope:Host
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

/ # hostname
e515687951f7
/ # 

我们不禁会问,这样一个封闭的网络有什么用呢?

其实还真有应用场景。封闭意味着隔离,一些对安全性要求高并且不需要联网的应用可以使用 none 网络。

比如某个容器的唯一用途是生成随机密码,就可以放到 none 网络中避免密码被窃取。

当然大部分容器是需要网络的,我们接着看 host 网络。

host 网络

挂在host网络上的容器共享宿主机的network namespace。即容器的网络配置与host网络配置完全一样。

连接到 host 网络的容器共享 Docker host 的网络栈,容器的网络配置与 host 完全一样。可以通过 --network=host 指定使用 host 网络。

bash 复制代码
[root@docker ~ 19:28:03]# docker run -it --network=host busybox
#容器网络与宿主机相同
/ # ip l
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
2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq qlen 1000
    link/ether 00:0c:29:b9:3b:42 brd ff:ff:ff:ff:ff:ff
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue 
    link/ether 02:42:8c:dd:10:c4 brd ff:ff:ff:ff:ff:ff
5: vethbdd2e7f@if4: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue master docker0 
    link/ether 82:3a:a4:bc:80:ee brd ff:ff:ff:ff:ff:ff
7: veth1c2e06a@if6: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue master docker0 
    link/ether a2:8b:98:89:b2:de brd ff:ff:ff:ff:ff:ff
#容器主机与宿主机相同
/ # hostname
docker

在容器中可以看到 host 的所有网卡,并且连 hostname 也是 host 的。host 网络的使用场景又是什么呢?

直接使用 Docker host 的网络最大的好处就是性能,如果容器对网络传输效率有较高要求,则可以选择 host 网络。当然不便之处就是牺牲一些灵活性,比如要考虑端口冲突问题,Docker host 上已经使用的端口就不能再用了。

Docker host 的另一个用途是让容器可以直接配置 host 网路。比如某些跨 host 的网络解决方案,其本身也是以容器方式运行的,这些方案需要对网络进行配置,比如管理 iptables。

下一节讨论应用更广的 bridge 网络。

学容器必须懂brige网络

上一节我们讨论了 none 和 host 类型的容器网络,本节学习应用最广泛也是默认的 bridge 网络。

Docker 安装时会创建一个 命名为 docker0 的 linux bridge,实际上它是 Linux 的一个 bridge (网桥),可以理解为一个软件交换机,它会在挂载到它的网口之间进行转发。如果不指定--network,创建的容器默认都会挂到 docker0 上。

Docker 就创建了在主机和所有容器之间一个虚拟共享网络 当创建一个 Docker 容器的时候,同时会创建了一对 veth pair 接口(当数据包发送到一个接口时,另外一个接口也可以收到相同的数据包), 这对接口

  • 一端在容器内即 eth0;
  • 另一端在本地并被挂载到 docker0 网桥,名称以 veth 开头(例如 vethAQI2QT)

通过这种方式,主机可以跟容器通信,容器之间也可以相互通信。

下面演示:

先配置yum源用于安装软件

bash 复制代码
[root@docker ~ 19:42:38]# cd /etc/yum.repos.d/
[root@docker yum.repos.d 19:42:49]# vim cloud.repo
[centos-openstack-victoria]
name=CentOS 8 - OpenStack victoria
baseurl=https://mirrors.aliyun.com/centos-vault/8-stream/cloud/x86_64/openstack-victoria/
enabled=1
gpgcheck=0

[root@docker yum.repos.d 19:43:07]# yum install -y bridge-utils
bash 复制代码
[root@docker ~ 20:23:41]# brctl show
bridge name	bridge id		STP enabled	interfaces
docker0		8000.02427551b183	no	

当前 docker0 上没有任何其他网络设备,我们创建一个容器看看有什么变化。

bash 复制代码
[root@docker ~ 20:27:07]# docker run -itd --name busybox1 busybox
92c018e844eb8f5a08d38f9145c410f54d38bf7aa0d87281c87a776d713c9d2c

[root@docker ~ 20:28:25]# brctl show
bridge name	bridge id		STP enabled	interfaces
docker0		8000.02427551b183	no		veth725344a

一个新的网络接口 veth725344a 被挂到了 docker0 上,veth725344a就是新创建容器的虚拟网卡。

下面看一下容器的网络配置。

bash 复制代码
[root@docker ~ 20:29:31]# docker exec -it busybox1 sh
/ # ip a
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
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
# 容器里的网卡是4号网卡名字叫eth0,对面是5号网卡
4: eth0@if5: <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

容器有一个网卡 eth0@if5。大家可能会问了,为什么不是veth725344a 呢?

实际上 eth0@if5veth725344a 是一对 veth pair。veth pair 是一种成对出现的特殊网络设备,可以把它们想象成由一根虚拟网线连接起来的一对网卡,网卡的一头(eth0@if5)在容器中,另一头(veth725344a)挂在网桥 docker0 上,其效果就是将 eth0@if5 也挂在了 docker0 上。

在宿主机上查看IP地址,可以证明Docker0上的veth725344a和容器中的eth0@if15 是一对

在宿主机看到有块网卡:5: veth725344a@if4

宿主机的15号网卡:5: veth725344a@if4 含义就是宿主机5号网卡对面连接了一块4号网卡,5号网卡的名字叫veth725344a

证明了容器busybox1里的eth0连接到了docker0网桥的veth725344a

bash 复制代码
[root@docker ~ 03:22:17]# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default 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
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 00:0c:29:b9:3b:42 brd ff:ff:ff:ff:ff:ff
    altname enp3s0
    inet 192.168.108.30/24 brd 192.168.108.255 scope global noprefixroute ens160
       valid_lft forever preferred_lft forever
    inet6 fe80::20c:29ff:feb9:3b42/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:75:51:b1:83 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:75ff:fe51:b183/64 scope link 
       valid_lft forever preferred_lft forever
5: veth725344a@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default 
    link/ether 16:6b:e6:ae:7a:c4 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::146b:e6ff:feae:7ac4/64 scope link 
       valid_lft forever preferred_lft forever

我们还看到eth0@if5 已经配置了 IP 172.17.0.2,为什么是这个网段呢?让我们通过 docker network inspect bridge 看一下 bridge 网络的配置信息:

bash 复制代码
[root@docker ~ 03:22:58]# docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "2989b0a5dd84fcbd857d54fbecdc80fe57cbfa151d3fb3c18481a2fabbe42990",
        "Created": "2025-11-26T03:15:13.412122277+08:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "92c018e844eb8f5a08d38f9145c410f54d38bf7aa0d87281c87a776d713c9d2c": {
                "Name": "busybox1",
                "EndpointID": "30202d3ede45f5865d9d2ce78b288027b7ef95b5c14fcde9646552d49f0947bb",
                "MacAddress": "02:42:ac:11:00:02",
                "IPv4Address": "172.17.0.2/16",
                "IPv6Address": ""
            }
        },
        "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": {}
    }
]

原来 bridge 网络配置的 subnet 就是 172.17.0.0/16,并且网关是 172.17.0.1。这个网关在哪儿呢?大概你已经猜出来了,就是 docker0。

bash 复制代码
[root@docker ~ 03:24:31]# ip a |grep docker0
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0

容器创建时,docker 会自动从 172.17.0.0/16 中分配一个 IP,这里 16 位的掩码保证有足够多的 IP 可以供容器使用。

除了 none, host, bridge 这三个自动创建的网络,用户也可以根据业务需要创建 user-defined 网络

如何自定义容器网络?

除了 none, host, bridge 这三个自动创建的网络,用户也可以根据业务需要创建 user-defined 网络。

Docker 提供三种 user-defined 网络驱动:bridge, overlay 和 macvlan。overlay 和 macvlan 用于创建跨主机的网络,我们后面有章节单独讨论。

我们可通过 bridge 驱动创建类似前面默认的 bridge 网络,例如:

bash 复制代码
[root@docker ~ 21:14:42]# docker network create --driver bridge my_net
b65c10754bb107d4a7da706e3ac2503dcea9b6c9c698935176d6ec1fe339075c

查看一下当前 host 的网络结构变化:

bash 复制代码
[root@docker ~ 21:15:02]# brctl show
bridge name	bridge id		STP enabled	interfaces
br-b65c10754bb1		8000.0242a45e97b9	no	
 #my_net,看bridge  name和刚才创建的my_net id一样
docker0		8000.02428cdd10c4	no		vethb8e7eb4

新增了一个网桥 br-b65c10754bb1,这里 br-b65c10754bb1 正好是新建 bridge 网络 my_net 的短 id。执行 docker network inspect 查看一下 my_net 的配置信息:

bash 复制代码
[root@docker ~ 20:30:51]# docker network inspect my_net
[
    {
        "Name": "my_net",
        "Id": "b65c10754bb107d4a7da706e3ac2503dcea9b6c9c698935176d6ec1fe339075c",
        "Created": "2025-11-18T21:15:02.338092759+08:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.18.0.0/16",
                    "Gateway": "172.18.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {},
        "Labels": {}
    }
]

这里 172.18.0.0/16 是 Docker 自动分配的 IP 网段。

我们可以自己指定 IP 网段吗?

答案是:可以。

只需在创建网段时指定 --subnet--gateway 参数:

bash 复制代码
[root@docker ~ 21:15:12]# docker network create --driver bridge --subnet 172.22.16.0/24 --gateway 172.22.16.1 my_net2
4c4ad6ce54ce6bc383c7424fb69b59ccf15232fd49f13e669d8e260dc34b0e8b
[root@docker ~ 21:16:30]# brctl show
bridge name	bridge id		STP enabled	interfaces
br-4c4ad6ce54ce		8000.0242dfdd8da6	no		# my_net2
br-b65c10754bb1		8000.0242a45e97b9	no		 #my_net
docker0		8000.02428cdd10c4	no		vethb8e7eb4

[root@docker ~ 01:00:21]# docker network inspect my_net2
[
    {
        "Name": "my_net2",
        "Id": "4c4ad6ce54ce6bc383c7424fb69b59ccf15232fd49f13e669d8e260dc34b0e8b",
        "Created": "2025-11-18T21:16:30.354623858+08:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.22.16.0/24",
                    "Gateway": "172.22.16.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "158f2b178dde124e872c03f0661e53612b975e8664ada3352dfba517789f8305": {
                "Name": "busybox1",
                "EndpointID": "789ad975dd9c62c5ca49c8ae30ddbbbe834e251f9213c2f56cfaeaa2081775c8",
                "MacAddress": "02:42:ac:16:10:03",
                "IPv4Address": "172.22.16.3/24",
                "IPv6Address": ""
            },
            "962b114d246cfbe19c544a49003d7d406e37d6963404c56a5825188a1d2de750": {
                "Name": "bbox2",
                "EndpointID": "7c5cec977971e9960f66b0def92c8b11f05528b155fb65d8d54bd52c5f2c07ac",
                "MacAddress": "02:42:ac:16:10:05",
                "IPv4Address": "172.22.16.5/24",
                "IPv6Address": ""
            },
            "975482e464776283abf6bb2179da2ed4e1f17fd430904460b6b4ea915750958b": {
                "Name": "busybox3",
                "EndpointID": "bd563a5c69a22dcc56a89d8bac91ddb4df5295be64628870baf9ab1af684cd27",
                "MacAddress": "02:42:ac:16:10:08",
                "IPv4Address": "172.22.16.8/24",
                "IPv6Address": ""
            },
            "af84d5cf6213cac74f5887fd8cf2ab091c3bae95da4441c89c9b0de92d31073a": {
                "Name": "bbox1",
                "EndpointID": "70413cbf81bc3411450d1de3b72342efcf4f133237b2cd8b9b3e6ef120a256b3",
                "MacAddress": "02:42:ac:16:10:04",
                "IPv4Address": "172.22.16.4/24",
                "IPv6Address": ""
            },
            "efc97893a8e4ffb4e89782eac2c2e77093171814a11b39053bf5b9ccbf992d7f": {
                "Name": "busybox2",
                "EndpointID": "ace60288b24c11cf49ce558efb1f6d73f9d584c06379f7c74a726acc136c2306",
                "MacAddress": "02:42:ac:16:10:02",
                "IPv4Address": "172.22.16.2/24",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    }
]

这里我们创建了新的 bridge 网络 my_net2,网段为 172.22.16.0/24,网关为 172.22.16.1。与前面一样,网关在 my_net2 对应的网桥 br-4c4ad6ce54ce 上:

bash 复制代码
[root@docker ~ 21:16:30]# brctl show
bridge name	bridge id		STP enabled	interfaces
br-4c4ad6ce54ce		8000.0242dfdd8da6	no		#my_net2
br-b65c10754bb1		8000.0242a45e97b9	no		#my_net
docker0		8000.02428cdd10c4	no		vethb8e7eb4

#同时宿主机上出现了与网桥my_net,my_net2同名的网卡
#查看my_net2网卡
[root@docker ~ 21:17:29]# ip a |grep br-b65
16: br-b65c10754bb1: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    inet 172.18.0.1/16 brd 172.18.255.255 scope global br-b65c10754bb1

容器要使用新的网络,需要在启动时通过 --network 指定

bash 复制代码
[root@docker ~ 21:18:58]# docker run -it --network=my_net2 --name busybox2 busybox
/ # ip a
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
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
18: eth0@if19: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue 
    link/ether 02:42:ac:16:10:02 brd ff:ff:ff:ff:ff:ff
    inet 172.22.16.2/24 brd 172.22.16.255 scope global eth0
       valid_lft forever preferred_lft forever
# ctrl+P,ctrl+q退出容器

[root@docker ~ 21:20:08]# brctl show
bridge name	bridge id		STP enabled	interfaces
#my_net2上新增的接口veth5382c73连接容器busybox2
br-4c4ad6ce54ce		8000.0242dfdd8da6	no		veth5382c73
br-b65c10754bb1		8000.0242a45e97b9	no		
docker0		8000.02428cdd10c4	no		vethb8e7eb4

容器分配到的 IP 为 172.22.16.2。

到目前为止,容器的 IP 都是 docker 自动从 subnet 中分配,我们能否指定一个静态 IP 呢?

答案是:可以,通过--ip指定。

bash 复制代码
[root@docker ~ 21:21:36]# docker run -it --network=my_net2 --ip 172.22.16.8 --name busybox3 busybox
/ # ip a
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
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
20: eth0@if21: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue 
    link/ether 02:42:ac:16:10:08 brd ff:ff:ff:ff:ff:ff
    inet 172.22.16.8/24 brd 172.22.16.255 scope global eth0
    #确实是指定的IP
       valid_lft forever preferred_lft forever
# ctrl+P,ctrl+q退出容器

[root@docker ~ 21:22:06]# brctl show
bridge name	bridge id		STP enabled	interfaces
br-4c4ad6ce54ce		8000.0242dfdd8da6	no		veth3a57224   #my_net2   #veth3a57224是连接busybox3
							veth5382c73
br-b65c10754bb1		8000.0242a45e97b9	no		#my_net
docker0		8000.02428cdd10c4	no		vethb8e7eb4 

注:只有使用 --subnet 创建的网络才能指定静态 IP

my_net 创建时没有指定 --subnet,如果指定静态 IP 报错如下:

bash 复制代码
[root@docker ~ 21:22:14]# docker run -it --network=my_net --ip 172.18.0.8 busybox
docker: Error response from daemon: invalid config for network my_net: invalid endpoint settings:
user specified IP address is supported only when connecting to networks with user configured subnets.
See 'docker run --help'.

理解容器之间的连通性

通过前面小节的实践,当前 docker host 的网络拓扑结构如下图所示,今天我们将讨论这几个容器之间的连通性。

busybox2、busybox3 容器都挂在 my_net2 上,应该能够互通,我们验证一下:

bash 复制代码
# 登陆busybox2 ping busybox3
[root@docker ~ 21:23:41]# docker exec -it busybox2 sh
/ # ifconfig eth0
eth0      Link encap:Ethernet  HWaddr 02:42:AC:16:10:02  
          inet addr:172.22.16.2  Bcast:172.22.16.255  Mask:255.255.255.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:20 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0 
          RX bytes:1672 (1.6 KiB)  TX bytes:0 (0.0 B)

/ # ping -c 172.22.16.8
ping: invalid number '172.22.16.8'
/ # ping -c 3 172.22.16.8
PING 172.22.16.8 (172.22.16.8): 56 data bytes
64 bytes from 172.22.16.8: seq=0 ttl=64 time=0.100 ms
64 bytes from 172.22.16.8: seq=1 ttl=64 time=0.175 ms
64 bytes from 172.22.16.8: seq=2 ttl=64 time=0.120 ms

--- 172.22.16.8 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.100/0.131/0.175 ms

# ping my_net2网关地址
/ # ping -c 3 172.22.16.1
PING 172.22.16.1 (172.22.16.1): 56 data bytes
64 bytes from 172.22.16.1: seq=0 ttl=64 time=0.098 ms
64 bytes from 172.22.16.1: seq=1 ttl=64 time=0.238 ms
64 bytes from 172.22.16.1: seq=2 ttl=64 time=0.181 ms

--- 172.22.16.1 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.098/0.172/0.238 ms

可见同一网络中的容器、网关之间都是可以通信的。

my_net2 与默认 bridge 网络(docker0)能通信吗?

从拓扑图可知,两个网络属于不同的网桥,应该不能通信,我们通过实验验证一下,让 busybox2 容器 ping buxybox1 容器:

bash 复制代码
/ # ping -c 3 172.17.0.2
PING 172.17.0.2 (172.17.0.2): 56 data bytes

--- 172.17.0.2 ping statistics ---
3 packets transmitted, 0 packets received, 100% packet loss
/ # 

确实 ping 不通,符合预期。

"等等!不同的网络如果加上路由应该就可以通信了吧?"

这是一个非常非常好的想法。

确实,如果 host 上对每个网络的都有一条路由,同时操作系统上打开了 ip forwarding,host 就成了一个路由器,挂接在不同网桥上的网络就能够相互通信。下面我们来看看 docker host 满不满足这些条件呢?

ip r 查看 host 上的路由表:

bash 复制代码
[root@docker ~ 22:05:42]# ip r
default via 192.168.108.2 dev ens160 proto static metric 100 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/3e767428d95942fabad2531966d63add.png#pic_center)

172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 
172.18.0.0/16 dev br-b65c10754bb1 proto kernel scope link src 172.18.0.1 linkdown 
172.22.16.0/24 dev br-4c4ad6ce54ce proto kernel scope link src 172.22.16.1 
192.168.108.0/24 dev ens160 proto kernel scope link src 192.168.108.30 metric 100

172.17.0.0/16 和 172.22.16.0/24 两个网络的路由都定义好了。再看看 ip forwarding:

bash 复制代码
[root@docker ~ 22:05:45]# sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1

ip forwarding 也已经启用了。

条件都满足,为什么不能通行呢?

我们还得看看 iptables:

bash 复制代码
[root@docker ~ 22:06:50]# iptables-save 
# Generated by iptables-save v1.8.5 on Tue Nov 18 22:06:58 2025
*filter
:INPUT ACCEPT [7817:6436378]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [5877:592132]
:DOCKER - [0:0]
:DOCKER-ISOLATION-STAGE-1 - [0:0]
:DOCKER-ISOLATION-STAGE-2 - [0:0]
:DOCKER-USER - [0:0]
-A FORWARD -j DOCKER-USER
-A FORWARD -j DOCKER-ISOLATION-STAGE-1
-A FORWARD -o br-4c4ad6ce54ce -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -o br-4c4ad6ce54ce -j DOCKER
-A FORWARD -i br-4c4ad6ce54ce ! -o br-4c4ad6ce54ce -j ACCEPT
-A FORWARD -i br-4c4ad6ce54ce -o br-4c4ad6ce54ce -j ACCEPT
-A FORWARD -o br-b65c10754bb1 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -o br-b65c10754bb1 -j DOCKER
-A FORWARD -i br-b65c10754bb1 ! -o br-b65c10754bb1 -j ACCEPT
-A FORWARD -i br-b65c10754bb1 -o br-b65c10754bb1 -j ACCEPT
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j ACCEPT
-A DOCKER-ISOLATION-STAGE-1 -i br-4c4ad6ce54ce ! -o br-4c4ad6ce54ce -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -i br-b65c10754bb1 ! -o br-b65c10754bb1 -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -i docker0 ! -o docker0 -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -j RETURN
-A DOCKER-ISOLATION-STAGE-2 -o br-4c4ad6ce54ce -j DROP
-A DOCKER-ISOLATION-STAGE-2 -o br-b65c10754bb1 -j DROP
-A DOCKER-ISOLATION-STAGE-2 -o docker0 -j DROP
-A DOCKER-ISOLATION-STAGE-2 -j RETURN
-A DOCKER-USER -j RETURN
COMMIT
# Completed on Tue Nov 18 22:06:58 2025
# Generated by iptables-save v1.8.5 on Tue Nov 18 22:06:58 2025
*security
:INPUT ACCEPT [7808:6433426]
:FORWARD ACCEPT [6:504]
:OUTPUT ACCEPT [5877:592132]
COMMIT
# Completed on Tue Nov 18 22:06:58 2025
# Generated by iptables-save v1.8.5 on Tue Nov 18 22:06:58 2025
*raw
:PREROUTING ACCEPT [7826:6437134]
:OUTPUT ACCEPT [5877:592132]
COMMIT
# Completed on Tue Nov 18 22:06:58 2025
# Generated by iptables-save v1.8.5 on Tue Nov 18 22:06:58 2025
*mangle
:PREROUTING ACCEPT [7826:6437134]
:INPUT ACCEPT [7817:6436378]
:FORWARD ACCEPT [9:756]
:OUTPUT ACCEPT [5877:592132]
:POSTROUTING ACCEPT [5883:592636]
COMMIT
# Completed on Tue Nov 18 22:06:58 2025
# Generated by iptables-save v1.8.5 on Tue Nov 18 22:06:58 2025
*nat
:PREROUTING ACCEPT [17:3528]
:INPUT ACCEPT [4:240]
:POSTROUTING ACCEPT [136:12516]
:OUTPUT ACCEPT [135:12432]
:DOCKER - [0:0]
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.22.16.0/24 ! -o br-4c4ad6ce54ce -j MASQUERADE
-A POSTROUTING -s 172.18.0.0/16 ! -o br-b65c10754bb1 -j MASQUERADE
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A DOCKER -i br-4c4ad6ce54ce -j RETURN
-A DOCKER -i br-b65c10754bb1 -j RETURN
-A DOCKER -i docker0 -j RETURN
COMMIT
# Completed on Tue Nov 18 22:06:58 2025

原因就在这里了:iptables DROP 掉了网桥 docker0 与 br-ec761bc51778(my_net2) 之间双向的流量

bash 复制代码
[root@docker ~ 22:06:58]# brctl show
bridge name	bridge id		STP enabled	interfaces
br-4c4ad6ce54ce		8000.0242dfdd8da6	no		veth3a57224
							veth5382c73
br-b65c10754bb1		8000.0242a45e97b9	no		
docker0		8000.02428cdd10c4	no		vethb8e7eb4

从规则的命名 DOCKER-ISOLATION 可知 docker 在设计上就是要隔离不同的 netwrok。

那么接下来的问题是:怎样才能让 busybox1与busybox2 通信呢?

答案是:为 busybox1 容器添加一块 my_net2 的网卡。这个可以通过docker network connect 命令实现。

bash 复制代码
[root@docker ~ 22:07:34]# docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED          STATUS          PORTS     NAMES
975482e46477   busybox   "sh"      46 minutes ago   Up 46 minutes             busybox3
efc97893a8e4   busybox   "sh"      48 minutes ago   Up 48 minutes             busybox2
158f2b178dde   busybox   "sh"      2 hours ago      Up 2 hours                busybox1
[root@docker ~ 22:07:58]# docker network connect my_net2 busybox1

我们在 httpd 容器中查看一下网络配置:

bash 复制代码
[root@docker ~ 22:08:34]# docker exec -it busybox1 sh
/ # ip a
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
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
14: eth0@if15: <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
22: eth1@if23: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue 
    link/ether 02:42:ac:16:10:03 brd ff:ff:ff:ff:ff:ff
    inet 172.22.16.3/24 brd 172.22.16.255 scope global eth1
       valid_lft forever preferred_lft forever
/ # 

容器中增加了一个网卡 eth1,分配了 my_net2 的 IP 172.22.16.3。现在 busybox2 应该能够访问busybox1 了,验证一下:

bash 复制代码
[root@docker ~ 22:08:34]# docker exec -it busybox1 sh
/ # ip a
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
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
14: eth0@if15: <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
22: eth1@if23: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue 
    link/ether 02:42:ac:16:10:03 brd ff:ff:ff:ff:ff:ff
    inet 172.22.16.3/24 brd 172.22.16.255 scope global eth1
       valid_lft forever preferred_lft forever

busybox 能够 ping 到 busybox2。当前网络结构如图所示:

容器通信的三种方式

容器之间可通过 IP,Docker DNS Server 或 joined 容器三种方式通信。

IP 通信

从上一节的例子可以得出这样一个结论:两个容器要能通信,必须要有属于同一个网络的网卡。

满足这个条件后,容器就可以通过 IP 交互了。具体做法是在容器创建时通过 --network 指定相应的网络,或者通过 docker network connect 将现有容器加入到指定网络。可参考上一节 busybox 的例子,这里不再赘述。

Docker DNS Server

通过 IP 访问容器虽然满足了通信的需求,但还是不够灵活。因为我们在部署应用之前可能无法确定 IP,部署之后再指定要访问的 IP 会比较麻烦。对于这个问题,可以通过 docker 自带的 DNS 服务解决。

从 Docker 1.10 版本开始,docker daemon 实现了一个内嵌的 DNS server,使容器可以直接通过"容器名"通信。方法很简单,只要在启动时用 --name 为容器命名就可以了。

下面启动两个容器 bbox1 和 bbox2:

bash 复制代码
[root@docker ~ 22:29:08]# docker run -it --network my_net2 --name bbox1 busybox
/ #
# ctrl_p,ctrl_q退出容器

[root@docker ~ 22:31:05]# docker run -it --network my_net2 --name bbox2 busybox 
/ #
# ctrl_p,ctrl_q退出容器

然后,bbox2 就可以直接 ping 到 bbox1 了:

bash 复制代码
[root@docker ~ 22:31:22]# docker exec -it bbox2 sh
/ # ping -c 2 bbox1
PING bbox1 (172.22.16.4): 56 data bytes
64 bytes from 172.22.16.4: seq=0 ttl=64 time=0.113 ms
64 bytes from 172.22.16.4: seq=1 ttl=64 time=0.176 ms

--- bbox1 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.113/0.144/0.176 ms

使用 docker DNS 有个限制:只能在 user-defined 网络中使用。也就是说,默认的 bridge 网络是无法使用 DNS 的。下面验证一下:

创建 bbox3 和 bbox4,均连接到 bridge 网络。

bash 复制代码
[root@docker ~ 22:33:45]# docker run -it --name bbox3 busybox
/ # 
# ctrl_p,ctrl_q退出容器

[root@docker ~ 22:34:02]# docker run -it --name bbox4 busybox
/ # 
# ctrl_p,ctrl_q退出容器

bbox4 无法 ping 到 bbox3。

bash 复制代码
[root@docker ~ 22:34:53]# docker exec -it bbox4 sh
/ # ping -c 3 bbox3
ping: bad address 'bbox3'

joined 容器

joined 容器是另一种实现容器间通信的方式。

joined 容器非常特别,它可以使两个或多个容器共享一个网络栈,共享网卡和配置信息,joined 容器之间可以通过 127.0.0.1 直接通信。请看下面的例子:

先创建一个 httpd 容器,名字为 web1。

bash 复制代码
[root@docker ~ 22:35:48]# docker run -d -it --name web1 httpd
b578e8369eb7c1ba8d65c5ba2dac4331716f80f43ecd11810e94aa90ed7c702e

下面我们查看一下 web1 的网络:

bash 复制代码
[root@docker ~ 22:35:48]# docker run -d -it --name web1 httpd
b578e8369eb7c1ba8d65c5ba2dac4331716f80f43ecd11810e94aa90ed7c702e
[root@docker ~ 22:36:11]# docker exec -it web1 bash

root@b578e8369eb7:/usr/local/apache2# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default 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
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
32: eth0@if33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:ac:11:00:05 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.17.0.5/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

# 也可以直接通过hostname -I观察IP地址

然后创建 busybox 容器并通过 --network=container:web1 指定 jointed 容器为 web1:

请注意 busybox 容器中的网络配置信息

bash 复制代码
[root@docker ~ 23:03:32]# docker run -it --network container:web1 busybox
/ # ip a
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
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
32: eth0@if33: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue 
    link/ether 02:42:ac:11:00:05 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.5/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

busybox 和 web1 的网卡 mac 地址与 IP 完全一样,它们共享了相同的网络栈。busybox 可以直接用 127.0.0.1 访问 web1 的 http 服务。

bash 复制代码
/ # wget 127.0.0.1
Connecting to 127.0.0.1 (127.0.0.1:80)
saving to 'index.html'
index.html           100% |****************************************|    45  0:00:00 ETA
'index.html' saved
/ # cat index.html
<html><body><h1>It works!</h1></body></html>

joined 容器非常适合以下场景:

  1. 不同容器中的程序希望通过 loopback 高效快速地通信,比如 web server 与 app server。
  2. 希望监控其他容器的网络流量,比如运行在独立容器中的网络监控程序。

容器如何访问外部世界

前面我们已经解决了容器间通信的问题,接下来讨论容器如何与外部世界通信。这里涉及两个方向:

  1. 容器访问外部世界
  2. 外部世界访问容器

容器访问外部世界

在我们当前的实验环境下,docker host 是可以访问外网的。

bash 复制代码
[root@docker ~ 17:44:16]# ping -c 3 www.baidu.com
PING www.a.shifen.com (180.101.51.73) 56(84) bytes of data.
64 bytes from 180.101.51.73 (180.101.51.73): icmp_seq=1 ttl=128 time=38.9 ms
64 bytes from 180.101.51.73 (180.101.51.73): icmp_seq=2 ttl=128 time=20.8 ms
64 bytes from 180.101.51.73 (180.101.51.73): icmp_seq=3 ttl=128 time=54.8 ms

--- www.a.shifen.com ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 3077ms
rtt min/avg/max/mdev = 20.798/38.145/54.765/13.876 ms

我们看一下容器是否也能访问外网呢?

bash 复制代码
[root@docker ~ 17:44:51]# docker run -it --name test1 busybox
Unable to find image 'busybox:latest' locally
latest: Pulling from library/busybox
e59838ecfec5: Pull complete 
Digest: sha256:e3652a00a2fabd16ce889f0aa32c38eec347b997e73bd09e69c962ec7f8732ee
Status: Downloaded newer image for busybox:latest
/ # ping -c 3 www.baidu.com
PING www.baidu.com (180.101.49.44): 56 data bytes
64 bytes from 180.101.49.44: seq=0 ttl=127 time=63.218 ms
64 bytes from 180.101.49.44: seq=1 ttl=127 time=42.754 ms
64 bytes from 180.101.49.44: seq=2 ttl=127 time=51.334 ms

--- www.baidu.com ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 42.754/52.435/63.218 ms

可见,容器默认就能访问外网

请注意:这里外网指的是容器网络以外的网络环境,并非特指 internet。

现象很简单,但更重要的:我们应该理解现象下的本质。

在上面的例子中,busybox 位于 docker0 这个私有 bridge 网络中(172.17.0.0/16),当 busybox 从容器向外 ping 时,数据包是怎样到达 www.baidu.com 的呢?

这里的关键就是 NAT。我们查看一下 docker host 上的 iptables 规则:

bash 复制代码
[root@docker ~ 17:46:36]# iptables -t nat -S
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P POSTROUTING ACCEPT
-P OUTPUT ACCEPT
-N DOCKER
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A POSTROUTING -s 172.18.0.0/16 ! -o br-b65c10754bb1 -j MASQUERADE
-A POSTROUTING -s 172.22.16.0/24 ! -o br-4c4ad6ce54ce -j MASQUERADE
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A DOCKER -i docker0 -j RETURN
-A DOCKER -i br-b65c10754bb1 -j RETURN
-A DOCKER -i br-4c4ad6ce54ce -j RETURN

在 NAT 表中,有这么一条规则:

-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE

其含义是:如果网桥 docker0 收到来自 172.17.0.0/16 网段的外出包,把它交给 MASQUERADE 处理。而 MASQUERADE 的处理方式是将包的源地址替换成 host 的地址发送出去,即做了一次网络地址转换(NAT)

下面我们通过 tcpdump 查看地址是如何转换的。先查看 docker host 的路由表:

bash 复制代码
[root@docker ~ 17:46:44]# ip r
default via 192.168.108.2 dev ens160 proto static metric 100 
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown 
172.18.0.0/16 dev br-b65c10754bb1 proto kernel scope link src 172.18.0.1 linkdown 
172.22.16.0/24 dev br-4c4ad6ce54ce proto kernel scope link src 172.22.16.1 linkdown 
192.168.108.0/24 dev ens160 proto kernel scope link src 192.168.108.30 metric 100 

默认路由通过 ens160 发出去,所以我们要同时监控 ens160 和 docker0 上的 icmp(ping)数据包。

使用tcpdump观察现象

bash 复制代码
[root@docker ~ 17:47:09]# yum install -y tcpdump

[root@docker ~ 17:47:47]# tcpdump -i docker0 -n icmp
dropped privs to tcpdump
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on docker0, link-type EN10MB (Ethernet), capture size 262144 bytes

再开一个窗口

bash 复制代码
[root@docker ~ 17:48:20]# tcpdump -i ens160 -n icmp
dropped privs to tcpdump
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens160, link-type EN10MB (Ethernet), capture size 262144 bytes

再开一个窗口, busybox ping www.baidu.com

bash 复制代码
[root@docker ~ 17:48:51]# docker run -it busybox
/ # ping -c 3 www.baidu.com
PING www.baidu.com (180.101.49.44): 56 data bytes
64 bytes from 180.101.49.44: seq=0 ttl=127 time=117.573 ms
64 bytes from 180.101.49.44: seq=1 ttl=127 time=115.790 ms
64 bytes from 180.101.49.44: seq=2 ttl=127 time=118.920 ms

--- www.baidu.com ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 115.790/117.427/118.920 ms

tcpdump 输出如下:

bash 复制代码
[root@docker ~ 17:47:47]# tcpdump -i docker0 -n icmp
dropped privs to tcpdump
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on docker0, link-type EN10MB (Ethernet), capture size 262144 bytes
17:49:39.707611 IP 172.17.0.2 > 180.101.49.44: ICMP echo request, id 7, seq 0, length 64
17:49:39.824988 IP 180.101.49.44 > 172.17.0.2: ICMP echo reply, id 7, seq 0, length 64
17:49:40.708374 IP 172.17.0.2 > 180.101.49.44: ICMP echo request, id 7, seq 1, length 64
17:49:40.823994 IP 180.101.49.44 > 172.17.0.2: ICMP echo reply, id 7, seq 1, length 64
17:49:41.709044 IP 172.17.0.2 > 180.101.49.44: ICMP echo request, id 7, seq 2, length 64
17:49:41.827763 IP 180.101.49.44 > 172.17.0.2: ICMP echo reply, id 7, seq 2, length 64

docker0 收到 busybox 的 ping 包,源地址为容器 IP 172.17.0.2,这没问题,交给 MASQUERADE 处理。这时,在 ens160 上我们看到了变化:

bash 复制代码
[root@docker ~ 17:48:20]# tcpdump -i ens160 -n icmp
dropped privs to tcpdump
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens160, link-type EN10MB (Ethernet), capture size 262144 bytes
17:49:39.707644 IP 192.168.108.30 > 180.101.49.44: ICMP echo request, id 7, seq 0, length 64
17:49:39.824926 IP 180.101.49.44 > 192.168.108.30: ICMP echo reply, id 7, seq 0, length 64
17:49:40.708462 IP 192.168.108.30 > 180.101.49.44: ICMP echo request, id 7, seq 1, length 64
17:49:40.823936 IP 180.101.49.44 > 192.168.108.30: ICMP echo reply, id 7, seq 1, length 64
17:49:41.709083 IP 192.168.108.30 > 180.101.49.44: ICMP echo request, id 7, seq 2, length 64
17:49:41.827652 IP 180.101.49.44 > 192.168.108.30: ICMP echo reply, id 7, seq 2, length 64

ping 包的源地址变成了 ens160 的 IP 192.168.108.130

这就是 iptable NAT 规则处理的结果,从而保证数据包能够到达外网。下面用一张图来说明这个过程:

  1. busybox 发送 ping 包:172.17.0.2 > www.baidu.com
  2. docker0 收到包,发现是发送到外网的,交给 NAT 处理。
  3. NAT 将源地址换成 ens160的 IP:192.168.108.30 > www.baidu.com
  4. ping 包从 ens160 发送出去,到达 www.baidu.com

通过 NAT,docker 实现了容器对外网的访问。

下一节我们讨论另一个方向的流量:外部世界如何访问容器。

外部世界如何访问容器

上节我们学习了容器如何访问外部网络,今天讨论另一个方向:外部网络如何访问到容器?

答案是:端口映射

docker 可将容器对外提供服务的端口映射到 host 的某个端口,外网通过该端口访问容器。容器启动时通过-p参数映射端口:

bash 复制代码
[root@docker ~ 18:01:22]# docker run -d --name web1 -p 80 httpd
b705106ae2a949f9c65fc69dacaae91b91bd3d14be6430872ada0495127673c5
[root@docker ~ 18:02:17]# docker ps
CONTAINER ID   IMAGE     COMMAND              CREATED          STATUS          PORTS                                     NAMES
b705106ae2a9   httpd     "httpd-foreground"   8 seconds ago    Up 6 seconds    0.0.0.0:32768->80/tcp, :::32768->80/tcp   web1
9e039a98683d   busybox   "sh"                 13 minutes ago   Up 13 minutes                                             unruffled_black

[root@docker ~ 19:35:40]# docker port b7

容器启动后,可通过 docker ps 或者 docker port 查看到 host 映射的端口。在上面的例子中,httpd 容器的 80 端口被映射到 host 32768 上,这样就可以通过 <host ip>:<32768> 访问容器的 web 服务了。

bash 复制代码
[root@docker ~ 18:02:24]# curl 192.168.108.30:32768
<html><body><h1>It works!</h1></body></html>

除了映射动态端口,也可在 -p 中指定映射到 host 某个特定端口,例如可将 80 端口映射到 host 的 8080 端口:

bash 复制代码
[root@docker ~ 18:03:07]# docker run -d -p 8080:80 --name web2 httpd
666ac2b781ee02614c3ae38dd2f2fd9642d967c21c0707a25e9bbb8ea987fba1
[root@docker ~ 18:04:26]# curl 192.168.108.30:8080
<html><body><h1>It works!</h1></body></html>

每一个映射的端口,host 都会启动一个 docker-proxy 进程来处理访问容器的流量:

bash 复制代码
[root@docker ~ 18:04:44]# ps -elf |grep docker-proxy
4 S root        2258    1180  0  80   0 - 401037 -     18:02 ?        00:00:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 32768 -container-ip 172.17.0.3 -container-port 80
4 S root        2263    1180  0  80   0 - 401037 -     18:02 ?        00:00:00 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 32768 -container-ip 172.17.0.3 -container-port 80
4 S root        2471    1180  0  80   0 - 401101 -     18:04 ?        00:00:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 8080 -container-ip 172.17.0.4 -container-port 80
4 S root        2476    1180  0  80   0 - 419470 -     18:04 ?        00:00:00 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 8080 -container-ip 172.17.0.4 -container-port 80
0 S root        2622    1844  0  80   0 -  3054 -      18:05 pts/0    00:00:00 grep --color=auto docker-prox

以 0.0.0.0:8080->80/tcp 为例分析整个过程:

  1. docker-proxy 监听 host 的 8080 端口。
  2. 当 curl 访问 192.168.108.30:8080时,docker-proxy 转发给容器 172.17.0.3:80。
  3. httpd 容器响应请求并返回结果。

实战:安装tomcat

docker hub上面查找tomcat镜像

bash 复制代码
[root@docker ~]# docker search tomcat

从docker hub上拉取tomcat镜像到本地

bash 复制代码
[root@docker ~ 18:05:14]# docker pull tomcat

# docker images查看是否有拉取到的tomcat
[root@docker ~ 18:21:40]# docker images
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
httpd        latest    c00bfb4edfeb   2 days ago      117MB
tomcat       latest    391829227220   6 days ago      412MB
ubuntu       latest    c3a134f2ace4   4 weeks ago     78.1MB
busybox      latest    08ef35a1c3f0   13 months ago   4.43MB

# 使用tomcat镜像创建容器实例(也叫运行镜像)
[root@docker ~ 18:28:43]# docker run -itd -p 8080:8080 tomcat
2b88ee781d2a307751c77d57b40624a172090b60ed4c9ab7a998684b6b903f0b

访问tomcat首页

把webapps.dist目录换成webapps

bash 复制代码
#查看刚才创建的tomcat容器ID
[root@docker ~ 18:29:12]# docker ps 
CONTAINER ID   IMAGE     COMMAND             CREATED          STATUS          PORTS                                       NAMES
2b88ee781d2a   tomcat    "catalina.sh run"   52 seconds ago   Up 51 seconds   0.0.0.0:8080->8080/tcp, :::8080->8080/tcp   sharp_ishizaka
9e039a98683d   busybox   "sh"                40 minutes ago   Up 40 minutes                                               unruffled_black

#进入tomcat容器
[root@docker ~ 18:30:03]# docker exec -it 2b bash

#查看webapps文件夹为空
root@2b88ee781d2a:/usr/local/tomcat# ls webapps
root@2b88ee781d2a:/usr/local/tomcat#

#文件在webapps.dist
root@2b88ee781d2a:/usr/local/tomcat# ls webapps.dist
docs  examples  host-manager  manager  ROOT

#用webapps.dist替换webapps
root@2b88ee781d2a:/usr/local/tomcat# mv webapps.dist/* webapps
root@2b88ee781d2a:/usr/local/tomcat# ls webapps
docs  examples  host-manager  manager  ROOT

再次访问tomcat

思考:我想要基于tomcat镜像做出一个能够直接访问的tomcat镜像该如何做?

bash 复制代码
[root@docker ~ 18:34:25]# docker ps
CONTAINER ID   IMAGE     COMMAND             CREATED          STATUS          PORTS                                       NAMES
2b88ee781d2a   tomcat    "catalina.sh run"   5 minutes ago    Up 5 minutes    0.0.0.0:8080->8080/tcp, :::8080->8080/tcp   sharp_ishizaka
9e039a98683d   busybox   "sh"                45 minutes ago   Up 45 minutes                                               unruffled_black

[root@docker ~ 18:35:27]# docker commit 2b88ee781d2a tomcat_web
sha256:ac48a9b308980cf747b0aa161a6992eb571fb86a1d87649654861c9d4e6f0c49

本章小结

在这一章我们首先学习了 Docker 的三种网络:none, host 和 bridge 并讨论了它们的不同使用场景;然后我们实践了创建自定义网络;最后详细讨论了如何实现容器与容器之间,容器与外部网络之间的通信。

本章重点关注的是单个主机内的容器网络,下一节开始学习 Docker 存储。

第6章 存储

Docker的两类存储资源

我们从本章开始讨论 Docker 存储。

Docker 为容器提供了两种存放数据的资源:

  1. 由 storage driver 管理的镜像层和容器层。
  2. Data Volume。

我们会详细讨论它们的原理和特性。

storage driver

在前面镜像章节我们学习到 Docker 镜像的分层结构,简单回顾一下。

容器由最上面一个可写的容器层,以及若干只读的镜像层组成,容器的数据就存放在这些层中。这样的分层结构最大的特性是 Copy-on-Write:

  1. 新数据会直接存放在最上面的容器层。
  2. 修改现有数据会先从镜像层将数据复制到容器层,修改后的数据直接保存在容器层中,镜像层保持不变。
  3. 如果多个层中有命名相同的文件,用户只能看到最上面那层中的文件。

分层结构使镜像和容器的创建、共享以及分发变得非常高效,而这些都要归功于 Docker storage driver。正是 storage driver 实现了多层数据的堆叠并为用户提供一个单一的合并之后的统一视图。

Docker 支持多种 storage driver,有 AUFS、Device Mapper、Btrfs、OverlayFS、VFS 和 ZFS。它们都能实现分层的架构,同时又有各自的特性。对于 Docker 用户来说,具体选择使用哪个 storage driver 是一个难题,因为:

  1. 没有哪个 driver 能够适应所有的场景。
  2. driver 本身在快速发展和迭代。

不过 Docker 官方给出了一个简单的答案:
优先使用 Linux 发行版默认的 storage driver

Docker 安装时会根据当前系统的配置选择默认的 driver。默认 driver 具有最好的稳定性,因为默认 driver 在发行版上经过了严格的测试。

运行docker info查看CentOS的默认 driver:

bash 复制代码
[root@docker ~ 18:36:14]# docker info 
Client: Docker Engine - Community
 Version:    26.1.3
 Context:    default
 Debug Mode: false
 Plugins:
  buildx: Docker Buildx (Docker Inc.)
    Version:  v0.14.0
    Path:     /usr/libexec/docker/cli-plugins/docker-buildx
  compose: Docker Compose (Docker Inc.)
    Version:  v2.27.0
    Path:     /usr/libexec/docker/cli-plugins/docker-compose

Server:
 Containers: 5
  Running: 3
  Paused: 0
  Stopped: 2
 Images: 5
 Server Version: 26.1.3
 Storage Driver: overlay2
  Backing Filesystem: xfs
  Supports d_type: true
  Using metacopy: false
  Native Overlay Diff: true
  userxattr: false
 Logging Driver: json-file
 Cgroup Driver: cgroupfs
 Cgroup Version: 1
 Plugins:
  Volume: local
  Network: bridge host ipvlan macvlan null overlay
  Log: awslogs fluentd gcplogs gelf journald json-file local splunk syslog
 Swarm: inactive
 Runtimes: io.containerd.runc.v2 runc
 Default Runtime: runc
 Init Binary: docker-init
 containerd version: 8b3b7ca2e5ce38e8f31a34f35b2b68ceb8470d89
 runc version: v1.1.12-0-g51d5e94
 init version: de40ad0
 Security Options:
  seccomp
   Profile: builtin
 Kernel Version: 4.18.0-553.6.1.el8.x86_64
 Operating System: CentOS Stream 8
 OSType: linux
 Architecture: x86_64
 CPUs: 2
 Total Memory: 7.486GiB
 Name: docker
 ID: a0c2e5d3-41f1-4e35-9a13-edeb3bf4d811
 Docker Root Dir: /var/lib/docker
 Debug Mode: false
 Experimental: false
 Insecure Registries:
  192.168.108.30
  127.0.0.0/8
 Registry Mirrors:
  https://563b4056eb3942d78e70f3f171276f0a.mirror.swr.myhuaweicloud.com/
 Live Restore Enabled: false

CentOS Stream 8 用的overlay2,底层文件系统是xfs,各层数据存放在 /var/lib/docker。

对于某些容器,直接将数据放在由 storage driver 维护的层中是很好的选择,比如那些无状态的应用。无状态意味着容器没有需要持久化的数据,随时可以从镜像直接创建。

比如 busybox,它是一个工具箱,我们启动 busybox 是为了执行诸如 wget,ping 之类的命令,不需要保存数据供以后使用,使用完直接退出,容器删除时存放在容器层中的工作数据也一起被删除,这没问题,下次再启动新容器即可。

但对于另一类应用这种方式就不合适了,它们有持久化数据的需求,容器启动时需要加载已有的数据,容器销毁时希望保留产生的新数据,也就是说,这类容器是有状态的。

这就要用到 Docker 的另一种存储机制:Data Volume,下一节我们讨论。

Data Volume之bind mount

storage driver 和 data volume 是容器存放数据的两种方式,上一节我们学习了 storage driver,本节开始讨论 Data Volume。

Data Volume 本质上是 Docker Host 文件系统中的目录或文件,能够直接被 mount 到容器的文件系统中。Data Volume 有以下特点:

  1. Data Volume 是目录或文件,而非没有格式化的磁盘(块设备)。
  2. 容器可以读写 volume 中的数据。
  3. volume 数据可以被永久的保存,即使使用它的容器已经销毁。

好,现在我们有数据层(镜像层和容器层)和 volume 都可以用来存放数据,具体使用的时候要怎样选择呢?考虑下面几个场景:

  1. Database 软件 vs Database 数据
  2. Web 应用 vs 应用产生的日志
  3. 数据分析软件 vs input/output 数据
  4. Apache Server vs 静态 HTML 文件

相信大家会做出这样的选择:

  1. 前者放在数据层中。因为这部分内容是无状态的,应该作为镜像的一部分。
  2. 后者放在 Data Volume 中。这是需要持久化的数据,并且应该与镜像分开存放。

还有个大家可能会关心的问题:如何设置 voluem 的容量?

因为 volume 实际上是 docker host 文件系统的一部分,所以 volume 的容量取决于文件系统当前未使用的空间,目前还没有方法设置 volume 的容量。

在具体的使用上,docker 提供了两种类型的 volume:bind mount 和 docker managed volume。

bind mount

bind mount 是将 host 上已存在的目录或文件 mount 到容器。

例如 docker host 上有目录 $HOME/htdocs:

bash 复制代码
[root@docker ~ 19:11:11]# pwd
/root
[root@docker ~ 19:11:27]# mkdir htdocs
[root@docker ~ 19:11:42]# cd htdocs/
[root@docker htdocs 19:11:50]# touch index.html
[root@docker htdocs 19:12:02]# vim index.html 
<html><body><h1>This is a file in host file system !</h1></body></html>
[root@docker htdocs 19:12:28]# cd 
[root@docker ~ 19:12:31]# docker run -d -p 80:80 -v ~/htdocs:/usr/local/apache2/htdocs httpd
f63dff45ca64a38b2e1b38c4c1956dc7f10687c212db32f32de48bcca20250f7
[root@docker ~ 19:13:57]# curl 127.0.0.1:80
<html><body><h1>This is a file in host file system !</h1></body></html>

通过 -v 将其 mount 到 httpd 容器:

bash 复制代码
[root@docker ~ 19:12:31]# docker run -d -p 80:80 -v ~/htdocs:/usr/local/apache2/htdocs httpd
f63dff45ca64a38b2e1b38c4c1956dc7f10687c212db32f32de48bcca20250f7

-v 的格式为 <host path>:<container path>。/usr/local/apache2/htdocs 就是 apache server 存放静态文件的地方。由于 /usr/local/apache2/htdocs 已经存在,原有数据会被隐藏起来,取而代之的是 host $HOME/htdocs/ 中的数据,这与 linux mount 命令的行为是一致的。

bash 复制代码
[root@docker ~ 19:13:57]# curl 127.0.0.1:80
<html><body><h1>This is a file in host file system !</h1></body></html>

curl 显示当前主页确实是 $HOME/htdocs/index.html 中的内容。更新一下,看是否能生效:

bash 复制代码
[root@docker ~ 19:14:05]# echo "uptated index page!" > ~/htdocs/index.html 
[root@docker ~ 19:14:50]# curl 127.0.0.1:80
uptated index page!

host 中的修改确实生效了,bind mount 可以让 host 与容器共享数据。这在管理上是非常方便的。

下面我们将容器销毁,看看对 bind mount 有什么影响:

bash 复制代码
[root@docker ~ 19:14:53]# docker stop f6
f6
[root@docker ~ 19:15:22]# docker rm f6
f6
[root@docker ~ 19:15:39]# cat ~/htdocs/index.html 
uptated index page!

可见,即使容器没有了,bind mount 也还在。这也合理,bind mount 是 host 文件系统中的数据,只是借给容器用用,哪能随便就删了啊。

另外,bind mount 时还可以指定数据的读写权限,默认是可读可写,可指定为只读:

bash 复制代码
[root@docker ~ 19:15:52]# docker run -d -p 80:80 -v ~/htdocs:/usr/local/apche2/htdoc:ro httpd
3e9bbf41d49e58bc87b2e5ff9774514b726580ab16230b1874ce65d1c1752e5e
[root@docker ~ 19:17:24]# docker exec -it 3e bash
root@3e9bbf41d49e:/usr/local/apache2# echo "do some changes" > htdocs/index.html

ro 设置了只读权限,在容器中是无法对 bind mount 数据进行修改的。只有 host 有权修改数据,提高了安全性。

除了 bind mount 目录,还可以单独指定一个文件:

bash 复制代码
# 删除上一个容器不然80端口冲突
[root@docker ~ 19:21:39]# docker run -d -p 80:80 -v ~/htdocs/index.html:/usr/local/apche2/htdocs/new_index.html httpd
87e3d79397f5b133ce9f01e93373bbdab1893567635e97fb83c63fb46ce1f530

使用 bind mount 单个文件的场景是:只需要向容器添加文件,不希望覆盖整个目录。在上面的例子中,我们将 html 文件加到 apache 中,同时也保留了容器原有的数据。

使用单一文件有一点要注意:host 中的源文件必须要存在,不然会当作一个新目录 bind mount 给容器。

mount point 有很多应用场景,比如我们可以将源代码目录 mount 到容器中,在 host 中修改代码就能看到应用的实时效果。再比如将 mysql 容器的数据放在 bind mount 里,这样 host 可以方便地备份和迁移数据。

bind mount 的使用直观高效,易于理解,但它也有不足的地方:bind mount 需要指定 host 文件系统的特定路径,这就限制了容器的可移植性,当需要将容器迁移到其他 host,而该 host 没有要 mount 的数据或者数据不在相同的路径时,操作会失败。

移植性更好的方式是 docker managed volume,下一节我们讨论。

Data Volume之docker managed volume

docker managed volume 与 bind mount 在使用上的最大区别是不需要指定 mount 源,指明 mount point 就行了。还是以 httpd 容器为例:

bash 复制代码
[root@docker ~ 17:34:59]# docker run -d -p 80:80 -v /usr/local/apache2/htdocs httpd
f8ed30c48b9bca7a13243936f53eccfd9e7641190d8b1ef36841d5f5b67b44e3

我们通过 -v 告诉 docker 需要一个 data volume,并将其 mount 到 /usr/local/apache2/htdocs。那么这个 data volume 具体在哪儿呢?

这个答案可以在容器的配置信息中找到,执行 docker inspect 命令:

bash 复制代码
#docker inspect后面跟的是容器ID
[root@docker ~ 17:35:38]# docker inspect f 
......
 "Mounts": [
            {
                "Type": "volume",
                "Name": "c0b558a72fb92ec5f5ffff24a38ba2e7dc8c907d654bc0031923651370335157",
                "Source": "/var/lib/docker/volumes/c0b558a72fb92ec5f5ffff24a38ba2e7dc8c907d654bc0031923651370335157/_data",
                "Destination": "/usr/local/apache2/htdocs",
                "Driver": "local",
                "Mode": "",
                "RW": true,
                "Propagation": ""
            }
        ],
......

docker inspect 的输出很多,我们感兴趣的是 Mounts 这部分,这里会显示容器当前使用的所有 data volume,包括 bind mount 和 docker managed volume。

Source 就是该 volume 在 host 上的目录。

原来,每当容器申请 mount docker manged volume 时,docker 都会在/var/lib/docker/volumes 下生成一个目录(例子中是 "/var/lib/docker/volumes/c0b558a72fb92ec5f5ffff24a38ba2e7dc8c907d654bc0031923651370335157/_data ),这个目录就是 mount 源。

下面继续研究这个 volume,看看里面有些什么东西:

bash 复制代码
[root@docker ~ 17:37:28]# ls -l /var/lib/docker/volumes/c0b558a72fb92ec5f5ffff24a38ba2e7dc8c907d654bc0031923651370335157/_data
total 4
-rw-r--r-- 1 501 ftp 45 Jun 12  2007 index.html
[root@docker ~ 17:37:51]# curl 127.0.0.1:80
<html><body><h1>It works!</h1></body></html>

volume 的内容跟容器原有 /usr/local/apache2/htdocs 完全一样,这是怎么回事呢?

这是因为:如果 mount point 指向的是已有目录,原有数据会被复制到 volume 中。

但要明确一点:此时的 /usr/local/apache2/htdocs 已经不再是由 storage driver 管理的层数据了,它已经是一个 data volume。我们可以像 bind mount 一样对数据进行操作,例如更新数据:

bash 复制代码
[root@docker ~ 17:38:07]# echo "update volume from host" > /var/lib/docker/volumes/c0b558a72fb92ec5f5ffff24a38ba2e7dc8c907d654bc0031923651370335157/_data/index.html 
[root@docker ~ 17:38:43]# curl 127.0.0.1:80
update volume from host

简单回顾一下 docker managed volume 的创建过程:

  1. 容器启动时,简单的告诉 docker "我需要一个 volume 存放数据,帮我 mount 到目录 /abc"。
  2. docker 在 /var/lib/docker/volumes 中生成一个随机目录作为 mount 源。
  3. 如果 /abc 已经存在,则将数据复制到 mount 源,
  4. 将 volume mount 到 /abc

除了通过 docker inspect 查看 volume,我们也可以用 docker volume 命令:

bash 复制代码
[root@docker ~ 17:38:46]# docker volume ls
DRIVER    VOLUME NAME
local         c0b558a72fb92ec5f5ffff24a38ba2e7dc8c907d654bc0031923651370335157
[root@docker ~ 17:42:42]# docker volume inspect c0b558a72fb92ec5f5ffff24a38ba2e7dc8c907d654bc0031923651370335157
[
    {
        "CreatedAt": "2024-09-17T17:38:46+08:00",
        "Driver": "local",
        "Labels": {
            "com.docker.volume.anonymous": ""
        },
        "Mountpoint": "/var/lib/docker/volumes/c0b558a72fb92ec5f5ffff24a38ba2e7dc8c907d654bc0031923651370335157/_da
        "Name": "c0b558a72fb92ec5f5ffff24a38ba2e7dc8c907d654bc0031923651370335157",
        "Options": null,
        "Scope": "local"
    }
]

目前,docker volume 只能查看 docker managed volume,还看不到 bind mount;同时也无法知道 volume 对应的容器,这些信息还得靠docker inspect

我们已经学习了两种 data volume 的原理和基本使用方法,下面做个对比:

  1. 相同点:两者都是 host 文件系统中的某个路径。
  2. 不同点:
bind mount docker managed volume
volume 位置 可任意指定 /var/lib/docker/volumes/...
对已有mount point 影响 隐藏并替换为 volume 原有数据复制到 volume
是否支持单个文件 支持 不支持,只能是目录
权限控制 可设置为只读,默认为读写权限 无控制,均为读写权限
移植性 移植性弱,与 host path 绑定 移植性强,无需指定 host 目录

下节讨论如何通过 data volume 实现容器与 host,容器与容器共享数据。

如何共享数据

数据共享是 volume 的关键特性,本节我们详细讨论通过 volume 如何在容器与 host 之间,容器与容器之间共享数据。

容器与 host 共享数据

我们有两种类型的 data volume,它们均可实现在容器与 host 之间共享数据,但方式有所区别。

对于 bind mount 是非常明确的:直接将要共享的目录 mount 到容器。具体请参考前面 httpd 的例子,不再赘述。

docker managed volume 就要麻烦点。由于 volume 位于 host 中的目录,是在容器启动时才生成,所以需要将共享数据拷贝到 volume 中。请看下面的例子:

bash 复制代码
[root@docker ~ 17:54:36]# docker ps -a
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
[root@docker ~ 17:54:40]# docke run -d -p 80:80 -v /usr/local/apache/htdocs httpd
-bash: docke: command not found
[root@docker ~ 17:55:26]# docker run -d -p 80:80 -v /usr/local/apache/htdocs httpd
d00dd0b4b91e64c48337b6645a5ba030a656aa4ab0c76894ae5c65d5dbf05ff0
[root@docker ~ 17:55:35]# curl 127.0.0.1:80
<html><body><h1>It works!</h1></body></html>

#将host os的/root/htdocs/index.html拷贝到容器中的/usr/local/apache2/htdocs目录下
[root@docker ~ 17:55:44]# docker  cp ~/htdocs/index.html d00d:/usr/local/apache2/htdocs
Successfully copied 2.05kB to d00d:/usr/local/apache2/htdocs
[root@docker ~ 17:56:49]# curl 127.0.0.1:80
i can do this

docker cp 可以在容器和 host 之间拷贝数据,当然我们也可以直接通过 Linux 的 cp 命令复制到 /var/lib/docker/volumes/xxx。

思考:容器中的文件拷贝到host os如何操作

容器之间共享数据

第一种方法是将共享数据放在 bind mount 中,然后将其 mount 到多个容器。还是以 httpd 为例,不过这次的场景复杂些,我们要创建由三个 httpd 容器组成的 web server 集群,它们使用相同的 html 文件,操作如下:

  1. 将 $HOME/htdocs mount 到三个 httpd 容器。
bash 复制代码
[root@docker ~ 17:57:19]# docker run --name web1 -d -p 80 -v ~/htdocs/:/usr/local/apache2/htdocs httpd
834e728cbde5cd5e59e65246cd2e6d8db210565d94edf29edf51b31ea62b361c
[root@docker ~ 17:58:15]# docker run --name web2 -d -p 80 -v ~/htdocs/:/usr/local/apache2/htdocs httpd
82d99661deb68dd3f6f4ffa5d0233d72ce882389ce9680aa22dd618fae90a875
[root@docker ~ 17:58:23]# docker run --name web3 -d -p 80 -v ~/htdocs/:/usr/local/apache2/htdocs httpd
6f40938b330b5d0007cb6c3bfe046466d9da3f5ec2ae79f6ef413ce7377d679c

2.查看当前主页内容。

bash 复制代码
[root@docker ~ 17:58:31]# docker ps
CONTAINER ID   IMAGE     COMMAND              CREATED          STATUS          PORTS                                     NAMES
6f40938b330b   httpd     "httpd-foreground"   9 seconds ago    Up 7 seconds    0.0.0.0:32770->80/tcp, :::32770->80/tcp   web3
82d99661deb6   httpd     "httpd-foreground"   17 seconds ago   Up 15 seconds   0.0.0.0:32769->80/tcp, :::32769->80/tcp   web2
834e728cbde5   httpd     "httpd-foreground"   25 seconds ago   Up 24 seconds   0.0.0.0:32768->80/tcp, :::32768->80/tcp   web1
d00dd0b4b91e   httpd     "httpd-foreground"   3 minutes ago    Up 3 minutes    0.0.0.0:80->80/tcp, :::80->80/tcp         hungry_bhabha
[root@docker ~ 17:58:39]# curl 127.0.0.1:32770
i can do this
[root@docker ~ 17:59:03]# curl 127.0.0.1:32769
i can do this
[root@docker ~ 17:59:10]# curl 127.0.0.1:32768
i can do this

3.修改 volume 中的主页文件,再次查看并确认所有容器都使用了新的主页。

bash 复制代码
[root@docker ~ 17:59:15]# echo "This is a new index page for web cluster" > ~/htdocs/index.html 
[root@docker ~ 18:04:05]# curl 127.0.0.1:32770
This is a new index page for web cluster
[root@docker ~ 18:04:08]# curl 127.0.0.1:32768
This is a new index page for web cluster
[root@docker ~ 18:04:11]# curl 127.0.0.1:32769
This is a new index page for web cluster

另一种在容器之间共享数据的方式是使用 volume container,下节讨论。


用volume container共享数据

volume container 是专门为其他容器提供 volume 的容器。它提供的卷可以是 bind mount,也可以是 docker managed volume。下面我们创建一个 volume container:

bash 复制代码
[root@docker ~ 18:26:43]#docker create --name vc_data -v ~/htdocs/:/usr/local/apache2/htdocs -v /other/userful/tools busybox
851587e047f8b5dbb364e474cfce19539c650cbd4738a7786b22375b10d6a2b7

我们将容器命名为 vc_data(vc 是 volume container 的缩写)。注意这里执行的是 docker create 命令,这是因为 volume container 的作用只是提供数据,它本身不需要处于运行状态。容器 mount 了两个 volume:

  1. bind mount,存放 web server 的静态文件。
  2. docker managed volume,存放一些实用工具(当然现在是空的,这里只是做个示例)。

通过 docker inspect 可以查看到这两个 volume。

bash 复制代码
[root@docker ~ 18:27:55]# docker inspect vc_data
......
 "Mounts": [
            {
                "Type": "bind",
                "Source": "/root/htdocs",
                "Destination": "/usr/local/apache2/htdocs",
                "Mode": "",
                "RW": true,
                "Propagation": "rprivate"
            },
            {
                "Type": "volume",
                "Name": "67625a8451b3ae288f568a2745908211ed9b9caa2522047547b411ce932eb471",
                "Source": "/var/lib/docker/volumes/67625a8451b3ae288f568a2745908211ed9b9caa2522047547b411ce932eb471/_data",
                "Destination": "/other/userful/tools",
                "Driver": "local",
                "Mode": "",
                "RW": true,
                "Propagation": ""
            }
        ],
......

其他容器可以通过 --volumes-from 使用 vc_data 这个 volume container:

bash 复制代码
[root@docker ~ 18:28:57]# docker run --name web1 -d -p 80 --volumes-from vc_data httpd
968b95745f76beaf062037990407bef0d5449461d84a0c589dc3df9a1b51d9eb
[root@docker ~ 18:29:06]# docker run --name web2 -d -p 80 --volumes-from vc_data httpd
4f24f672d2d8b87305e0334985dc89f8156bcf68ca7f3686e70b5f97e942ec1b
[root@docker ~ 18:29:13]# docker run --name web3 -d -p 80 --volumes-from vc_data httpd
ec178f26d181520ded2395a2eac1a3665131e8018462332c902d3b99a9551943

三个 httpd 容器都使用了 vc_data,看看它们现在都有哪些 volume,以 web1 为例:

bash 复制代码
[root@docker ~ 18:29:19]# docker inspect web1
......
"Mounts": [
            {
                "Type": "volume",
                "Name": "67625a8451b3ae288f568a2745908211ed9b9caa2522047547b411ce932eb471",
                "Source": "/var/lib/docker/volumes/67625a8451b3ae288f568a2745908211ed9b9caa2522047547b411ce932eb471/_data",
                "Destination": "/other/userful/tools",
                "Driver": "local",
                "Mode": "",
                "RW": true,
                "Propagation": ""
            },
            {
                "Type": "bind",
                "Source": "/root/htdocs",
                "Destination": "/usr/local/apache2/htdocs",
                "Mode": "",
                "RW": true,
                "Propagation": "rprivate"
            }
        ],
......

web1 容器使用的就是 vc_data 的 volume,而且连 mount point 都是一样的

下面我们讨论一下 volume container 的特点:

  1. 与 bind mount 相比,不必为每一个容器指定 host path,所有 path 都在 volume container 中定义好了,容器只需与 volume container 关联,实现了容器与 host 的解耦
  2. 使用 volume container 的容器其 mount point 是一致的,有利于配置的规范和标准化,但也带来一定的局限,使用时需要综合考虑。

另一种在容器之间共享数据的方式是 data-packed volume container,下一节讨论。

data-packed volume container

在上一节的例子中 volume container 的数据归根到底还是在 host 里,有没有办法将数据完全放到 volume container 中,同时又能与其他容器共享呢?

当然可以,通常我们称这种容器为 data-packed volume container。其原理是将数据打包到镜像中,然后通过 docker managed volume 共享。

我们用下面的 Dockfile 构建镜像:

bash 复制代码
[root@docker ~ 18:48:23]# vim Dockerfile
FROM busybox:latest
ADD htdocs /usr/local/apache2/htdocs
VOLUME /usr/local/apache2/htdocs

ADD 将静态文件添加到容器目录 /usr/local/apache2/htdocs。
VOLUME 的作用与 -v 等效,用来创建 docker managed volume,mount point 为 /usr/local/apache2/htdocs,因为这个目录就是 ADD 添加的目录,所以会将已有数据拷贝到 volume 中。

修改文本内容

bash 复制代码
[root@docker ~ 18:50:14]# echo "This content is from a data packed volume container!" > htdocs/index.html

build 新镜像 datapacked:

bash 复制代码
[root@docker ~ 18:50:43]# docker build -t datapacked .

用新镜像创建 data-packed volume container:

bash 复制代码
[root@docker ~ 18:51:05]# docker create --name vc_data datapacked
7e95b8f20bcbd6837d59364f1d2d91cd2489b84ef6b2921014b29ebe72b96a8b

因为在 Dockerfile 中已经使用了 VOLUME 指令,这里就不需要指定 volume 的 mount point 了。启动 httpd 容器并使用 data-packed volume container:

bash 复制代码
[root@docker ~ 18:51:27]# docker run -d -p 80:80 --volumes-from vc_data httpd
5777492cb72bb8b2ed1163556c1e1a08a58ca3274dea084eb4ff7edde0514603
[root@docker ~ 18:51:56]# curl 127.0.0.1:80
This content is from a data packed volume container!

容器能够正确读取 volume 中的数据。data-packed volume container 是自包含的,不依赖 host 提供数据,具有很强的移植性,非常适合 只使用 静态数据的场景,比如应用的配置信息、web server 的静态文件等。

容器数据共享就讨论到这里,下一节我们学习如何对 data volume 的生命周期进行管理。

volume生命周期管理

Data Volume 中存放的是重要的应用数据,如何管理 volume 对应用至关重要。前面我们主要关注的是 volume 的创建、共享和使用,本节将讨论如何备份、恢复、迁移和销毁 volume。

备份

因为 volume 实际上是 host 文件系统中的目录和文件,所以 volume 的备份实际上是对文件系统的备份。

还记得前面我们是如何搭建本地 Registry 的吗?

bash 复制代码
[root@docker ~ 19:03:12]# docker run -d -p 5000:5000 -v /myregistry:/var/lib/registry registry:2
Unable to find image 'registry:2' locally
2: Pulling from library/registry
44cf07d57ee4: Pull complete 
bbbdd6c6894b: Pull complete 
8e82f80af0de: Pull complete 
3493bf46cdec: Pull complete 
6d464ea18732: Pull complete 
Digest: sha256:a3d8aaa63ed8681a604f1dea0aa03f100d5895b6a58ace528858a7b332415373
Status: Downloaded newer image for registry:2
62e5c19fb9517c68e08f5674b224129dea2e6913a796646672bcc573f3f6171e

所有的本地镜像都存在 host 的 /myregistry 目录中,我们要做的就是定期备份这个目录。

恢复

volume 的恢复也很简单,如果数据损坏了,直接用之前备份的数据拷贝到 /myregistry 就可以了。

迁移

如果我们想使用更新版本的 Registry,这就涉及到数据迁移,方法是:

  1. docker stop 当前 Registry 容器。

  2. 启动新版本容器并 mount 原有 volume。

    docker run -d -p 5000:5000 -v /myregistry:/var/lib/registry registry:latest

当然,在启用新容器前要确保新版本的默认数据路径是否发生变化。

销毁

可以删除不再需要的 volume,但一定要确保知道自己正在做什么,volume 删除后数据是找不回来的。

docker 不会销毁 bind mount,删除数据的工作只能由 host 负责。对于 docker managed volume,在执行 docker rm 删除容器时可以带上 -v 参数,docker 会将容器使用到的 volume 一并删除,但前提是没有其他容器 mount 该 volume,目的是保护数据,非常合理。

如果删除容器时没有带 -v 呢?这样就会产生孤儿 volume,好在 docker 提供了 volume 子命令可以对 docker managed volume 进行维护。请看下面的例子:

bash 复制代码
[root@docker ~ 19:26:14]# docker volume ls
DRIVER    VOLUME NAME
local     67625a8451b3ae288f568a2745908211ed9b9caa2522047547b411ce932eb471
[root@docker ~ 19:27:05]# docker run --name bbox -v /test/data busybox
[root@docker ~ 19:27:39]# docker volume ls
DRIVER    VOLUME NAME
local     a66cce12b847118da791393ee92f905c4d832c276079478d3da91e24bccdc190
local     67625a8451b3ae288f568a2745908211ed9b9caa2522047547b411ce932eb471

容器 bbox 使用的 docker managed volume 可以通过 docker volume ls 查看到。

删除 bbox:

bash 复制代码
[root@docker ~ 19:27:50]# docker rm bbox
bbox
[root@docker ~ 19:28:17]# docker volume ls
DRIVER    VOLUME NAME
local     67625a8451b3ae288f568a2745908211ed9b9caa2522047547b411ce932eb471
local     a66cce12b847118da791393ee92f905c4d832c276079478d3da91e24bccdc190

因为没有使用 -v,volume 遗留了下来。对于这样的孤儿 volume,可以用 docker volume rm 删除:

bash 复制代码
[root@docker ~ 19:28:19]# docker volume rm  a66cce12b847118da791393ee92f905c4d832c276079478d3da91e24bccdc190
a66cce12b847118da791393ee92f905c4d832c276079478d3da91e24bccdc190
[root@docker ~ 19:28:40]# docker volume ls
local     67625a8451b3ae288f568a2745908211ed9b9caa2522047547b411ce932eb471

如果想批量删除孤儿 volume,可以执行:

docker volume rm $(docker volume ls -q)

删除全部volume

bash 复制代码
[root@docker ~ 19:31:51]# docker rm -fv $(docker ps -a)
[root@docker ~ 19:31:58]# docker volume ls
DRIVER    VOLUME NAME

小结

本章我们学习了以下内容:

  1. docker 为容器提供了两种存储资源:数据层和 Data Volume。
  2. 数据层包括镜像层和容器层,由 storage driver 管理。
  3. Data Volume 有两种类型:bind mount 和 docker managed volume。
  4. bind mount 可实现容器与 host 之间,容器与容器之间共享数据。
  5. volume container 是一种具有更好移植性的容器间数据共享方案,特别是 data-packed volume container。
  6. 最后我们学习了如何备份、恢复、迁移和销毁 Data Volume。

实战:安装mysql

docker hub上查找mysql镜像

bash 复制代码
[root@docker ~]# docker search mysql

从华为云加速器拉取mysql镜像到本地标签为5.7

bash 复制代码
[root@docker ~ 19:32:04]# docker pull mysql:5.7
5.7: Pulling from library/mysql
20e4dcae4c69: Pull complete 
1c56c3d4ce74: Pull complete 
e9f03a1c24ce: Pull complete 
68c3898c2015: Pull complete 
6b95a940e7b6: Pull complete 
90986bb8de6e: Pull complete 
ae71319cb779: Pull complete 
ffc89e9dfd88: Pull complete 
43d05e938198: Pull complete 
064b2d298fba: Pull complete 
df9a4d85569b: Pull complete 
Digest: sha256:4bc6bc963e6d8443453676cae56536f4b8156d78bae03c0145cbe47c2aad73bb
Status: Downloaded newer image for mysql:5.7
docker.io/library/mysql:5.7

使用mysql镜像

简单版

bash 复制代码
[root@docker ~ 19:57:13]# docker run -p 3306:3306 -e MYSQL_ROOT_PASSWORD=12345 -d mysql:5.7
289ea43e269de950e32501f36529e61eae3b81e7065438d0333967be2fe73577
[root@docker ~ 19:58:14]# docker ps
CONTAINER ID   IMAGE       COMMAND                  CREATED         STATUS         PORTS                                                  NAMES
289ea43e269d   mysql:5.7   "docker-entrypoint.s..."   4 seconds ago   Up 3 seconds   0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp   trusting_greider

[root@docker ~ 19:58:17]# docker exec -it 28 bash
bash-4.2# mysql -uroot -p12345

建库建表插入数据

bash 复制代码
#创建数据库叫db01
mysql> CREATE DATABASE db01;
Query OK, 1 row affected (0.00 sec)

#使用db01;
mysql> USE db01
Database changed

mysql> CREATE TABLE tablea (id int,name varchar(20));
Query OK, 0 rows affected (0.00 sec)

mysql> INSERT INTO tablea VALUES(1,'ghl');
Query OK, 1 row affected (0.03 sec)

mysql> SELECT * FROM tablea;
+------+------+
| id   | name |
+------+------+
|    1 | ghl  |
+------+------+
1 row in set (0.00 sec)

外部windows连接运行在docker上的mysql容器实例服务

安装navicat160_premium_cs_x64.exe,一直下一步直到安装完成

双击桌面图标打开

插入中文试试

报错了

为什么报错?

docker上默认字符集不支持中文

bash 复制代码
mysql> SHOW VARIABLES LIKE 'character%';
+--------------------------+----------------------------+
| Variable_name            | Value                      |
+--------------------------+----------------------------+
| character_set_client     | latin1                     |
| character_set_connection | latin1                     |
| character_set_database   | latin1                     |
| character_set_filesystem | binary                     |
| character_set_results    | latin1                     |
| character_set_server     | latin1                     |
| character_set_system     | utf8                       |
| character_sets_dir       | /usr/share/mysql/charsets/ |
+--------------------------+----------------------------+
8 rows in set (0.00 sec)

删除容器后,里面的mysql数据在不在?

bash 复制代码
[root@docker ~ 20:58:00]# docker rm -f 28
28

容器实例删除,你还有什么?删库到跑路?

再用同样的方式创建一个mysql,数据还在么?

bash 复制代码
[root@docker ~ 20:58:07]# docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=12345 mysql:5.7
b3396b87ecbd967ec1e2c6a1ff90642644c2c2b19998b6e043c9728ae5e1f52b

实战版

新建mysql实例

bash 复制代码
[root@docker ~ 21:15:46]# docker run -d -p 3306:3306 --privileged=true -v /ghl/mysql/log:/var/log/mysql -v /ghl/mysql/data:/var/lib/mysql -v /ghl/mysql/conf:/etc/mysql/conf.d -e MYSQL_ROOT_PASSWORD=123456 --name mysql mysql:5.7
aadd0e1569d45a6210403faba3465d3e01081cf76809e3fb98fcb96a471f5c48

新建my.cnf,通过容器卷同步给mysql容器实例

bash 复制代码
[root@docker ~ 21:18:49]# cd /ghl/mysql/conf/
[root@docker conf 21:18:59]# ls
#实现mysql支持中文
[root@docker conf 21:19:01]# vim my.cnf
[root@docker ~ 21:18:49]# cd /ghl/mysql/conf/
[root@docker conf 21:18:59]# ls
[root@docker conf 21:19:01]# vim my.cnf

重新启动mysql容器实例再重新进入并查看字符编码

bash 复制代码
[root@docker conf 21:19:42]# docker restart mysql
mysql
[root@docker conf 21:19:56]# docker exec -it mysql bash
bash-4.2# mysql -uroot -p123456
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.7.44 MySQL Community Server (GPL)

Copyright (c) 2000, 2023, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> SHOW VARIABLES LIKE 'character%';
+--------------------------+----------------------------+
| Variable_name            | Value                      |
+--------------------------+----------------------------+
| character_set_client     | utf8                       |
| character_set_connection | utf8                       |
| character_set_database   | utf8                       |
| character_set_filesystem | binary                     |
| character_set_results    | utf8                       |
| character_set_server     | utf8                       |
| character_set_system     | utf8                       |
| character_sets_dir       | /usr/share/mysql/charsets/ |
+--------------------------+----------------------------+
8 rows in set (0.00 sec)

mysql> CREATE DATABASE db01;
Query OK, 1 row affected (0.00 sec)

mysql> USE db01;
Database changed

mysql> CREATE TABLE tablea(id int,name varchar(20));
Query OK, 0 rows affected (0.00 sec)

mysql> INSERT INTO tablea VALUES(1,'ghl');
Query OK, 1 row affected (0.02 sec)

mysql> SELECT * FROM tablea;
+------+------+
| id   | name |
+------+------+
|    1 | ghl  |
+------+------+
1 row in set (0.00 sec)

在新建库新建表再插入中文测试

不报错了!

假如当前容器实例删除,再重新来一次,之前创建的db01实例还有吗??赶紧动起来尝试一下吧!!!

再次删库跑路!!!

bash 复制代码
[root@docker conf 21:26:15]# docker rm -f aa
aa
[root@docker conf 21:26:42]# docker run -d -p 3306:3306 --privileged=true -v /ghl/mysql/log:/var/log/mysql -v /ghl/mysql/data:/var/lib/mysql -v /ghl/mysql/conf:/etc/mysql/conf.d -e MYSQL_ROOT_PASSWORD=123456 --name mysql mysql:5.7
5755c870ce7b1cb4bb42bf9caa9874ece8208ac3be94c28d21a817d09bb2dfa2

发现数据还在

相关推荐
小钱c72 小时前
docker相关常用指令
运维·docker·容器
ChenXinBest2 小时前
一次firewalld和docker冲突问题排查
linux·docker
随风语2 小时前
Docker学习
docker·容器
星光一影2 小时前
同城搭子活动组局H5系统源码-伴伴搭子系统源码
vue.js·mysql·php·uniapp
m0_488777652 小时前
Docker Compose 编排
docker·容器·docker-compose·编排管理多个服务
Code知行合壹2 小时前
Kubernetes实战进阶
云原生·容器·kubernetes
xuhe22 小时前
[重磅更新] 支持最新 Overleaf 6.x!我的私有化部署方案 xuhe2/sharelatex-ce 迎来大升级
linux·docker·github·科研·overleaf
不想画图2 小时前
dockerfile镜像构建和docker compose编排
docker·容器
半壶清水3 小时前
ubuntu中PHP升级详细方法
linux·ubuntu·php
半壶清水3 小时前
ubuntu中使用使用Docker-Compose管理MySQL、Apache、PHP容器
mysql·ubuntu·docker·php·apache