Docker - Volume & Network Usage
Created by : Mr Dk.
2020 / 09 / 08 23:04
Nanjing, Jiangsu, China
通过一个最简单的例子来了解 Docker 中的 卷 (volume) 和网络。例子很简单:在 Docker 中运行一个 Nginx,Nginx 的静态资源目录 (比如网页代码) 位于宿主机上,并以卷的形式挂在到容器中。然后在宿主机上访问容器中的 Nginx 监听的端口。
准备工作
首先准备一个能够在容器中启动 Nginx 的镜像:
- 准备好操作系统 (基础镜像),并安装 Nginx
- 将 Nginx 的配置文件在宿主机上提前准备好,使用
ADD指令复制到镜像中 - 指明从镜像启动的容器暴露自己的
80端口
其余上述思想,实现了一个 Dockerfile。Dockerfile 所在目录的组织方式如下:
$ tree -a
.
├── Dockerfile
├── nginx
│ ├── global.conf
│ └── nginx.conf
└── src
└── index.html
2 directories, 4 files
Dockerfile 具体指令如下:
FROM ubuntu:20.04
MAINTAINER mrdrivingduck "mrdrivingduck@gmail.com"
RUN sed -i 's/archive.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list
RUN apt-get -yqq update && apt-get -yqq install nginx
RUN mkdir -p /var/www/html/website
ADD nginx/global.conf /etc/nginx/conf.d/
ADD nginx/nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
首先来看看 Nginx 的两个配置文件吧。首先是 nginx/global.conf,会被复制到容器中的 /etc/nginx/conf.d/ 下。其中指定了:
- 监听本机 (容器) 的
80端口 - 静态资源目录位于
/var/www/html/website/ - 日志路径
server {
listen 0.0.0.0:80;
server_name _;
root /var/www/html/website;
index index.html;
access_log /var/log/nginx/default_access.log;
error_log /var/log/nginx/default_error.log;
}
其次是 nginx/nginx.conf,将会被复制到 /etc/nginx/ 下。可以看到 Nginx 被配置成了一个基本的 HTTP 服务器。注意,配置中的 daemon off 使得 Nginx 在容器中以交互模式运行。
user www-data;
worker_processes 4;
pid /run/nginx.pid;
daemon off;
events { }
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
gzip on;
gzip_disable "msie6";
include /etc/nginx/conf.d/*.conf;
}
构建镜像
接下来,使用 Dockerfile 构建镜像。
$ sudo docker build -t mrdrivingduck/website .
Sending build context to Docker daemon 6.144kB
Step 1/8 : FROM ubuntu:20.04
---> 4e2eef94cd6b
Step 2/8 : MAINTAINER mrdrivingduck "mrdrivingduck@gmail.com"
---> Running in 88369a6f9503
Removing intermediate container 88369a6f9503
---> 2e6504b82ad7
Step 3/8 : RUN sed -i 's/archive.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list
---> Running in 66de419b4a0b
Removing intermediate container 66de419b4a0b
---> 8705db5025d4
Step 4/8 : RUN apt-get -yqq update && apt-get -yqq install nginx
---> Running in 5397334a1984
Removing intermediate container 5397334a1984
---> bc3ec63ef95e
Step 5/8 : RUN mkdir -p /var/www/html/website
---> Running in be3b9e8b57af
Removing intermediate container be3b9e8b57af
---> 1d854b54248b
Step 6/8 : ADD nginx/global.conf /etc/nginx/conf.d/
---> 108f1e4a78cb
Step 7/8 : ADD nginx/nginx.conf /etc/nginx/nginx.conf
---> 4b1099f18e8d
Step 8/8 : EXPOSE 80
---> Running in 188934c22f2d
Removing intermediate container 188934c22f2d
---> ea637a98908b
Successfully built ea637a98908b
Successfully tagged mrdrivingduck/website:latest
镜像构建完毕后,查看:
$ sudo docker image list
REPOSITORY TAG IMAGE ID CREATED SIZE
mrdrivingduck/website latest ea637a98908b 22 minutes ago 156MB
启动容器 & 卷
接下来启动容器:
- 容器以后台模式而非交互模型运行,所以使用
-d选项 - Dockerfile 的
EXPOSE指令指定容器将暴露80端口,因此这里使用-p 80选项打开80端口 - 这个容器被我自行命名
--name website - 通过
-v属性将宿主机下的$PWD/src目录作为卷挂载到容器中的/var/www/html/website目录 - 容器的启动命令为
nginx
$ sudo docker run -d -p 80 --name website -v $PWD/src:/var/www/html/website mrdrivingduck/website nginx
7302af622f9810b14b2107111346bb56ea95f14d5ba8b7d4a00e0f96dbd1c816
容器以后台模式运行,因此命令返回一个容器 ID 后就结束了。可以看到容器正在运行:
$ sudo docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7302af622f98 mrdrivingduck/website "nginx" 7 seconds ago Up 6 seconds 0.0.0.0:32770->80/tcp website
在上面的信息中可以看到,容器的 80 端口被映射到了宿主机的 32770 端口上。因此,通过 宿主机 IP:32770 或 容器 IP:80 都可以访问容器中的 Nginx。这里我们用宿主机 IP 进行尝试。命令得到的 HTML 就是宿主机目录 src/index.html 中的内容。
$ curl localhost:32770
<h1>Hello Docker!</h1>
$ curl localhost:32770
<h1>Hello Docker!</h1>
<p>Edited On Host!</p>
为什么两次命令的结果不一样呢?因为在两条命令中间,我直接对宿主机上的 src/index.html 进行了编辑。这里就体现了 卷 的特性。卷是一个宿主机目录,它可以被一个或多个容器选定,绕过 Docker 的联合文件系统,直接 mount 到容器内的某个目录上。卷用于为 Docker 提供 持久数据 或 共享数据,对卷的修改会立刻生效。当提交或创建镜像时,卷不包含在其中。在我的理解中,卷有点像一个能同时插在多台电脑上的移动硬盘。
如果不想把应用或者代码构建到镜像中时 (比如只想在镜像中营造一个生产环境,但是开发中的代码位于宿主机上),就体现出了卷的价值:
- 同时对代码开发和测试
- 代码改动频繁,不想在开发过程中反复构建镜像
- 在多个容器之间共享代码
在 docker run 命令中,通过 -v source:target:ro 指定卷的映射。如果容器内目录 target 不存在,Docker 会自动创建一个。在最后加上 rw 或 ro 来指定 容器内目录 (target) 的读写权限。
Docker 的网络连接
上述的例子中,通过搭建了一个生产环境容器,成功地在开发的同时测试了代码在生产环境中的使用情况。而其中包含了一个细节:我们有多少种访问容器内 Nginx 80 端口的方式?
- 宿主机上的进程作为客户端访问
- 在容器内的其它进程作为客户端访问
- 其它容器内的进程作为客户端访问
这里就涉及到了 Docker 的网络连接是如何实现的。Docker 中有三种网络连接方式:
- Docker 内部网络 (不太灵活)
- Docker Networking (After Docker 1.9,推荐)
- Docker 链接
Docker 内部网络
在默认情况下,Docker 容器都是公开端口并绑定到宿主机的网络接口上,这样可以把 Docker 内提供的服务放到宿主机所在的外部网络上公开。在安装 Docker 时,会创建一个新的网络接口 docker0。每个 Docker 容器都会在这个接口上被分配 IP 地址:
$ ifconfig
docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.17.0.1 netmask 255.255.0.0 broadcast 172.17.255.255
inet6 fe80::42:6ff:feae:e946 prefixlen 64 scopeid 0x20<link>
ether 02:42:06:ae:e9:46 txqueuelen 0 (Ethernet)
RX packets 21599 bytes 1191336 (1.1 MB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 42554 bytes 63422188 (63.4 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
veth0f97e6d: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet6 fe80::6ca3:8bff:fe50:3210 prefixlen 64 scopeid 0x20<link>
ether 6e:a3:8b:50:32:10 txqueuelen 0 (Ethernet)
RX packets 28 bytes 2858 (2.8 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 70 bytes 7060 (7.0 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
Docker 会使用一个 172.17.x.x 的子网作为 Docker 内部网络,docker0 对应的 IP 地址也就是这个网络的网关。docker0 是一个虚拟的以太网桥。Docker 每创建一个容器,就会创建一组互相连接的网络接口,与 管道 类似。管道的一端连接容器内的虚拟网卡 (如 eth0,IP 地址也在子网中),管道的另一端以 veth* 的名称连接到宿主机的 docker0 上。由此,Docker 将维护一个虚拟子网,由宿主机和所有 Docker 容器共享。
Docker 内部网络的互连还受宿主机的 防火墙规则 与 NAT 配置 的影响。在默认情况下,宿主机无法访问容器;只有明确指定了打开的端口,宿主机与容器才能够通信。以上面的应用场景为例,可以在路由表中看到容器的 80 端口与宿主机 32770 端口的通信规则:
$ sudo iptables -t nat -L -n
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:32770 to:172.17.0.2:80
通过以下命令,可以查看容器的网络详情:
$ sudo docker inspect website
[
{
"Id": "7302af622f9810b14b2107111346bb56ea95f14d5ba8b7d4a00e0f96dbd1c816",
...
"NetworkSettings": {
"Bridge": "",
"SandboxID": "bb4af0908c0eb3663ab86175bbc4cab01d11e5547390348b9d963fa5e951ba22",
"HairpinMode": false,
"LinkLocalIPv6Address": "",
"LinkLocalIPv6PrefixLen": 0,
"Ports": {
"80/tcp": [
{
"HostIp": "0.0.0.0",
"HostPort": "32770"
}
]
},
"SandboxKey": "/var/run/docker/netns/bb4af0908c0e",
"SecondaryIPAddresses": null,
"SecondaryIPv6Addresses": null,
"EndpointID": "9bd094ac1d2e4bc247fa843a5db32e921d995d357a4c57be8035db9e7eef960d",
"Gateway": "172.17.0.1",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"IPAddress": "172.17.0.2",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"MacAddress": "02:42:ac:11:00:02",
"Networks": {
"bridge": {
"IPAMConfig": null,
"Links": null,
"Aliases": null,
"NetworkID": "062db7d45781261af12df968dc9c139df61ba129ed8f5b356c5f90851e0ae361",
"EndpointID": "9bd094ac1d2e4bc247fa843a5db32e921d995d357a4c57be8035db9e7eef960d",
"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
}
}
}
}
]
从上面的信息中可以看到,容器的 IP 地址为 172.17.0.2,与宿主机网关 172.17.0.1 在同一个子网中。因此,不仅通过宿主机 IP 地址可以访问到容器的的 Nginx (172.17.0.1:32770),还可以通过容器 IP 地址访问 Nginx (172.17.0.2:80)。
这种容器局域网网络看似简单,但很不灵活:
- 需要根据容器的 IP 地址进行硬编码
- 重启容器后,Docker 会给容器分配新的 IP 地址
Docker Networking
Docker Networking 允许用户自行创建网络,甚至支持跨不同宿主机的容器间通信,网络配置也可以更加灵活地定制。想要使用这一套机制,首先需要 创建一个网络,然后在这个网络下启动容器。首先建立一个名为 mynet 的网络:
$ sudo docker network create mynet
08e97c3f58b9b128c5958b36e96c769b3c242ea55ab21af175b12cb40ffcadf7
$ sudo docker network inspect mynet
[
{
"Name": "mynet",
"Id": "08e97c3f58b9b128c5958b36e96c769b3c242ea55ab21af175b12cb40ffcadf7",
"Created": "2020-09-08T22:27:32.178555856+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": {}
}
]
$ sudo docker network ls
NETWORK ID NAME DRIVER SCOPE
062db7d45781 bridge bridge local
f88e74985810 host host local
08e97c3f58b9 mynet bridge local
abdf15c3754d none null local
可以看到这个网络的网关为 172.18.0.1,这是一个新的子网,且暂时没有任何容器在其中。再重新启动容器时,需要在 docker run 命令中启用 --net=mynet 显式将容器启动在新的网络中。然后再重新查看以下网络:
$ sudo docker rm website
website
$ sudo docker run -d -p 80 --name website --net=mynet -v $PWD/src:/var/www/html/website mrdrivingduck/website nginx
f550227293ff831255b76972011ee88120a7d44fadf29e2cd92161e9d5704d7b
$ sudo docker network inspect mynet
[
{
"Name": "mynet",
...
"Containers": {
"f550227293ff831255b76972011ee88120a7d44fadf29e2cd92161e9d5704d7b": {
"Name": "website",
"EndpointID": "61a412d1bc738020e606800e74fac45ff9dc85a45d2463b9cac0866b3691d377",
"MacAddress": "02:42:ac:12:00:02",
"IPv4Address": "172.18.0.2/16",
"IPv6Address": ""
}
},
"Options": {},
"Labels": {}
}
]
可以看到运行 Nginx 的容器已经被加入到网络中。这与之前的内部网络相比,到底灵活在哪呢?Docker 将会自动感知所有在这个网络下运行的容器,并将容器的网络信息保存到 /etc/hosts 中。在该文件中,除了该容器本身的 IP 地址,还会保存网络内其它容器的 IP 地址,并映射到 <container_name>.<network_name> 形式的域名上。重要的是,当任意一个容器重启时,其 IP 地址信息会自动在网络内所有容器的 /etc/hosts 中更新。如果容器内的上层应用全部使用 域名 而不是 IP 地址,那么容器的重启不会对应用程序产生影响。
通过 docker network connect <network_name> <container_name> 命令,可以将一个正在运行的容器添加到特定网络中。同理,docker network disconnect 就不多说。另外,一个容器还可以 同时隶属多个网络,从而构建复杂的网络模型。
Docker 链接
在 Docker 1.9 之前推荐这种方式。让一个容器链接到另一个容器是个简单的过程,只需要引用两个容器的名字就好了。先启动第一个容器,然后启动第二个容器时,在 docker run 命令中附加 --link <target_container>:<link_alias>。其中,第二个容器是 客户容器,要链接到的目标容器是 服务容器。容器启动后,会将 --link 中的参数添加到 /etc/hosts 中。在这样的链接后,只有客户容器可以直接访问服务容器的公开端口,而其它容器不行;服务容器的端口也不需要对宿主机公开。由此,这个模型非常安全,可以限制容器化应用程序的被攻击面,减少容器暴露的端口。
可以将多个客户容器链接到一个服务容器上。容器链接目前只能工作于同一台 Docker 宿主机中。
Docker 在建立容器间的链接时,会在自动创建一些以 链接别名 命名的环境变量,包含了丰富的链接信息。
灵活性
所谓的灵活性,就是避免在应用程序进行容器间通信时使用硬编码的 IP 地址。根据上述三种网络连接方式,灵活性通过以下两种方法保证:
- 利用环境变量中的连接信息
- 利用
/etc/hosts中的 DNS 映射信息