在上一篇中,我们讨论了 container 如何启动等基本的指令,以及简单地介绍了一下 image 等,通过这些我们知道了怎么 pull image、怎么启动、停止一个 container 等。不过,到目前为止我们仅仅停留在单独一个 container 的运行,今天就让我们来讨论看看 container 如何对外沟通。
既然要沟通,八九不离十就是要讲网络了,而我想要分成以下角度来讨论:
-
Container 跟 Host 之间(Host 是指用来跑 Docker 的环境,例如一台 Linux 或是 mac 之类的。)
-
Container 之间
-
不同 Host 的 container 之间
本次让我们来讨论比较简单的前两个:
Container 与 Host 之间
按照惯例让我们先用以下指令来启动一个 nginx 的 web server:
css
$ docker container run --name websrv1 -d -p 9090:80 nginx
启动 nginx container
刚好复习一下,在我们执行了这个指令后,因为本地没有 nginx 这个 image (Unable to find image 'nginx:latest' locally
),所以 Docker 会去 Docker Hub 上面 pull 这个 image 下来,也可以看到 nginx 这个 image 有 7 个 layer。
但跟上篇不太一样的是,我们加上了 --name
、 -d
与 -p
这三个参数,少了 -it
与 /bin/bash
,而且指令执行完后并没有进入 container 中。
--name
顾名思义就是给这个 container 取个名字,它会显示在我们执行 docker container ls
时显示的最后一栏 NAMES,如果像上篇那样没有指定,Docker 会帮你随机取个名字,做好命名,除了能更好的辨识之外,在操作 container 时,也可以用这个名字来取代 CONTAINER_ID ,例如:
php
# 用 name 取代 CONTAINER_ID
$ docker container stop websrv1
再来是 -d
( --detach
),这个参数让我们在启动 container 后可以停留在本机中,因为这个参数的意思是让这个 container 在 Host(执行 Docker 的这台电脑)的后台运行,所以执行之后,不会像上篇那样进入了一个执行 /bin/bash
的容器之中,如果不相信的话,你可以执行 docker container ls
来检查看看,这个 container 是在运行的。而当然,因为已经后台运行了,我们不跟这个 container 交互,所以不需要 -it
这两个参数了。
列出 container
在后台运行的容器,如果你想要进入这个 container 也还是可以的,就是通过我们上篇分享的 docker container exec
就可以了,注意在下图中,我们是用 websrv1 这个 container name 取代了 ID。
进入 nginx container 中
最后一个不一样的参数是 -p 9090:80
( --publish
),这个参数意思是说我们将我们本机的 9090 端口映射到 container 的 80 端口,当然,必须要这个 container 有开放 80 端口,这个映射才有意义。
端口映射
在设置了这个端口映射之后,你可以用浏览器打开 http://localhost:9090,这样就可以看到 nginx 的首页。或是在直接用 curl
指令测试。
在 Linux 上用 curl 指令测试
当我们对这台主机的 9090 端口 发出 HTTP 请求时,实际上是被转发到了 container 中。
Container 之间
首先我们先用 alpine 这个轻量级的 image 来启动两个 container:
shell
$ docker container run -dit --name alpine1 alpine /bin/sh
$ docker container run -dit --name alpine2 alpine /bin/sh
由于本机中没有 alpine 这个 image,所以一样 Docker 会先去 pull image 回来,在开始之前我们来看一下它有多轻量:
列出 image
有没有很惊人,竟然只有 7.34 MB,相较于 alpine , nginx , node 这些 image 真的是非常庞大了
再往下之前,不知道大家有没有发现到,这里在用 alpine 启动 container 时,用的参数是 -dit
,但之前讨论过的 -d
是让 container 在后台运行,那又为什么需要加上 -it
呢?这不是有需要交互时才需要的吗?这是因为 alpine 这样的 image 不像 nginx 一样, nginx image 在启动的时候会执行一个能持续运行的程序,例如 nginx image 是 nginx -g daemon off;
。而 alpine 则不然,它默认是执行 /bin/sh
,而我们上述的指令,是让它启动 sh
,如果没有加上 -it
,那这个 shell 会被开启,然后很快地就又结束了,container 也会随之关闭,所以我们需要加上 -it
来 sh 或其他 shell 可以被分配一个虚拟终端机 (pseudo terminal),以保持 container 的运行。
好,现在让我们来用 docker container inspect
这个指令查看我们所启动的 container:
shell
# 因为启动的时候有命名,所以这边可以直接用 container 的名字,没有命名的话,
# 可以用 Docker 随机给的,或是用 container id。
$ docker container inspect alpine1
当 inspect
这个指令下下去,会看到一大堆信息,有点太多了,我们来选择一下我们想看的部分:
yaml
$ docker container inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' alpine1
172.17.0.2
$ docker container inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' alpine2
172.17.0.3
由上述指令可以发现这两个 container 的 ip 分别为 172.17.0.2 与 172.17.0.3,看起来很像在同一个网络,而且会跟你 Host 的 ip 不同。
我们进入第一个 container alpine1 去测试看看:
进入 alpine1 测试网络
通过这个实验,我们发现在 alpine1 这个 container 中是可以通过 IP 直接 ping
到 alpine2 的,此外,也能 ping
到 google,也就是具有访问外部网络的能力,我们在启动 container 时,根本无须额外设置什么就能让 container 之间可以互相沟通跟具有对外沟通的能力。
不过,事情有这么简单吗?
Docker 的网络模型
让我们来看看 Docker 的网络模型,因为目前还是给初学者学习 Docker 用的,所以我们不会讲到太难的部分,但可以先有一个概念是 Docker 的网络系统是「可插拔」的,Docker 安装完后通常会提供几个默认的,但如果你想要用第三方开发的来替换掉,也是可以的。
Docker 默认提供的网络驱动:bridge
, host
, overlay
, ipvlan
, macvlan
。以下就让我们讨论看看 bridge
与 host
:
bridge
我们可以通过 docker network ls
这个指令来查看目前的网络有哪些:
列出 Docker 网络
这里可以看到默认有三个网络 bridge
, host
与 none
,而这三个网络使用的驱动分别是 bridge
, host
跟 null
。(因为默认的网络 bridge
与 host
会跟它们的 driver
同名,所以讨论起来真的是很容易混淆,后续的讨论中,要注意在讲的是网络还是驱动。)
bridge 网络(驱动是 bridge)是 Docker 默认采用的网络,也就是当我们在启动 container 时,如果没有指定使用哪一个网络,默认就是用这一个,所以其实我们到目前为止启动的 container 都是用 bridge 这种网络驱动。
在我们刚刚启动的两个 alpine container 的情况下,我们来查看一下默认的这个 network:
php
# 不管查看 network 还是查看 container,都一致性地用了 inspect,相当地好记
$ docker network inspect bridge
在显示结果中找到 Containers 这一部分:
可以看到我们刚刚创建的两个 alpine containers 被列在这里了。此外,也可以看到其子网为 "172.17.0.0/16",而我们用 bridge 这个网络创建出来的 container 的 ip 也是在这个子网范围中。
让我们来查看一下 container 与 Host 的网络:
从下图可以看到 alpine1 有一个 eth0@if19 的网络接口:
从下图可以看到 alpine2 有一个 eth0@if21 的网络接口:
从下图观察到 Host 中跟 Docker 有关的接口有 docker0、veth68f022d@if18、vethdd292f5@if20。
在 Host(本机)中的这个 docker0 是在安装 Docker 之后会被创建的一个 bridge,那bridge 与这些虚拟网卡之间怎么沟通呢?简单的来说,大概就是下图:
Docker bridge 网络
所以其实就是借由 docker0 这个 bridge,让 container 之间能互相沟通,也让 container 有了对外部网络进行访问的能力。
除了默认的这个 bridge 网络之外,我们还可以自定义网络,且官方建议我们在生产环境用我们自定义的 bridge 网络,而不是默认的这一个。
现在让我们来自定义一个 bridge network 试试看:
sql
$ docker network create --driver bridge my-net
0b0d2ab783c5da3970c92ca3d9c6e538ad68f9efee398a8aa6998844f5fbb6e8
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
f913defc7bff bridge bridge local
2475296a2439 host host local
0b0d2ab783c5 my-net bridge local
b75b01b8790f none null local
通过 docker network ls
我们看到有两个 network 都是 bridge 这种 driver,其中一个是我们刚刚创建的 my-net。让我们通过 docker network inspect
来查看一下:
php
$ docker network inspect my-net
[
{
"Name": "my-net",
"Id": "0b0d2ab783c5da3970c92ca3d9c6e538ad68f9efee398a8aa6998844f5fbb6e8",
"Created": "2023-08-29T02:19:39.985407948Z",
"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": {}
}
]
通过 inspect 可以看到这个我们自定义的 my-net 网络其子网是 172.18.0.0/16(你跟我的可能会不同),所以等一下用我们自已的这个网络启动的 container 的 ip 应该都会是在这个范围中。
让我们停止并移除刚刚创建的两个 alpine container,重新创建,并且通过 --network
来指令使用我们刚刚创建出来的 my-net:
shell
# 停止并移除 alpine1 与 alpine2 containers
$ docker container stop alpine1 alpine2
$ docker container rm alpine1 alpine2
# 启动两个 container,network 都指定为 my-net
$ docker container run -dit --network my-net --name alpine1 alpine
b4b9db3bc2a3946b9176d6694c65fffd0d23ac66e37605dba107aa32e531454f
$ docker container run -dit --network my-net --name alpine2 alpine
42449933ecbe4a8fd0f4cce3a1cd237436d79538bd04cde026e2296332a07e5a
# 查看这两个 container 的 IP
$ docker container inspect -f '{{range.NetworkSettings.Networks}} {{.IPAddress}}{{end}}' alpine1
172.18.0.2
$ docker container inspect -f '{{range.NetworkSettings.Networks}} {{.IPAddress}}{{end}}' alpine2
172.18.0.3
其 IP 果然是落在新的子网中。让我们进入 alpine1 中去测试看看:
一切都跟默认的那个 bridge 网络一样,不过,有一个地方不太一样,那就是我们自定义的网络可以通过「名字」来进行沟通,这个在默认的 bridge 网络是做不到的喔。
现在让我们来启动第三个 container,但让它用默认的 bridge 网络:
php
$ docker container run -dit --name alpine3 alpine
8bb081fcfb1346f9d8a95c6f81a0e2889d2bde663e0a3ea1d92950bbeffff0c9
$ docker container inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' alpine3
172.17.0.2
可以看到 alpine3 的 IP 是在默认的 bridge 网络的子网中。回到 alpine1 中:
果然 ping 不到了!
通过 ip addr
查看 Host 网卡,可以看到除了刚刚的 docker0 外,现在有一个新的 bridge br-0b0d2ab783c5
被创建出来了,这个 bridge 就是用来负责 my-net 这个网络的沟通工作的。
最后,我们再来创建一个用 my-net 的 container,但这次我们在创建后,会把这个 container 连接到默认的那一个 bridge 网络去:
perl
$ docker container run -dit --network my-net --name alpine4 alpine
85e7772028766549fe5873765b1b339e6c291012dbd2c86aa14334eb68a06b7d
# alpine4 创建时,网络是设置成 my-net,
# 但可以通过 docker network connect 指令将这个 container 连接至其他网络
$ docker network connect bridge alpine4
让我们看看 bridge 与 my-net 这两个网络的情况:
php
$ docker network inspect bridge
如下图,bridge 这个网络底下果然有两个 containers:
bridge 底下的 containers
perl
$ docker network inspect my-net
如下图,my-net 底下有三个,而且 alpine4 同时出现在 bridge 与 my-net 里。
my-net 底下的 containers
让我们看一下 alpine4 的网络:
alpine4 里的网络装置
alpine4 有两张网卡:eth0 其 IP 为 172.18.0.4,是属于 my-net 的,另外一个 eth1 其 IP 为 172.17.0.3,是属于默认的 bridge 网络的。
到目前为止,整个网络结构会如下图:
进入 alpine4 去测试看看:
python
$ docker container exec -it alpine4 ash
# 可以用名字来 ping 到 alpine1
/ # ping -c 2 alpine1
PING alpine1 (172.18.0.2): 56 data bytes
64 bytes from 172.18.0.2: seq=0 ttl=64 time=0.202 ms
64 bytes from 172.18.0.2: seq=1 ttl=64 time=0.090 ms
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.090/0.146/0.202 ms
# 不可以用名字来 ping 到 alpine3,默认的 bridge 网络不能用名字沟通
/ # ping -c 2 alpine3
ping: bad address 'alpine3'
# 可以用 ip ping 到 alpine3
/ # ping -c 2 172.17.0.2
PING 172.17.0.2 (172.17.0.2): 56 data bytes
64 bytes from 172.17.0.2: seq=0 ttl=64 time=0.338 ms
64 bytes from 172.17.0.2: seq=1 ttl=64 time=0.085 ms
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.085/0.211/0.338 ms
host
这种网络驱动是让 container 直接采用本机(host)网络,也就是不隔离网络了。而在 Docker 安装完成后,也会自动创建一个 host driver 的 host network,这个刚刚我们已经通过 docker network ls
看过了。
那就让我们来启动一个 nginx,但指定采用 host 这个 network,来看看会发生什么事:
xml
# 先确认 host 上 端口 80 的使用情况
$ ss -tnpl | grep :80
# 会没有任何结果
# 通过参数 --network 来指定使用何种网络驱动
$ docker container run -d --network host nginx
6d1696969c526c7abdee05e8905823d23964e2c4cb78fe8b78d203ea57230a28
# 查看 container
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6d1696969c52 nginx "/docker-entrypoint...." 11 seconds ago Up 10 seconds sleepy_golick
# 测试连接
$ curl localhost
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
...略
</style>
</head>
<body>
...略
</body>
</html>
# 确认 host 上 端口 80 的使用情况
$ ss -tnpl | grep :80
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
LISTEN 0 511 [::]:80 [::]:*
由上图可以看到,我们在启动 container 时,通过 --network
参数来指定用 host
网络,这是让这个 container 直接地使用了 host 的网络,在这个情况下,我们也不需要用 -p
来设置端口映射,因为当 container 中是要用 80 端口 时,它就会直接占用了我们本机的 80 端口。我们也通过 curl
跟 ss
指令验证了这件事。
备注1:host 网络驱动仅支援 Linux 操作系统,所以如果你的 host 跟我一样是 mac 或是 windows,那这个实验会做不出来喔。
备注2: 如果你用来实验的 linux 操作系统上 80 端口 已经被占用了的话,这个 container 就会启动失败,所以操作前可以先检查一下。
结语
本篇简单介绍了两个网络驱动 bridge 与 host,也展示了如何将 container 中的端口映射出来,以及 container 间是通过 bridge 来互相沟通与访问外部网络。
最后要推荐一下 Docker 的官方文档,至少在 bridge 与 host 这边写得还蛮不错的,本篇其实也只是记录了一下我自己做官方文档中提供的实验的纪录而已,真心推荐大家亲自去看看。
指令整理
这边整理本文讨论过的指令,方便大家练习与查找:
shell
# 从 image nginx 启动名称为 websrv1 的 container,设置为后台运行,并且将 container 的 80 端口 映射到本机的 9090
$ docker container run --name websrv1 -d -p 9090:80 nginx
# 查看名为 alpine1 的 container 的信息
$ docker container inspect alpine1
# 查看名为 alpine1 的 container 的信息,但只看 NetworkSettings 中的 Networks 的 IPAddress
$ docker container inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' alpine1
# 查看 docker 目前的网络
$ docker network ls
# 查看名为 bridge 这个网络的信息
$ docker network inspect bridge
# 启动名为 alpine1 的 container,并且其网络设置为 my-net
$ docker container run -dit --network my-net --name alpine1 alpine ash
# 将 alpine4 这个 container 连结至 bridge 这个网络
$ docker network connect bridge alpine4
# 移除 my-net 这个 docker 网络
$ docker network rm my-net