在本文中,我们将迈出使用Docker的第一步,学习第一个Docker容器。还会介绍如何与Docker进行交互的基本知识。
首先,我们会查看Docker是否能正常工作,然后学习基本的Docker的工作流:创建并管理容器。我们将浏览容器的典型生命周期:从创建、管理到停止,直到最终删除。
第一步,查看docker
程序是否存在,功能是否正常,如代码清单3-1所示。
代码清单3-1 查看docker
程序是否正常工作
$ sudo docker info
Containers: 1
Images: 8
Storage Driver: aufs
Root Dir: /var/lib/docker/aufs
Backing Filesystem: extfs
Dirs: 10
Execution Driver: native-0.2
Kernel Version: 3.13.0-43-generic
Operating System: Ubuntu 14.04.2 LTS
CPUs: 1
Total Memory: 994 MiB
Name: riemanna
ID: DOIT:XN5S:WNYP:WP7Q:BEUP:EBBL:KGIX:GO3V:NDR7:YW6E:VFXT:FXHM
WARNING: No swap limit support
在这里我们调用了docker
可执行程序的info
命令,该命令会返回所有容器和镜像(镜像即是Docker用来构建容器的“构建块”)的数量、Docker使用的执行驱动和存储驱动(execution and storage driver),以及Docker的基本配置。
在前面几章已经介绍过,Docker是基于客户端-服务器构架的。它有一个docker
程序,既能作为客户端,也可以作为服务器端。作为客户端时,docker
程序向Docker守护进程发送请求(如请求返回守护进程自身的信息),然后再对返回的请求结果进行处理。
现在,让我们尝试启动第一个Docker容器。我们可以使用docker run
命令创建容器,如代码清单3-2所示。docker run
命令提供了Docker容器的创建到启动的功能,在本书中我们也会使用该命令来创建新容器。
代码清单3-2 运行我们的第一个容器
$ sudo docker run -i -t ubuntu /bin/bash
Unable to find image 'ubuntu' locally
ubuntu:latest: The image you are pulling has been verified
511136ea3c5a: Pull complete
d497ad3926c8: Pull complete
ccb62158e970: Pull complete
e791be0477f2: Pull complete
3680052c0f5c: Pull complete
22093c35d77b: Pull complete
5506de2b643b: Pull complete
Status: Downloaded newer image for ubuntu:latest
root@fcd78e1a3569:/#
提示
官方文档列出了完整的Docker命令列表,也可以使用docker help获取这些命令。此外,还可以使用Docker的man页(即执行man docker-run)。
代码清单3-3所示的命令的输出结果非常丰富,下面来逐条解析。
代码清单3-3 docker run
命令
$ sudo docker run -i -t ubuntu /bin/bash
首先,我们告诉Docker执行docker run
命令,并指定了-i
和-t
两个命令行参数。-i
标志保证容器中STDIN
是开启的,尽管我们并没有附着到容器中。持久的标准输入是交互式shell的“半边天”,-t
标志则是另外“半边天”,它告诉Docker为要创建的容器分配一个伪tty终端。这样,新创建的容器才能提供一个交互式shell。若要在命令行下创建一个我们能与之进行交互的容器,而不是一个运行后台服务的容器,则这两个参数已经是最基本的参数了。
提示
官方文档上列出了docker run命令的所有标志,此外还可以用命令docker help run查看这些标志。或者,也可以用Docker的man页(也就是执行man docker-run命令)。
接下来,我们告诉Docker基于什么镜像来创建容器,示例中使用的是ubuntu
镜像。ubuntu
镜像是一个常备镜像,也可以称为“基础”(base)镜像,它由Docker公司提供,保存在Docker Hub****Registry上。可以以ubuntu
基础镜像(以及类似的fedora
、debian
、centos
等镜像)为基础,在选择的操作系统上构建自己的镜像。到目前为止,我们基于此基础镜像启动了一个容器,并且没有对容器增加任何东西。
提示
我们将在第4章对镜像做更详细的介绍,包括如何构建我们自己的镜像。
那么,在这一切的背后又都发生了什么呢?首先Docker会检查本地是否存在ubuntu
镜像,如果本地还没有该镜像的话,那么Docker就会连接官方维护的Docker Hub Registry,查看Docker Hub中是否有该镜像。Docker一旦找到该镜像,就会下载该镜像并将其保存到本地宿主机中。
随后,Docker在文件系统内部用这个镜像创建了一个新容器。该容器拥有自己的网络、IP地址,以及一个用来和宿主机进行通信的桥接网络接口。最后,我们告诉Docker在新容器中要运行什么命令,在本例中我们在容器中运行/bin/bash
命令启动了一个Bash shell。
当容器创建完毕之后,Docker就会执行容器中的/bin/bash
命令,这时就可以看到容器内的shell了,就像代码清单3-4所示。
代码清单3-4 第一个容器的shell
root@f7cbdac22a02:/#
现在,我们已经以root
用户登录到了新容器中,容器的ID f7cbdac22a02``,乍看起来有些令人迷惑的字符串
。这是一个完整的Ubuntu系统,可以用它来做任何事情。下面就来研究一下这个容器。首先,我们可以获取该容器的主机名,如代码清单3-5所示。
代码清单3-5 检查容器的主机名
root@f7cbdac22a02:/# hostname
f7cbdac22a02
可以看到,容器的主机名就是该容器的ID。再来看看/etc/hosts
文件,如代码清单3-6所示。
代码清单3-6 检查容器的/etc/hosts文件
root@f7cbdac22a02:/# cat /etc/hosts
172.17.0.4 f7cbdac22a02
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
Docker已在hosts
文件中为该容器的IP地址添加了一条主机配置项。再来看看容器的网络配置情况,如代码清单3-7所示。
代码清单3-7 检查容器的接口
root@f7cbdac22a02:/# ip a
1: lo: mtu 1500 qdisc noqueue state
UNKNOWN group default
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
899: eth0: mtu 1500 qdisc pfifo_fast
state UP group default qlen 1000
link/ether 16:50:3a:b6:f2:cc brd ff:ff:ff:ff:ff:ff
inet 172.17.0.4/16 scope global eth0
inet6 fe80::1450:3aff:feb6:f2cc/64 scope link
valid_lft forever preferred_lft forever
可以看到,这里有lo
的环回接口,还有IP为172.17.0.4
的标准eth0
网络接口,和普通宿主机是完全一样的。我们还可以查看容器中运行的进程,如代码清单3-8所示。
代码清单3-8 检查容器的进程
root@f7cbdac22a02:/# ps -aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 18156 1936 ? Ss May30 0:00 /bin/bash
root 21 0.0 0.0 15568 1100 ? R+ 02:38 0:00 ps -aux
接下来要干些什么呢?安装一个软件包怎么样?如代码清单3-9所示。
代码清单3-9 在第一个容器中安装软件包
root@f7cbdac22a02:/# apt-get update && apt-get install vim
通过上述命令,就在容器中安装了Vim软件。
用户可以继续在容器中做任何自己想做的事情。当所有工作都结束时,输入exit
,就可以返回到Ubuntu宿主机的命令行提示符了。
这个容器现在怎样了?容器现在已经停止运行了!只有在指定的/bin/bash
命令处于运行状态的时候,我们的容器也才会相应地处于运行状态。一旦退出容器,/bin/bash
命令也就结束了,这时容器也随之停止了运行。
但容器仍然是存在的,可以用docker ps -a
命令查看当前系统中容器的列表,如代码清单3-10所示。
代码清单3-10 列出Docker容器
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1cd57c2cdf7f ubuntu:14.04 "/bin/bash" A minute Exited
gray_cat
默认情况下,当执行docker ps
命令时,只能看到正在运行的容器。如果指定-a
标志的话,那么docker ps
命令会列出所有容器,包括正在运行的和已经停止的。
提示
也可以为docker ps命令指定-l标志,列出最后一个运行的容器,无论其正在运行还是已经停止。也可以通过–format标志,进一步控制显示哪些信息,以及如何显示这些信息。
从该命令的输出结果中我们可以看到关于这个容器的很多有用信息:ID、用于创建该容器的镜像、容器最后执行的命令、创建时间以及容器的退出状态(在上面的例子中,退出状态是0
,因为容器是通过正常的exit
命令退出的)。我们还可以看到,每个容器都有一个名称。
注意
有3种方式可以唯一指代容器:短UUID(如f7cbdac22a02)、长UUID(如f7cbdac 22a02e03c9438c729345e54db9d20cfa2ac1fc3494b6eb60872e74778)或者名称(如gray_cat)。
Docker会为我们创建的每一个容器自动生成一个随机的名称。例如,上面我们刚刚创建的容器就被命名为gray_cat
。如果想为容器指定一个名称,而不是使用自动生成的名称,则可以用--name
标志来实现,如代码清单3-11所示。
代码清单3-11 给容器命名
$ sudo docker run --name bob_the_container -i -t ubuntu /bin/bash
root@aa3f365f0f4e:/# exit
上述命令将会创建一个名为bob_the_container
的容器。一个合法的容器名称只能包含以下字符:小写字母a~z、大写字母A~Z、数字0~9、下划线、圆点、横线(如果用正则表达式来表示这些符号,就是[a-zA-Z0-9``_``.-]
)。
在很多Docker命令中,都可以用容器的名称来替代容器ID,后面我们将会看到。容器名称有助于分辨容器,当构建容器和应用程序之间的逻辑连接时,容器的名称也有助于从逻辑上理解连接关系。具体的名称(如web
、db
)比容器ID和随机容器名好记多了。我推荐大家都使用容器名称,以更加方便地管理容器。
注意
我们将会在第5章详细介绍如何连接到Docker容器。
容器的命名必须是唯一的。如果试图创建两个名称相同的容器,则命令将会失败。如果要使用的容器名称已经存在,可以先用docker rm
命令删除已有的同名容器后,再来创建新的容器。
bob_the_container
容器已经停止了,接下来我们能对它做些什么呢?如果愿意,我们可以用下面的命令重新启动一个已经停止的容器,如代码清单3-12所示。
代码清单3-12 启动已经停止运行的容器
$ sudo docker start bob_the_container
除了容器名称,也可以用容器ID来指定容器,如代码清单3-13所示。
代码清单3-13 通过ID启动已经停止运行的容器
$ sudo docker start aa3f365f0f4e
提示
也可以使用docker restart命令来重新启动一个容器。
这时运行不带-a
标志的docker ps
命令,就应该看到我们的容器已经开始运行了。
注意
类似地,Docker也提供了docker create命令来创建一个容器,但是并不运行它。这让我们可以在自己的容器工作流中对其进行细粒度的控制。
Docker容器重新启动的时候,会沿用docker run
命令时指定的参数来运行,因此我们的容器重新启动后会运行一个交互式会话shell。此外,也可以用docker attach
命令,重新附着到该容器的会话上,如代码清单3-14所示。
代码清单3-14 附着到正在运行的容器
$ sudo docker attach bob_the_container
也可以使用容器ID,重新附着到容器的会话上,如代码清单3-15所示。
代码清单3-15 通过ID附着到正在运行的容器
$ sudo docker attach aa3f365f0f4e
现在,又重新回到了容器的Bash提示符,如代码清单3-16所示。
代码清单3-16 重新附着到容器的会话
root@aa3f365f0f4e:/_#_
提示
可能需要按下回车键才能进入该会话。
如果退出容器的shell,容器会再次停止运行。
除了这些交互式运行的容器(interactive container),也可以创建长期运行的容器。守护式容器(daemonized container)没有交互式会话,非常适合运行应用程序和服务。大多数时候我们都需要以守护式来运行我们的容器。下面就来启动一个守护式容器,如代码清单3-17所示。
代码清单3-17 创建长期运行的容器
$ sudo docker run --name daemon_dave -d ubuntu /bin/sh -c "while
true; do echo hello world; sleep 1; done"
1333bb1a66af402138485fe44a335b382c09a887aa9f95cb9725e309ce5b7db3
我们在上面的docker run
命令使用了-d
参数,因此Docker会将容器放到后台运行。
我们还在容器要运行的命令里使用了一个while
循环,该循环会一直打印hello world
,直到容器或其进程停止运行。
通过组合使用上面的这些参数,你会发现docker run
命令并没有像上一个容器一样将主机的控制台附着到新的shell会话上,而是仅仅返回了一个容器ID而已,我们还是在主机的命令行之中。如果执行docker ps
命令,可以看到一个正在运行的容器,如代码清单3-18所示。
代码清单3-18 查看正在运行的daemon_dave
容器
CONTAINER ID IMAGE COMMAND CREATED
STATUS PORTS NAMES
1333bb1a66af ubuntu:14.04 /bin/sh -c 'while tr 32 secs ago Up 27
daemon_dave
现在我们已经有了一个在后台运行while
循环的守护型容器。为了探究该容器内部都在干些什么,可以用docker logs
命令来获取容器的日志,如代码清单3-19所示。
代码清单3-19 获取守护式容器的日志
$ sudo docker logs daemon_dave
hello world
hello world
hello world
hello world
hello world
hello world
hello world
. . .
这里,我们可以看到while
循环正在向日志里打印hello world
。Docker会输出最后几条日志项并返回。我们也可以在命令后使用-f
参数来监控Docker的日志,这与tail -f
命令非常相似,如代码清单3-20所示。
代码清单3-20 跟踪守护式容器的日志
$ sudo docker logs -f daemon_dave
hello world
hello world
hello world
hello world
hello world
hello world
hello world
. . .
提示
可以通过Ctr+C退出日志跟踪。
我们也可以跟踪容器日志的某一片段,和之前类似,只需要在tail
命令后加入-f --tail
标志即可。例如,可以用docker logs --tail 10 daemon_dave
获取日志的最后10行内容。另外,也可以用docker logs --tail 0 -f daemon_dave
命令来跟踪某个容器的最新日志而不必读取整个日志文件。
为了让调试更简单,还可以使用-t
标志为每条日志项加上时间戳,如代码清单3-21所示。
代码清单3-21 跟踪守护式容器的最新日志
$ sudo docker logs -ft daemon_dave
[May 10 13:06:17.934] hello world
[May 10 13:06:18.935] hello world
[May 10 13:06:19.937] hello world
[May 10 13:06:20.939] hello world
[May 10 13:06:21.942] hello world
. . .
提示
同样,可以通过Ctr+C退出日志跟踪。
自Docker 1.6开始,也可以控制Docker守护进程和容器所用的日志驱动,这可以通过–log-driver选项来实现。可以在启动Docker守护进程或者执行docker run命令时使用这个选项。
有好几个选项,包括默认的json-file,json-file也为我们前面看到的docker logs命令提供了基础。
其他可用的选项还包括syslog,该选项将禁用docker logs命令,并且将所有容器的日志输出都重定向到Syslog。可以在启动Docker守护进程时指定该选项,将所有容器的日志都输出到Syslog,或者通过docker run对个别的容器进行日志重定向输出。
代码清单3-22 在容器级别启动Syslog
$ sudo docker run --log-driver="syslog" --name daemon_dwayne -d
ubuntu /bin/sh -c "while true; do echo hello world; sleep 1;
done"
. . .
提示
如果是在Docker Toolbox中运行Docker,应该在虚拟机中启动Syslog守护进程。可以先通过docker-machine ssh命令连接到Docker Toolbox虚拟机,再在其中运行syslogd命令来启动Syslog守护进程。
上面的命令会将daemon_dwayne容器的日志都输出到Syslog,导致docker logs命令不输出任何东西。
最后,还有一个可用的选项是none,这个选项将会禁用所有容器中的日志,导致docker logs命令也被禁用。
提示
新的日志驱动也在不断地增加,在Docker 1.8中,新增了对Graylog GELF协议、Fluentd以及日志轮转驱动的支持。
除了容器的日志,也可以查看容器内部运行的进程。要做到这一点,要使用docker top
命令,如代码清单3-23所示。
代码清单3-23 查看守护式容器的进程
$ sudo docker top daemon_dave
该命令执行后,可以看到容器内的所有进程(主要还是我们的while
循环)、运行进程的用户及进程ID,如代码清单3-24所示。
代码清单3-24 docker
**top
命令的输出结果
PID USER COMMAND
977 root /bin/sh -c while true; do echo hello world; sleep 1;
done
1123 root sleep 1
除了docker top
命令,还可以使用docker stats
命令,它用来显示一个或多个容器的统计信息。让我们来看看它的输出是什么样的。下面我们来查看一下容器daemon_dave
以及其他守护式容器的统计信息。
代码清单3-25 docker stats命令
$ sudo docker stats daemon_dave daemon_kate daemon_clare daemon_sarah
CONTAINER CPU % MEM USAGE/LIMIT MEM % NET I/O BLOCK I/O
daemon_clare 0.10% 220 KiB/994 MiB 0.02% 1.898 KiB/648 B 12.75 MB / 0 B
daemon_dave 0.14% 212 KiB/994 MiB 0.02% 5.062 KiB/648 B 1.69 MB / 0 B
daemon_kate 0.11% 216 KiB/994 MiB 0.02% 1.402 KiB/648 B 24.43 MB / 0 B
daemon_sarah 0.12% 208 KiB/994 MiB 0.02% 718 B/648 B 11.12 MB / 0 B
我们能看到一个守护式容器的列表,以及它们的CPU、内存、网络I/O及存储I/O的性能和指标。这对快速监控一台主机上的一组容器非常有用。
注意
docker stats是Docker 1.5.0中引入的命令。
在Docker 1.3之后,也可以通过docker exec
命令在容器内部额外启动新进程。可以在容器内运行的进程有两种类型:后台任务和交互式任务。后台任务在容器内运行且没有交互需求,而交互式任务则保持在前台运行。对于需要在容器内部打开shell的任务,交互式任务是很实用的。下面先来看一个后台任务的例子,如代码清单3-26所示。
代码清单3-26 在容器中运行后台任务
$ sudo docker exec -d daemon_dave touch /etc/new_config_file
这里的-d
标志表明需要运行一个后台进程,-d
标志之后,指定的是要在内部执行这个命令的容器的名字以及要执行的命令。上面例子中的命令会在daemon_dave
容器内创建了一个空文件,文件名为/etc/new_config_file
。通过docker exec
后台命令,可以在正在运行的容器中进行维护、监控及管理任务。
{提示}
从Docker 1.7开始,可以对docker exec启动的进程使用–u标志为新启动的进程指定一个用户属主。
我们也可以在daemon_dave
容器中启动一个诸如打开shell的交互式任务,如代码清单3-27所示。
代码清单3-27 在容器内运行交互命令
$ sudo docker exec -t -i daemon_dave /bin/bash
和运行交互容器时一样,这里的-t
和-i
标志为我们执行的进程创建了TTY并捕捉STDIN
。接着我们指定了要在内部执行这个命令的容器的名字以及要执行的命令。在上面的例子中,这条命令会在daemon_dave
容器内创建一个新的bash会话,有了这个会话,我们就可以在该容器中运行其他命令了。
注意
docker exec命令是Docker 1.3引入的,早期版本并不支持该命令。对于早期Docker版本,请参考第6章中介绍的nsenter命令。
要停止守护式容器,只需要执行docker stop
命令,如代码清单3-28所示。
代码清单3-28 停止正在运行的Docker容器
$ sudo docker stop daemon_dave
当然,也可以用容器ID来指代容器名称,如代码清单3-29所示。
代码清单3-29 通过容器ID停止正在运行的容器
$ sudo docker stop c2c4e57c12c4
注意
docker stop命令会向Docker容器进程发送SIGTERM信号。如果想快速停止某个容器,也可以使用docker kill命令来向容器进程发送SIGKILL信号。
要想查看已经停止的容器的状态,则可以使用docker ps
命令。还有一个很实用的命令docker ps -n x
,该命令会显示最后x
个容器,不论这些容器正在运行还是已经停止。
如果由于某种错误而导致容器停止运行,还可以通过--restart
标志,让Docker自动重新启动该容器。--restart
标志会检查容器的退出代码,并据此来决定是否要重启容器。默认的行为是Docker不会重启容器。
代码清单3-30是一个在docker run
命令中使用—restart
标志的例子。
代码清单3-30 自动重启容器
$ sudo docker run --restart=always --name daemon_dave -d ubuntu /
bin/sh -c "while true; do echo hello world; sleep 1; done"
在本例中,--restart
标志被设置为always
。无论容器的退出代码是什么,Docker都会自动重启该容器。除了always
,还可以将这个标志设为on-failure
,这样,只有当容器的退出代码为非0值的时候,才会自动重启。另外,on-failure``还接受一个可选的
重启次数参数,如代码清单3-31所示。
代码清单3-31 为on-failure
指定count
参数
--restart=on-failure:5
这样,当容器退出代码为非0时,Docker会尝试自动重启该容器,最多重启5次。
注意
–restart标志是Docker1.2.0引入的选项。
除了通过docker ps
命令获取容器的信息,还可以使用docker inspect``来
获得更多的容器信息,如代码清单3-32所示。
代码清单3-32 查看容器
$ sudo docker inspect daemon_dave
[{
"ID": "
c2c4e57c12c4c142271c031333823af95d64b20b5d607970c334784430bcbd0f
",
"Created": "2014-05-10T11:49:01.902029966Z",
"Path": "/bin/sh",
"Args": [
"-c",
"while true; do echo hello world; sleep 1; done"
],
"Config": {
"Hostname": "c2c4e57c12c4",
. . .
docker inspect
命令会对容器进行详细的检查,然后返回其配置信息,包括名称、命令、网络配置以及很多有用的数据。
也可以用-f
或者--format
标志来选定查看结果,如代码清单3-33所示。
代码清单3-33 有选择地获取容器信息
$ sudo docker inspect --format='{{ .State.Running }}' daemon_dave
false
上面这条命令会返回容器的运行状态,示例中该状态为false
。我们还能获取其他有用的信息,如容器IP地址,如代码清单3-34所示。
代码清单3-34 查看容器的IP地址
$ sudo docker inspect --format '{{ .NetworkSettings.IPAddress }}'
daemon_dave
172.17.0.2
提示
–format或者-f标志远非表面看上去那么简单。该标志实际上支持完整的Go语言模板。用它进行查询时,可以充分利用Go语言模板的优势。
也可以同时指定多个容器,并显示每个容器的输出结果,如代码清单3-35所示。
代码清单3-35 查看多个容器
$ sudo docker inspect --format '{{.Name}} {{.State.Running}}' \
daemon_dave bob_the_container
/daemon_dave false
/bob_the_container false
可以为该参数指定要查询和返回的查看散列(inspect hash)中的任意部分。
注意
除了查看容器,还可以通过浏览/var/lib/docker目录来深入了解Docker的工作原理。该目录存放着Docker镜像、容器以及容器的配置。所有的容器都保存在/var/lib/docker/containers目录下。
如果容器已经不再使用,可以使用docker rm
命令来删除它们,如代码清单3-36所示。
代码清单3-36 删除容器
$ sudo docker rm 80430f8d0921
80430f8d0921
注意
从Docker 1.6.2开始,可以通过给docker rm命令传递-f标志来删除运行中的Docker容器。这之前的版本必须先使用docker stop或docker kill命令停止容器,才能将其删除。
目前,还没有办法一次删除所有容器,不过可以通过代码清单3-37所示的小技巧来删除全部容器。
代码清单3-37 删除所有容器
$ sudo docker rm `sudo docker ps -a -q`
上面的docker ps
命令会列出现有的全部容器,-a
标志代表列出所有容器,而-q
标志则表示只需要返回容器的ID而不会返回容器的其他信息。这样我们就得到了容器ID的列表,并传给了docker rm
命令,从而达到删除所有容器的目的。
本文中介绍了Docker容器的基本工作原理。这里学到的内容也是本书剩余章节中学习如何使用Docker的基础。
预知下一篇:如何构建自己的Docker镜像,以及如何使用Docker仓库和Docker Registry请购买《第一本Docker书》(修订版)进行学习。
本书专注于Docker 1.9及以上版本,指导读者完成Docker的安装、部署、管理和扩展,带领读者经历从测试到生产的整个开发生命周期,让读者了解Docker适用于什么场景。书中先介绍Docker及其组件的基础知识,然后介绍用Docker构建容器和服务来完成各种任务:利用Docker为新项目建立测试环境,演示如何使用持续集成的工作流集成Docker,如何构建应用程序服务和平台,如何使用Docker的API,如何扩展Docker。