《精通Docker第三版》完整目录:
第一章 Docker概览
第二章 创建容器镜像
第三章 存储和发布镜像
第四章 管理容器
第五章 Docker Compose
第六章 Windows容器
第七章 Docker Machine
第八章 Docker Swarm
第九章 Docker和Kubernetes
第十章 在公有云上运行Docker
第十一章 Portainer – 一个Docker的GUI
第十二章 Docker安全
第十三章 Docker工作流
第十四章 Docker进阶
学到这里,我们主要集中在如何构建、存储和发布我们的Docker镜像。下面我们来看看如何启动容器,以及使用Docker命令行客户端来管理窗口和与容器交互。
在深入学习其它可用的命令之前,我们将重新使用第一章所使用到的命令,并会更深入讲解这些命令。一旦我们熟悉了这些容器命令之后。我们会来学习Docker的网络和Docker数据卷。
本章主要涵盖如下内容:
Docker容器命令
命令基础
与容器交互
日志和进程信息
资源限制
容器状态和其它命令
删除容器
Docker网络和数据卷
技术准备
本章中,我们将继续使用我们的本地Docker安装。和此前一样,本章中的截图来自我个人偏好的操作系统macOS,但我们所运行的Docker命令可以在我们所安装的三个操作系统上都可以运行。不过,其中有极少数的命令仅能用于macOS和基于Linux的操作系统。
访问如下视频来查看实现代码的运行:http://t.cn/AiCgEnfu
Docker容器命令
在我们深入学习更复杂的Docker命令之前,让我们先来做个回顾,再来深入了解下前几章中所使用过的命令。
基础知识
在第一章 Docker概览 中,我们使用如下命令启动了一个最基础的容器,helloworld容器:
$ docker container run hello -world
你应该还记得,这一命令从Docker Hub上拉取一个1.84 KB的镜像。你可以在Docker商店页面https://hub.docker.com/_/hello-world上找到这一镜像,如同下面这个Dockerfile,它执行了一条名为hello的可执行命令:
FROM scratch
COPY hello /
CMD [ "/hello" ]
hello执行语句向终端打印了文本Hello from Docker!,然后进行退出。在如下的Terminal的输出中可以看到完整的输出文本,hello二进制还告诉了你刚刚所发生的具体步骤:
在进程退出时,容器也停止了,可通过运行如下命令看出:
该命令的输出如下:
可以看到在Terminal中第一次运行加和不加-a 标记的docker container ls时输出,其中 -a 为–all 的简写形式,不加-a 标记不会显示已退出的容器。
我们无需为这一容器命名,因为它在调用后很快就会关闭。但Docker会自动为容器分配名称,我这里得到的名称为 jolly_fermat。
如果你让Docker 自动分配名称,还会看到在使用过程中,它会为容器生成一些非常有意思的名称。虽然有些跑题,这段生成代码可通过names-generator.go进行查看。在代码结尾处,有如下语句:
if name == "boring_wozniak" /* Steve Wozniak is not boring */ {
goto begin
}
这表示永远不会有名为boring_wozniak的容器(确实如此)。
ℹ️Steve Wozniak是一名发明家、电子工程师、程序员和与乔布斯共同创立Apple Inc.企业家。他是上世纪70年和80年代个人电脑的先行者,当然一点也不会boring!
在一个容器处于退出状态时我们可以执行如下命令进行删除,务必将下面的容器名替换为你自己的名称:
$ docker container rm jolly_fermat
还有,在第一章 Docker概览 的最后,我们执行如下命令使用官方 Nginx镜像来启动了一个容器:
$ docker container run -d --name nginx -test -p 8080 : 80 nginx
你可能还记得,这会下载镜像并运行,将主机上的8080端口与容器的80端口进行映射,并将其命名为nginx-test:
可以看出,运行docker image ls会显示我们已下载并运行了两个镜像。如下命令显示了我们有一个容器在运行中:
如下Terminal输出显示我的容器在运行命令时已运行了8分钟:
可以看出通过docker container run命令,我们使用了三个标记。其中一个是-d,为–detach的简写。如果没添加这一个标记,我们的容器会在前台执行,这表示Terminal会被冻结,直接按下Ctrl + C来传递退出指令。
我们可以通过如下命令再启动一个 Nginx 容器来查看实时的效果:
$ docker container run --name nginx -foreground -p 9090 : 80 nginx
运行docker container ls -a会显示我们有两个容器,其中一个已退出:
究竟发生了什么呢?当我们删除-d 参数时,Docker让我们直接连接了容器中的 Nginx 进程,意味着我们对该进程的stdin, stdout和stderr 都要可见。在使用Ctrl + C时,我们实际上是发送了一条终止Nginx 进程的指令。因其是保持容器运行的进程,在没有运行中进程的情况下容器立即退出。
ℹ️标准输入(stdin)是我们进程从终端用户读取信息的指针。标准输出(stdout)为进程写入普通信息的地方。标准错误(stderr)是进程写入错误消息的地方。
你会注意到我们在启动nginx-foreground容器时使用–name标记指定了另一个名称。
这是因为两个容器不能有相同的名称,因为Docker让我们既可以使用容器 ID 也可以使用容器名来与容器进行交互。这也是名称生成器函数存在的原因:在不想要自己命名时为容器分配一个随机名称-并确保不说Steve Wozniak是boring的(boring_wozniak)。
最后要提的一点是在我们启动nginx-foreground时,我们让Docker将9090端口映射到容器的80端口。这是因为我们不能将多个进程映射到宿主机的同一端口上,如果我们尝试以相同端口启动第二个容器时,会得到一个错误消息:
docker : Error response from daemon : driver failed programming external connectivity on endpoint nginx -foreground ( 01b9b16f92307699d2e8bf503222be52d5554e52cead7403fe95639e517b39da ) : Bind for 0.0.0.0 : 8080 failed : port is already allocated .
同时,因为我们是在前台运行的容器,还会从 Nginx 进程得到条未成功启动的错误:
ERRO [ 0000 ] error waiting for container : context canceled
但是你还会看到我们向容器的80端口映射 – 为什么没有报错呢?
在第一章 Docker概览 中我们讲解过,容器本身是独立的资源,这表示我们可以启动很多个容器并使用80端口来进行映射,它们永远不会与其它容器产生冲突。只有来通过宿主机向容器暴露的端口进行路由时才会出现问题。
我们先保持 Nginx 容器运行并进入到下一部分。
与容器交互
截至目前,我们的容器都是运行单个进程。Docker提供了一些工具让我们既可以fork其它进程也可以与它们进行交互。
attach
第一种方式是连接运行中的容器的运行中进程。我们的nginx-test容器还在运行, 因此可以运行如下命令来进行连接:
$ docker container attach nginx -test
打开浏览器并访问http://localhost:8080/,会在屏幕上打印出Nginx访问日志,效果和启动nginx-foreground容器时一样。按下 Ctrl + C 停止进程并将 Terminal 恢复为正常;但是和此前一样,我们会终止让容器得以运行的进程:
我可以通过运行如下命令来再次启动该容器:
$ docker container start nginx -test
这会以非连接状态再次启动容器,也就说容器在后台运行,因为这是容器最初启动的状态。访问http://localhost:8080/,可以再次看到 Nginx 的欢迎页面。
让我们再次连接该进程,但这次添加一个选项:
$ docker container attach --sig -proxy =false nginx -test
访问几次容器的 URL,然后按下Ctrl + C 来取消与 Nginx 进程的关联,但这次不是终止 Nginx 的进程,仅仅返回到Terminal,让容器保持取消关联的状态,通过运行docker container ls可进行查看:
exec
attach 命令对于想要连接容器中所运行的进程非常有用,但假如你需要进行更多的互动呢?
这时可以使用exec命令,这会在容器中生成另一个进行来进行交互。例如,要查看/etc/debian_version文件中的内容,可以运行如下命令:
我们可以通过运行如下命令来更进一步:
$ docker container exec -i -t nginx -test /bin /bash
这次我们启动一个bash进程并使用-i和-t标记来保持打开对容器的访问终端。-i标记是–interactive的简写,它告诉Docker保持stdin打开,这样我们可以向进程发送命令。-t标记是–tty的缩写并向会话分配一个伪终端(pseudo-TTY)。
ℹ️早期连接电脑的用户终端被称为teletypewriter(电传打字机)。虽然这些设备今天已不再使用,TTY被保留下来用于描述现代电脑的纯文本终端。
这表示你可以像远程终端会话那样与容器进行交互,类似SSH:
虽然这极其有用,因为我们可以像虚拟机器那样与容器进行交互,我并不推荐对容器进行任何修改,因为使用的是伪终端。这些修改很有可能不会持久并且在删除容器时就会丢失。我们会在第十三章 Docker工作流 中进行背后更深入的思考。
日志和进程信息
截至目前,我们分别通过连接容器进程或容器本身来查看信息。Docker提供了一些命令让我们可以无需使用attach或exec命令来查看容器的相关信息。
logs
logs命令见名知义,让我们可以与容器的stdout流进行交互,Docker在后台会对容器进行追踪。例如,要查看nginx-test容器stdout中写入的最后几条,你只需使用如下命令:
$ docker container logs --tail 5 nginx -test
该命令的输出如下所示:
可通过运行如下命令来实时查看日志:
$ docker container logs -f nginx -test
-f标记是–follow的简写。我们已可以通过运行如下命令来查看某个时间点之后的所有日志:
$ docker container logs --since 2019 -04 -11T17 : 17 nginx -test
该命令的输出如下:
你可能注意到以上输出的访问日志时间截为9:17,这远在17:17之前。这又是为什么呢?
日志命令显示的stdout时间戳是由Docker记录的,而非容器中的时间。可通过运行如下命令来进行查看:
$ date
$ docker container exec nginx -test date
输出如下所示:
这是是因为我本机使用的是中国标准时间(CST-China Standard Time),与容器存在8个小时的时间差。
所幸的是,为避免困扰 – 也许是增加了困扰,取决于你的视角 – 你可以在logs命令后添加-t参数:
$ docker container logs --since 2018 -08 -25T17 : 17 -t nginx -test
-t标记是–timestamp的简写,该选项前接Docker捕获输出的时间:
top
top 命令非常之简单,它列出你所指定的容器中运行的进程,使用如下:
$ docker container top nginx -test
该命令的输出如下所示:
$ docker container top nginx -test
PID USER TIME COMMAND
2638 root 0 : 00 nginx : master process nginx -g daemon off ;
2679 101 0 : 00 nginx : worker process
可以从Terminal输出中拷到,我们有两个运行的进程,都是 Nginx,这在预料之中。
stats
stats命令提供指定容器或者在未传递容器名或 ID 时运行中容器的实时信息:
$ docker container stats nginx -test
可以在如下Terminal输出中看到,返回了指定容器的CPU, RAM, NETWORK, DISK IO和PIDS信息:
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I /O BLOCK I /O PIDS
ff4d54839ec2 nginx -test 0.00 % 2.117MiB / 1.952GiB 0.11 % 1.79kB / 0B 9.12MB / 4.1kB 2
我们还可以传递-a标记,它是–all的简写,显示所有的容器,不论是运行还是没运行。例如,可使用如下命令:
$ docker container stats -a
你会收到类似如下的输出:
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I /O BLOCK I /O PIDS
ff4d54839ec2 nginx -test 0.00 % 2.117MiB / 1.952GiB 0.11 % 1.86kB / 0B 9.12MB / 4.1kB 2
f3f452413f76 nginx -foreground 0.00 % 0B / 0B 0.00 % 0B / 0B 0B / 0B 0
但从以上输出中可以看出如果容器不在运行,就不会使用任何资源,所以并不是很有价值,仅仅是向你提供运行过多少个容器以及所占用资源位置的视觉展示。
还需指出stats所展示的信息仅是实时的,Docker并不会记录资源占用并像 logs 命令那样事后展示。我们会在后续章节中讲解资源占用的长期存储。
资源限制
我们运行的上一条命令向展示了容器的资源占用情况,默认情况下启动的容器可在需要时消耗宿主机上的所有可用资源。我们可以为容器所能消耗的资源设一个上限,下面就来更新nginx-test容器所能使用的资源量。
通常我们在使用run命令启动容器时添加限制,例如,让 CPU 优先级减半并设置内存上限为128M,我们可以使用如下命令:
$ docker container run -d --name nginx -test --cpu -shares 512 --memory 128M -p 8080 : 80 nginx
但我们这里在启动容器nginx-test时并没进行资源限制,这表示我们要对已经运行的容器进行更新,我们可以使用update命令来进行实现。现在你可能会想只需用到如下命令即可:
$ docker container update --cpu -shares 512 --memory 128M nginx -test
但实际上运行如上命令会产生错误:
Error response from daemon : Cannot update container ff4d54839ec214bd50c38342e7e8c8a481dbdf6e01cf97e89e3ac4edd2ea4e05 : Memory limit should be smaller than already set memoryswap limit , update the memoryswap at the same time
那么现在所设置的memoryswap上限是多少呢?要想知道,我们可以使用inspect命令来显示所运行容器的所有配置数据,只需运行:
$ docker container inspect nginx -test
通过运行以上命令我们会得到很多的配置数据。在作者运行命令时,返回了一个199行的 JSON 数组。我们来使用 grep 命令仅过滤出包含单词memory的这些行:
$ docker container inspect nginx -test | grep -i memory
这时会返回如下配置数据:
"Memory" : 0 ,
"KernelMemory" : 0 ,
"MemoryReservation" : 0 ,
"MemorySwap" : 0 ,
"MemorySwappiness" : null ,
所有的设置都为0,那么128M又怎么会比0小呢?
在资源配置的上下文中,0实际上是默认值并表示不设限 – 注意每个数值后并没有跟 M。这意味着我们的更新命令应该这样写:
$ docker container update --cpu -shares 512 --memory 128M --memory -swap 256M nginx -test
ℹ️Paging是一种内核存储、获取或交换二级存储中的数据到主内存中使用的一种内存管理方案。这允许进程的使用超过可用物理内存的大小。
默认情况下,在你将–memory加入到run命令中时,Docker会将–memory-swap的大小设置为–memory两倍。如果此时再运行docker container stats nginx-test,就可以看到这些限制已设定:
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I /O BLOCK I /O PIDS
ff4d54839ec2 nginx -test 0.00 % 2.117MiB / 128MiB 1.65 % 2.28kB / 0B 9.12MB / 4.1kB 2
同样,再次运行docker container inspect nginx-test | grep -i memory会显示修改如下:
"Memory" : 134217728 ,
"KernelMemory" : 0 ,
"MemoryReservation" : 0 ,
"MemorySwap" : 268435456 ,
"MemorySwappiness" : null ,
小贴士:运行docker container inspect时值都以字节显示,而不是兆字节(MB)。
容器状态和其它命令
这一节的最后一部分,我们来看容器可处于的不同状态,以及docker container命令剩下还未讲解的一些命令。
运行docker container ls -a会在Terminal中得到类似如下的输出:
$ docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
bd9b5b2d88e8 nginx "nginx -g 'daemon of…" 7 seconds ago Up 6 seconds 0.0.0.0 : 8080 -> 80 /tcp nginx -test
920148a321fd nginx "nginx -g 'daemon of…" 27 seconds ago Exited ( 0 ) 25 seconds ago nginx -foreground
可以看出,我们有两个容器,其中一个的状态为Up另一个为Exited。在继续之前,让我们再启动5个容器。可运行如下命令来快速完成:
$ for i in { 1..5 } ; do docker container run -d --name nginx $ ( printf "$i" ) nginx ; done
这时再运行docker container ls -a,可看到名为nginx1到nginx5的5个新容器:
$ docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
edbbf5bb700e nginx "nginx -g 'daemon of…" 12 seconds ago Up 11 seconds 80 /tcp nginx5
1e5df62ca423 nginx "nginx -g 'daemon of…" 13 seconds ago Up 12 seconds 80 /tcp nginx4
e67c0e6dc1a2 nginx "nginx -g 'daemon of…" 14 seconds ago Up 13 seconds 80 /tcp nginx3
80f93970608b nginx "nginx -g 'daemon of…" 14 seconds ago Up 13 seconds 80 /tcp nginx2
b084e5c1cfe5 nginx "nginx -g 'daemon of…" 15 seconds ago Up 14 seconds 80 /tcp nginx1
bd9b5b2d88e8 nginx "nginx -g 'daemon of…" 2 minutes ago Up 2 minutes 0.0.0.0 : 8080 -> 80 /tcp nginx -test
920148a321fd nginx "nginx -g 'daemon of…" 2 minutes ago Exited ( 0 ) 2 minutes ago nginx -foreground
暂停和取消暂停
我们来暂停nginx1。只需运行如下命令来实现:
$ docker container pause nginx1
运行docker container ls会显示该容器的状态是Up,但同时也显示了Paused:
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
edbbf5bb700e nginx "nginx -g 'daemon of…" 3 minutes ago Up 2 minutes 80 /tcp nginx5
1e5df62ca423 nginx "nginx -g 'daemon of…" 3 minutes ago Up 3 minutes 80 /tcp nginx4
e67c0e6dc1a2 nginx "nginx -g 'daemon of…" 3 minutes ago Up 3 minutes 80 /tcp nginx3
80f93970608b nginx "nginx -g 'daemon of…" 3 minutes ago Up 3 minutes 80 /tcp nginx2
b084e5c1cfe5 nginx "nginx -g 'daemon of…" 3 minutes ago Up 3 minutes ( Paused ) 80 /tcp nginx1
bd9b5b2d88e8 nginx "nginx -g 'daemon of…" 5 minutes ago Up 5 minutes 0.0.0.0 : 8080 -> 80 /tcp nginx -test
注意我们无需使用-a标记来查看该容器的信息,因为其进程并未终止,而是使用cgroups冻结器(freezer)将其挂起了。通过cgroups冻结器,进程本身并不知道其被挂起,也就是说可以进行恢复的。
不难猜到,恢复暂停的容器要使用unpause命令,如下所示:
$ docker container unpause nginx1
这条命令在我们想要冻结容器状态时非常有用,例如,你有一个容器出现了故障,想要稍后做一些调查,但又不想要对其它运行中容器产生负面的影响。
停止、启动、重启和杀死容器
接下来,我们来学习stop, start, restart和kill命令。我们已经使用过start来恢复状态为Exited的容器。stop命令的效果和我们在前台运行容器使用Ctrl + C来取消连接时的效果一样。运行如下命令:
$ docker container stop nginx2
通过它会向进程发送请求进行终止,称为SIGTERM。如果在宽限期内进程没有自己终止就会发送杀死进程的信号,称为SIGKILL。这会马上终止进程,不会引发延时的任何程序额外的时间,比如用结果向磁盘提交数据库查询。
因为这可能会产生问题,Docker提供了一个覆盖默认10秒宽限期的选项,这借由-t标记,是–time的缩写。例如,运行如下命令,在需要发送杀死进程时,会在发送SIGKILL前等待多达60秒:
$ docker container stop -t 60 nginx3
我们已经看到过,start命令会将进程再次启动,但是与pause和unpause命令不同,这时该进程使用原行启动时使用的参数重新启动,而不是在上一个进程结束处进行启动:
$ docker container start nginx2 nginx3
restart命令是这两个命令的组合:它停止、然后启动你先传递ID 或名称的容器。同时像 stop 一样,可以传递一个-t 标记:
$ docker container restart -t 60 nginx4
最后,还可以通过运行kill命令来直接向容器发送SIGKILL命令:
$ docker container kill nginx5
删除容器
我们通过docker container ls -a命令来查看已运行过的容器。运行这个命令时,可以看到有两个容器的状态为Exited,其它的为运行中:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
edbbf5bb700e nginx "nginx -g 'daemon of…" 2 hours ago Exited ( 137 ) About a minute ago nginx5
1e5df62ca423 nginx "nginx -g 'daemon of…" 2 hours ago Up 2 minutes 80 /tcp nginx4
e67c0e6dc1a2 nginx "nginx -g 'daemon of…" 2 hours ago Up 7 minutes 80 /tcp nginx3
80f93970608b nginx "nginx -g 'daemon of…" 2 hours ago Up 7 minutes 80 /tcp nginx2
b084e5c1cfe5 nginx "nginx -g 'daemon of…" 2 hours ago Up 2 hours 80 /tcp nginx1
bd9b5b2d88e8 nginx "nginx -g 'daemon of…" 2 hours ago Up 2 hours 0.0.0.0 : 8080 -> 80 /tcp nginx -test
920148a321fd nginx "nginx -g 'daemon of…" 2 hours ago Exited ( 0 ) 2 hours ago nginx -foreground
要删除两个退出状态的容器,仅需运行prune命令:
执行这一命令时,会提示是否确定要执行删除,如下所示:
$ docker container prune
WARNING ! This will remove all stopped containers .
Are you sure you want to continue ? [ y /N ] y
Deleted Containers :
edbbf5bb700eb66dbdc557a63615ce5e0c7bb0acb1eeea19b923a214ed4ad4f2
920148a321fd6842856c9c16a9e7f02bf3f96677439df5375454640402853a73
Total reclaimed space : 2B
你可以使用rm来选择所要删除的容器,示例如下:
$ docker container rm nginx4
译者注: 如 nginx4在运行中需使用 rm -f
另一个选择是将 stop 和 rm 命令拼接在一起:
$ docker container stop nginx3 && docker container rm nginx3
但既然我们现在可以使用 prune 命令了,这可能会太麻烦了,尤其是在尝试删除容器且不关心进程终止是否优雅的情况下。
读者可以使用任意一种方式来删除剩下的容器。
其它命令
这里的最后一部分我们来看一些你在日常用Docker时可能不太会使用的命令。第一个是create。
create命令和run命令非常相似,只是它不会启动容器,而是准备并配置好一个容器:
$ docker container create --name nginx -test -p 8080 : 80 nginx
可以通过运行docker container ls -a来创建容器的状态,然后在再次查看状态之前使用docker container start nginx-test启动容器:
AlansMac : ~ alan $ docker container create --name nginx -test -p 8080 : 80 nginx
0e57d0fb06583d3796e5ea7281449d459ebf77cfbe0c736b86bb19a88e56237b
AlansMac : ~ alan $ docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
0e57d0fb0658 nginx "nginx -g 'daemon of…" 6 seconds ago Created nginx -test
AlansMac : ~ alan $ docker container start nginx -test
nginx -test
AlansMac : ~ alan $ docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
0e57d0fb0658 nginx "nginx -g 'daemon of…" 24 seconds ago Up 3 seconds 0.0.0.0 : 8080 -> 80 /tcp nginx -test
我们要快速查看的下一条命令是port命令,它会显示其端口以及与容器映射的端口
$ docker container port nginx -test
应该会返回如下结果:
我们早就知道了,因为这正是我们所配置的。同时在docker container ls的输出中也显示了这些端口。
最后我们要来快速看的一个命令是diff。该命令打印出所有的在容器启动后新增(A)或修改(C)过的文件列表,所以基本上这个列表是用于启动的原始镜像和当前文件在文件系统中的差别。
在运行该命令之前,我们使用exec命令在容器nginx-test中创建一个空白文件:
$ docker container exec nginx -test touch /tmp /testing
现在在/tmp下有了一个名为testing的文件,我们就可以使用如下命令查看原始镜像与运行中容器的不同了:
$ docker container diff nginx -test
这会返回一系列文件,从下面的列表可以看到testing文件,以及在启动 Nginx
This will return a list of files; as you can see from the following list, our testing file is there, along with the files that were created when nginx started:
C /tmp
A /tmp /testing
C /var
C /var /cache
C /var /cache /nginx
A /var /cache /nginx /client_temp
A /var /cache /nginx /fastcgi_temp
A /var /cache /nginx /proxy_temp
A /var /cache /nginx /scgi_temp
A /var /cache /nginx /uwsgi_temp
C /run
A /run /nginx . pid
需要说明的是一旦我们停止并删除该容器,这些文件都会丢失。在本章的下一节中,我们会看Docker的数据卷以及如何持久化存储数据。
还有如果你一直照着操作的话请使用自己喜欢的命令删除本节中所启动的容器。
Docker网络和数据卷
在结束本章之前,我们将学习一下Docker网络和使用默认驱动的Docker数据卷的基础知识。首先我们来看网络。
Docker网络
截至目前,我们都在一个单独的共享网络上启动的容器。虽然还没说到,这表示我们启动的这些容器无需使用宿机网络即可相互间进行通讯。
我们先不进行细节,通过一个示例来进行展示。这里运行一个双容器应用,第一个容器运行Redis,第二个就是我们应用了,它使用Redis容器来存储系统状态。
ℹ️Redis是一种内存数据结构存储,可用作数据库、缓存或消息代理(message broker)。它支持不同级别的磁盘持久化。
在启动应用之前,先来下载要用到的容器镜像,同时创建网络:
$ docker image pull redis : alpine
$ docker image pull russmckendrick /moby -counter
$ docker network create moby -counter
你应该可以看到类似下面的Terminal输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ docker image pull redis : alpine
alpine : Pulling from library /redis
bdf0201b3a05 : Pull complete
542e0c4f2f18 : Pull complete
cbf113c39f65 : Pull complete
09158274ea6c : Pull complete
ffc2a2e9a3a6 : Pull complete
bcdc222d2d8e : Pull complete
Digest : sha256 : ef67270b8bcb6b801a67d186ed16051520867b985e4f2f1233b510938dc55b22
Status : Downloaded newer image for redis : alpine
$ docker image pull russmckendrick /moby -counter
Using default tag : latest
latest : Pulling from russmckendrick /moby -counter
ff3a5c916c92 : Pull complete
0384617ecf25 : Pull complete
3e2743173da8 : Pull complete
40c2a5cd7772 : Pull complete
e00657f4abd2 : Pull complete
32312bfbca18 : Pull complete
Digest : sha256 : d0f51203130cb934a2910c2e0d577e68b7ab17962ce01918d37d7de9686553cc
Status : Downloaded newer image for russmckendrick /moby -counter : latest
$ docker network create moby -counter
b1a1b80693a44268d7f07bdb16e61935bd1480a3bfeddec2f3658ccd7f39bc2e
现在我们已拉取了镜像并创建了网络,可以启动容器了,先从Redis容器开始:
$ docker container run -d --name redis --network moby -counter redis : alpine
可以看到,我们使用了–network标记来定义启动的容器所处的网络。既然已经启动了Redis容器,我们可以通过运行如下命令来启动应用容器:
$ docker container run -d --name moby -counter --network moby -counter -p 8080 : 80 russmckendrick /moby -counter
同样我们在moby-counter网络中启动了该容器,这次我们将8080端口与容器中的80端口进行了映射。注意我们不必担心要为Redis容器暴露任何端口。那是因为Redis镜像有一些默认设置来身外暴露默认端口,6379。通过运行docker container ls即可查看:
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
54e2e92d8a46 russmckendrick /moby -counter "node index.js" 2 minutes ago Up 2 minutes 0.0.0.0 : 8080 -> 80 /tcp moby -counter
498b3ca559c8 redis : alpine "docker-entrypoint.s…" 5 minutes ago Up 5 minutes 6379 /tcp redis
现在剩下的就是访问应用了,打开浏览器并访问http://localhost:8080/。应该会出现一个空白页面,其中有条消息Click to add logos:
在页面上任意位置进行点击会添加Docker的logo,所以随意点吧:
那么发生了什么呢?moby-counter中的应用在向redis容器做出连接,并使用该服务来存储点击时需要放置 logo 的屏幕上坐标。
moby-counter应用是如何连接redis容器的呢?在server.js文件中设置了如下默认值:
var port = opts . redis_port | | process . env . USE_REDIS_PORT | | 6379
var host = opts . redis_host | | process . env . USE_REDIS_HOST | | 'redis'
这表示moby-counter查看一个主机名为redis并连接该主机的6379端口。我们试着使用exec来从moby-counter应用向redis容器发出 ping 请求并查看结果:
$ docker container exec moby -counter ping -c 3 redis
应该会类似如下的输出:
$ docker container exec moby -counter ping -c 3 redis
PING redis ( 172.18.0.2 ) : 56 data bytes
64 bytes from 172.18.0.2 : seq =0 ttl =64 time =0.089 ms
64 bytes from 172.18.0.2 : seq =1 ttl =64 time =0.122 ms
64 bytes from 172.18.0.2 : seq =2 ttl =64 time =0.122 ms
--- redis ping statistics ---
3 packets transmitted , 3 packets received , 0 % packet loss
round -trip min /avg /max = 0.089 /0.111 /0.122 ms
可以看到,moby-counter将redis解析到了该容器的 IP 地址,172.18.0.2。你可能会想这个应用的主文件包含一个对redis容器的入口,我们使用如下命令来进行查看:
$ docker container exec moby -counter cat /etc /hosts
这会返回/etc/hosts中的内容,我这里的内容如下:
127.0.0.1 localhost
: : 1 localhost ip6 -localhost ip6 -loopback
fe00 : : 0 ip6 -localnet
ff00 : : 0 ip6 -mcastprefix
ff02 : : 1 ip6 -allnodes
ff02 : : 2 ip6 -allrouters
172.18.0.3 54e2e92d8a46
除了最后的入口,它实际上是本地容器解析到主机名的 IP 地址,54e2e92d8a46是容器的 ID,没有任何redis入口的迹象。下面我们通过运行如下命令来查看/etc/resolv.conf:
$ docker container exec moby -counter cat /etc /resolv . conf
这返回了我们要查看的内容,可以看到我们使用的是本地nameserver:
nameserver 127.0.0.11
options ndots : 0
让我们用如下命令对redis使用127.0.0.11来执行一个 DNS 查询:
$ docker container exec moby -counter nslookup redis 127.0.0.11
返回了redis容器的IP地址:
Server : 127.0.0.11
Address 1 : 127.0.0.11
Name : redis
Address 1 : 172.18.0.2 redis . moby -counter
我们再创建一个网络并启动另一个应用容器:
$ docker network create moby -counter2
$ docker run -itd --name moby -counter2 --network moby -counter2 -p 9090 : 80 russmckendrick /moby -counter
此时第二应用容器也已启动并运行,我们来通过它向redis容器发进 ping 请求:
$ docker container exec moby -counter2 ping -c 3 redis
我这里得到了如下的错误:
ping : bad address 'redis'
我们来查看resolv.conf文件,看是否使用了相同的nameserver,如下所示:
$ docker container exec moby -counter2 cat /etc /resolv . conf
可以从输出中看到,确实已使用了该nameserver:
nameserver 127.0.0.11
options ndots : 0
因为我们在与运行redis容器不同的另一个网络上启动了moby-counter2容器,我们无法解析到该容器的主机名,因此返回了一个bad address报错:
$ docker container exec moby -counter2 nslookup redis 127.0.0.11
Server : 127.0.0.11
nslookup : can 't resolve ' redis ': Name does not resolve
Address 1 : 127.0.0.11
我们再来在第二个网络中启动另一个Redis服务器,前面已经讲到两个容器的名称不能相同,我们将其命名为redis2。
我们的应用配置连接到解析为redis的容器,这是否表示我们要对应用容器做出修改呢?不需要,但 Docker 会替你解决。
虽然不能使用桢名称运行两个容器,这个我们已经知道了,第二个网络的运行与第一个是完全分离的,这意味着我们仍可以使用DNS名称 redis。实现这点,我们需要像如下这样添加一个– network-alias 标记:
$ docker container run -d --name redis2 --network moby -counter2 --network -alias redis redis : alpine
可以看到,我们将容器命名为redis2,将设置–network-alias为 redis,这表示我们在执行查询时,会看到正确的 IP 地址得到返回:
$ docker container exec moby -counter2 nslookup redis 127.0.0.1
Server : 127.0.0.1
Address 1 : 127.0.0.1 localhost
Name : redis
Address 1 : 172.19.0.3 redis2 . moby -counter2
可以看到,redis实际上是redis2.moby-counter2的一个别名,然后被解析到172.19.0.3。
现在我们本地Docker主机上应该有两个在不同网络上同时运行的两个应用,可分别通过http://localhost:8080/和http://localhost:9090/进行访问。运行docker network ls会显示 Docker 主机上配置的所有网络,含默认网络:
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
174f8b8df3dd bridge bridge local
2ddc43a490c6 host host local
b1a1b80693a4 moby -counter bridge local
cb3808d1450e moby -counter2 bridge local
240ad1cfc29c none null local
你可以通过运行如下的inspect命令来查看网络配置的更多信息:
$ docker network inspect moby -counter
运行以上命令会返回如下的JSON数组:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
[
{
"Name" : "moby-counter" ,
"Id" : "b1a1b80693a44268d7f07bdb16e61935bd1480a3bfeddec2f3658ccd7f39bc2e" ,
"Created" : "2019-04-12T15:40:10.774585Z" ,
"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" : {
"498b3ca559c8ae38df20994b2de1cbfd725f67f90e44de493de66e58e9561b04" : {
"Name" : "redis" ,
"EndpointID" : "b3e3c553cdb93e114cb664811bac8db89142d273637e8a33ce13ef2b9e1f0452" ,
"MacAddress" : "02:42:ac:12:00:02" ,
"IPv4Address" : "172.18.0.2/16" ,
"IPv6Address" : ""
} ,
"54e2e92d8a461c1fd24e4a143d59f95480d3f3b3873f7fe100926d7e769e3c00" : {
"Name" : "moby-counter" ,
"EndpointID" : "4f5965362a58707a5f9a09d8c4e1c1d9191d3f2e55fab3cab0c82809e3d4bb2b" ,
"MacAddress" : "02:42:ac:12:00:03" ,
"IPv4Address" : "172.18.0.3/16" ,
"IPv6Address" : ""
}
} ,
"Options" : { } ,
"Labels" : { }
}
]
可以看到,它包含在IPAM部分中使用的网络地址信息,以及运行在网络上的两个容器的详细信息。
ℹ️IP地址管理(IPAM – IP address management)是在网络中规划、追踪和管理 IP地址的方式。IPAM包含 DNS 和 DHCP 服务,所以每种服务都会被告知另一种服务中的变化。例如,DHCP向container2分配地址。然后DNS服务会被更新,在每次对container2进行查询时都会返回DHCP所分配的 IP地址。
在进入下一部分之前,我们应该删除其中一个应用及关联网络。实现这一点,运行如下命令:
$ docker container stop moby -counter2 redis2
$ docker container prune
$ docker network prune
这会删除相应的容器和网络,如下所示:
$ docker container stop moby -counter2 redis2
moby -counter2
redis2
$ docker container prune
WARNING ! This will remove all stopped containers .
Are you sure you want to continue ? [ y /N ] y
Deleted Containers :
c3347c17d1d1f56de48ced419c0c43aba46a47c489d8b62483e7ae8028d3d451
db627e666ae136cf1b3e521fb1c7d01034aa21561c33e4e4e8e26c8f22634126
Total reclaimed space : 0B
$ docker network prune
WARNING ! This will remove all networks not used by at least one container .
Are you sure you want to continue ? [ y /N ] y
Deleted Networks :
moby -counter2
在这一部分的开头我们讲到,这只是默认的网络驱动,也就是说我们仅限于在单个Docker主机上使用这些网络。在后续的章节中,我们会来看如何在多个主机甚至时多个供应商间扩展Docker的网络的。
Docker 数据卷
如果你按照前面一节的网络示例进行操作的话,现在会有两个容器正在运行中,如下所示:
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
54e2e92d8a46 russmckendrick /moby -counter "node index.js" 18 hours ago Up About an hour 0.0.0.0 : 8080 -> 80 /tcp moby -counter
498b3ca559c8 redis : alpine "docker-entrypoint.s…" 18 hours ago Up About an hour 6379 /tcp redis
在浏览器中访问该应用(http://localhost:8080/)时,你可能会看到屏幕上已有经一些Docker的 Logo 了。我们来停止并删除Redis容器看看会发生什么。执行如下命令来完成操作:
$ docker container stop redis
$ docker container rm redis
如果你保持浏览器打开,会注意到Docker图标淡化到背景中并且在屏幕中间有一个动画加载器。这显示应用在等待与Redis容器两次建立连接:
使用如下命令再次启动Redis容器:
$ docker container run -d --name redis --network moby -counter redis : alpine
这会重新建立连接,但是在你开始与应用进行交互时,此前的图标会消失,又恢复成白板一块。快速的在屏幕上添加一些 logo,这次像我一样使用一个不同的图案:
一旦添加好了图案,我们来通过如下命令再次删除Redis容器:
$ docker container stop redis
$ docker container rm redis
本章前面已讨论到,预期中会丢失容器中的数据。但是,因为我们使用的是官方的Redis镜像,我们其实并没丢失任何数据。
我们使用的官方Redis镜像的Dockerfile像下面这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
FROM alpine : 3.9
# add our user and group first to make sure their IDs get assigned consistently, regardless of whatever dependencies get added
RUN addgroup -S redis && adduser -S -G redis redis
RUN apk add --no-cache \
# grab su-exec for easy step-down from root
'su-exec>=0.2' \
# add tzdata for https://github.com/docker-library/redis/issues/138
tzdata
ENV REDIS_VERSION 5.0.4
ENV REDIS_DOWNLOAD_URL http://download.redis.io/releases/redis-5.0.4.tar.gz
ENV REDIS_DOWNLOAD_SHA 3ce9ceff5a23f60913e1573f6dfcd4aa53b42d4a2789e28fa53ec2bd28c987dd
# for redis-sentinel see: http://redis.io/topics/sentinel
RUN set -ex; \
\
apk add --no -cache --virtual . build -deps \
coreutils \
gcc \
linux -headers \
make \
musl -dev \
; \
\
wget -O redis . tar . gz "$REDIS_DOWNLOAD_URL" ; \
echo "$REDIS_DOWNLOAD_SHA *redis.tar.gz" | sha256sum -c -; \
mkdir -p /usr /src /redis ; \
tar -xzf redis . tar . gz -C /usr /src /redis --strip -components =1 ; \
rm redis . tar . gz ; \
\
# disable Redis protected mode [1] as it is unnecessary in context of Docker
# (ports are not automatically exposed when running inside Docker, but rather explicitly by specifying -p / -P)
# [1]: https://github.com/antirez/redis/commit/edd4d555df57dc84265fdfb4ef59a4678832f6da
grep -q '^#define CONFIG_DEFAULT_PROTECTED_MODE 1$' /usr /src /redis /src /server . h ; \
sed -ri 's!^(#define CONFIG_DEFAULT_PROTECTED_MODE) 1$!\1 0!' /usr /src /redis /src /server . h ; \
grep -q '^#define CONFIG_DEFAULT_PROTECTED_MODE 0$' /usr /src /redis /src /server . h ; \
# for future reference, we modify this directly in the source instead of just supplying a default configuration flag because apparently "if you specify any argument to redis-server, [it assumes] you are going to specify everything"
# see also https://github.com/docker-library/redis/issues/4#issuecomment-50780840
# (more exactly, this makes sure the default behavior of "save on SIGTERM" stays functional by default)
\
make -C /usr /src /redis -j "$(nproc)" ; \
make -C /usr /src /redis install ; \
\
rm -r /usr /src /redis ; \
\
runDeps ="$( \
scanelf --needed --nobanner --format '%n#p' --recursive /usr/local \
| tr ',' '\n' \
| sort -u \
| awk 'system(" [ -e /usr /local /lib /" $1 " ] ") == 0 { next } { print " so : " $1 }' \
)" ; \
apk add --virtual . redis -rundeps $ runDeps ; \
apk del . build -deps ; \
\
redis -server --version
RUN mkdir /data && chown redis : redis /data
VOLUME /data
WORKDIR /data
COPY docker -entrypoint . sh /usr /local /bin /
ENTRYPOINT [ "docker-entrypoint.sh" ]
EXPOSE 6379
CMD [ "redis-server" ]
仔细看下在文件的结束处,声明了VOLUME和WORKDIR指令,这表示在我们的容器启动时,Docker实际上创建了一个数据卷,然后在数据卷中运行redis-server。
我们可以通过运行如下命令来进行查看:
这应该至少会显示两个数据卷,如下所示:
$ docker volume ls
DRIVER VOLUME NAME
local 469286787ea9245bf72f45e8b0a76febfb05e4bf2c87f69bc733efdb9b5612e9
local d51662e4a1b9ec0059abddf7df49a8e96d5d2ec601ce5d83e0a03b4ef92d371a
可以看到,数据卷的名称一点不友好,事实上它是数据卷的唯一 ID。那么我们要如何在启动Redis容器时使用数据卷呢?
我们从Dockerfile中知道了数据卷在容器中的/data处挂载,因此我们只需告诉Docker要使用的是哪个数据卷,以及在运行时在何处挂载。
要实现这点,运行如下命令,记得将磁盘卷的ID修改为你自己的:
$ docker container run -d --name redis -v d51662e4a1b9ec0059abddf7df49a8e96d5d2ec601ce5d83e0a03b4ef92d371a : /data --network moby -counter redis : alpine
如果在启动Redis容器后你的应用页面还在尝试连接Redis容器,那么可能需要刷新浏览器,如若还不行,通过docker container restart moby-counter命令来重启应用容器,然后再次刷新浏览器应该就可以了。
你们通过运行如下命令来连接容器并列出/data中的文件进而查看数据卷中的内容:
$ docker container exec redis ls -lhat /data
这将会返回类似如下内容:
total 12
drwxr -xr -x 1 root root 4.0K Apr 13 11 : 01 . .
drwxr -xr -x 2 redis redis 4.0K Apr 13 09 : 59 .
-rw -r --r -- 1 redis redis 260 Apr 13 09 : 59 dump . rdb
你也可以删除运行中的容器并重新启动,但这次使用第二个磁盘卷的 ID。可以通过浏览器中查看应用,原来创建的不同图案保持完整。
最后,你可以将数据卷重写为你自己的。要创建数据卷,我们需要使用volume命令:
$ docker volume create redis_data
一旦完成了创建,我们将可以通过如下命令使用redis_data数据卷来存储我们的Redis,如果已经运行,可以先删除Redis容器:
$ docker container run -d --name redis -v redis_data : /data --network moby -counter redis : alpine
我们可以在需要时使用该数据卷,以下显示了创建的数据卷,与接着会删除并最终重新连接到新容器的容器连接,
$ docker volume create redis_data
redis_data
$ docker container run -d --name redis -v redis_data : /data --network moby -counter redis : alpine
490dd86d8479612f16a051ff64071a26cdb37cd91844abf7fc6a3bf13a5d2df7
$ docker container restart moby -counter
moby -counter
$ docker container rm -f redis
redis
$ docker container run -d --name redis -v redis_data : /data --network moby -counter redis : alpine
966135e3afd741abcce00d3b699b1c058e28c74c9cc5b6f3c9e0cbc1e2b382c8
类似network命令,我们可以通过inspect命令来查看数据卷的更多信息,如下所示:
$ docker volume inspect redis_data
上面的代码可获得类似下面的输出:
[
{
"CreatedAt" : "2019-04-13T11:15:13Z" ,
"Driver" : "local" ,
"Labels" : null ,
"Mountpoint" : "/var/lib/docker/volumes/redis_data/_data" ,
"Name" : "redis_data" ,
"Options" : null ,
"Scope" : "local"
}
]
可以看到在使用本地驱动时数据卷并没有什么内容,有意思的是数据在Docker主机上存储的位置为/var/lib/docker/volumes/redis_data/_data。如果你使用的是 Mac 或 Windows 上的 Docker,该路径在Docker宿主虚拟主机上,而非本地机器上,也就是说你不能直接访问数据卷中的数据。
但不必担心,我们在后面的章节中会来看Docker的数据卷以及如果与数据进行交互。现在我们进行清理。首先删除这两个容器及其网络:
$ docker container stop redis moby -counter
$ docker container prune
$ docker network prune
然后我们可以通过运行如下命令来删除这些数据卷:
你在Terminal中应该会看到类似下面的输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$ docker container stop redis moby -counter
redis
moby -counter
$ docker container prune
WARNING ! This will remove all stopped containers .
Are you sure you want to continue ? [ y /N ] y
Deleted Containers :
966135e3afd741abcce00d3b699b1c058e28c74c9cc5b6f3c9e0cbc1e2b382c8
54e2e92d8a461c1fd24e4a143d59f95480d3f3b3873f7fe100926d7e769e3c00
Total reclaimed space : 0B
$ docker network prune
WARNING ! This will remove all networks not used by at least one container .
Are you sure you want to continue ? [ y /N ] y
Deleted Networks :
moby -counter
$ docker volume prune
WARNING ! This will remove all local volumes not used by at least one container .
Are you sure you want to continue ? [ y /N ] y
Deleted Volumes :
ad56987f9665f8bd13d81c80a72a3dedd46ef6f83605704a3d0374cfdd1adc7a
9f20e3647ae1eaff43401a4b13eb05674535eb1d5bfc1fe134f74f0e2ee607a5
d51662e4a1b9ec0059abddf7df49a8e96d5d2ec601ce5d83e0a03b4ef92d371a
redis_data
469286787ea9245bf72f45e8b0a76febfb05e4bf2c87f69bc733efdb9b5612e9
Total reclaimed space : 873B
我们现在已回到空白状态,可以进行下一章的学习了。
总结
本章中,我们学习了如何使用Docker命令行客户端来管理容器及在分离的Docker网络中启动多容器应用。我们还讨论了如何使用Docker数据卷在文件系统上持久化存储数据。至此,在本章及前面的章节中,我们了详细的讲解了在下面部分中会使用的大多数据命令:
$ docker container [ command ]
$ docker network [ command ]
$ docker volume [ command ]
$ docker image [ command ]
既然我们已经讲解了本地使用Docker的4个主要领域,要开始学习如何使用Docker Compose来创建更为复杂的应用了。
下一章中,我们将来看另一个核心Docker工具,名为Docker Compose。
课后问题
在docker container ls命令后要添加哪个标记来查看运行中和已停止的所有的容器?
是非题:-p 8080:80标记会将容器中的80端口与主机上的8080端口进行映射。
说明使用Ctrl + C来退出所连接的容器,与使用带有–sig-proxy=false的连接命令之间的区别。
是非题:exec将你与运行中的进程相连接。
在另一个网络中有以相同DNS名称运行的容器时,使用哪个标记添加容器的别名来响应DNS请求?
使用哪个命令来查看Docker数据卷的细节信息?
扩展阅读
通过如下链接你可以找到更多有关本章所讨论过的话题的信息:
名称生成器代码:https://github.com/moby/moby/blob/master/pkg/namesgenerator/names-generator.go
cgroups冻结器函数:https://www.kernel.org/doc/Documentation/cgroup-v1/freezer-subsystem.txt
Redis: https://redis.io/