本系列文章将介绍docker的有关知识:
(1)docker 安装及基本用法
(2)docker 镜像
(3)docker 容器的隔离性 – 使用 linux namespace 隔离容器的运行环境
(4)docker 容器的隔离性 – 使用 cgroups 限制容器使用的资源
(5)docker 网络
对于每个软件,除了它自身的代码以外,它的运行还需要有一个运行环境和依赖。不管这个软件是象往常一样运行在物理机或者虚机之中,还是运行在现在的容器之中,这些都是不变的。在传统环境中,软件在运行之前也需要经过 代码开发->运行环境准备 -> 安装软件 -> 运行软件 等环节,在容器环境中,中间的两个环节被镜像制作过程替代了。也就是说,镜像的制作也包括运行环境准备和安装软件等两个主要环节,以及一些其他环节。因此,docker 容器镜像其实并没有什么新的理论,只是这过程有了新的方式而已。
镜像(image)是动态的容器的静态表示(specification),包括容器所要运行的应用代码以及运行时的配置。docker 镜像包括一个或者多个只读层( read-only layers ),因此,镜像一旦被创建就再也不能被修改了。一个运行着的docker 容器是一个镜像的实例( instantiation )。从同一个镜像中运行的容器包含有相同的应用代码和运行时依赖。但是不像镜像是静态的,每个运行着的容器都有一个可写层( writable layer ,也成为容器层 container layer),它位于底下的若干只读层之上。运行时的所有变化,包括对数据和文件的写和更新,都会保存在这个层中。因此,从同一个镜像运行的多个容器包含了不同的容器层。
docker 有两种方式来创建一个容器镜像:
创建一个容器,运行若干命令,再使用 docker commit 来生成一个新的镜像。不建议使用这种方案。 创建一个 dockerfile 然后再使用 docker build 来创建一个镜像。大多人会使用 dockerfile 来创建镜像。1. docker build 生成镜像
1.1 生成过程实例
在使用 dockerfile 创建容器之前,需要先准备一个 dockerfile 文件,然后运行 docker build 命令来创建镜像。我们通过下面的例子来看看docker 创建容器的过程。
这是一个非常简单的dockerfile,它的目的是基于 ubuntu 14.04 基础镜像安装 ntp 从而生成一个新的镜像。看看其过程:
dockerfile 中的每个步骤都会对应每一个 docker build 输出中的 step。
step 1:from ubuntu:14.04
获取基础镜像 ubuntu:14.04. docker 首先会在本地查找,如果找到了,则直接利用;否则从 docker registry 中下载。在第一次使用这个基础镜像的时候,docker 会从 docker hub 中下载这个镜像,并保存在本地:
以后再使用的时候就直接使用这个镜像而不再需要下载了。
step 2:maintainer sammy “sammy@sammy.com”
本例中依然是从 cache 中环境新的镜像。在第一次的时候,docker 会创建一个临时的容器 1be8f33c1846,然后运行 maintainer 命令,再使用 docker commit 生成新的镜像
通过这个临时容器的过程(create -> commit -> destroy),生成了新的镜像 c4299e3f774c:
2016-09-16t21:58:09.010886393+08:00 container create 1be8f33c18469f089d1eee8c444dad1ff0c7309be82767092082311379245358 (image=sha256:4a725d3b3b1cc18c8cbd05358ffbbfedfe1eb947f58061e5858f08e2899731ee, name=focused_poitras)
2016-09-16t21:58:09.060071206+08:00 container commit 1be8f33c18469f089d1eee8c444dad1ff0c7309be82767092082311379245358 (comment=, image=sha256:4a725d3b3b1cc18c8cbd05358ffbbfedfe1eb947f58061e5858f08e2899731ee, name=focused_poitras)
2016-09-16t21:58:09.071988068+08:00 container destroy 1be8f33c18469f089d1eee8c444dad1ff0c7309be82767092082311379245358 (image=sha256:4a725d3b3b1cc18c8cbd05358ffbbfedfe1eb947f58061e5858f08e2899731ee, name=focused_poitras)这个镜像是基于 ubuntu 14.04 基础镜像生成的,layers 没有变化,只是元数据 cmd 发生了改变:
因此可以认为只是镜像的元数据发生了改变。生成的新的镜像作为中间镜像会被保存在 cache 中。
step 3: run apt-get update
本例中docker 仍然从缓存中获取了镜像。在第一次的时候,docker 仍然是通过创建临时容器在执行 docker commit 的方式来创建新的镜像:
step 3 : run apt-get update通过以上步骤,生成了新的中间镜像 694a19d54103,它也会被保存在缓存中。你可以使用 docker inspect 694a19d54103 命令查看该中间镜像,但是无法在docker images 列表中找到它,这是因为 docker images 默认隐藏了中间状态的镜像,因此你需要使用 docker images -a 来获取它:
该镜像和原始镜像相比,多了一个 layer,它保存的是 apt-get update 命令所带来的变化:
step 4: run apt-get -y install ntp
和上面 step 3 过程一样,这个步骤也会通过创建临时容器,执行该命令,再使用 docker commit 命令生成一个中间镜像 9cc05cf6f48d 。和上面步骤生成的镜像相比,它又多了一层:
step 5: expose 5555
这一步和上面的 step 2 一样,docker 生成了一个临时容器,执行 expose 55 命令,再通过 docker commit 创建了中间镜像 f5c96137bec9。该镜像的 layers 没有变化,但是元数据发生了一些变化,包括:
step 6: cmd [“/usr/sbin/ntpd”]
这一步和上面的步骤相同,最终它创建了镜像 af678df648bc,该镜像只是修改了 cmd 元数据:
该镜像也是docker 根据本 dockerfile 生成的最终镜像。它也出现在了 docker images 结果中:
我们可以使用 docker history 命令查看该镜像中每一层的信息:
以上过程说明:
容器镜像包括元数据和文件系统,其中文件系统是指对基础镜像的文件系统的修改,元数据不影响文件系统,只是会影响容器的配置 每个步骤都会生成一个新的镜像,新的镜像与上一次的镜像相比,要么元数据有了变化,要么文件系统有了变化而多加了一层 docker 在需要执行指令时通过创建临时镜像,运行指定的命令,再通过 docker commit 来生成新的镜像 docker 会将中间镜像都保存在缓存中,这样将来如果能直接使用的话就不需要再从头创建了。关于镜像缓存,请搜索相关文档。1.2 docker 镜像分层,cow 和 镜像大小(size)
1.2.1 镜像分层和容器层
从上面例子可以看出,一个 docker 镜像是基于基础镜像的多层叠加,最终构成和容器的 rootfs (根文件系统)。当 docker 创建一个容器时,它会在基础镜像的容器层之上添加一层新的薄薄的可写容器层。接下来,所有对容器的变化,比如写新的文件,修改已有文件和删除文件,都只会作用在这个容器层之中。因此,通过不拷贝完整的 rootfs,docker 减少了容器所占用的空间,以及减少了容器启动所需时间。
1.2.2 cow 和镜像大小
cow,copy-on-write 技术,一方面带来了容器启动的快捷,另一方也造成了容器镜像大小的增加。每一次 run 命令都会在镜像上增加一层,每一层都会占用磁盘空间。举个例子,在 ubuntu 14.04 基础镜像中运行 run apt-get upgrade 会在保留基础层的同时再创建一个新层来放所有新的文件,而不是修改老的文件,因此,新的镜像大小会超过直接在老的文件系统上做更新时的文件大小。因此,为了减少镜像大小起见,所有文件相关的操作,比如删除,释放和移动等,都需要尽可能地放在一个 run 指令中进行。
比如说,通过将上面的示例 dockerfile 修改为:
结果产生的镜像,不仅层数少了一层(7 -> 6),而且大小减少了 0.001m :),因为这个例子比较特殊,文件都是添加,而没有更新,因此size 的下降非常小。
1.2.3 使用容器需要避免的一些做法
下面列举了一些在使用容器时需要避免的做法,包括:
不要在容器中保存数据(dont store data in containers) 将应用打包到镜像再部署而不是更新到已有容器(dont ship your application in two pieces) 不要产生过大的镜像 (dont create large images) 不要使用单层镜像 (dont use a single layer image) 不要从运行着的容器上产生镜像 (dont create images from running containers ) 不要只是使用 “latest”标签 (dont use only the “latest” tag) 不要在容器内运行超过一个的进程 (dont run more than one process in a single container ) 不要在容器内保存 credentials,而是要从外面通过环境变量传入 ( dont store credentials in the image. use environment variables) 不要使用 root 用户跑容器进程(dont run processes as a root user ) 不要依赖于ip地址,而是要从外面通过环境变量传入 (dont rely on ip addresses )2. dockerfile 语法
上面的步骤说明了 docker 可以通过读取 dockerfile 的内容来生成容器镜像。dockerfile 的每一行都是 instruction arguments 格式,即 “指令 参数”。关于 dockerfile 的预防,请参考https://docs.docker.com/engine/reference/builder/。下面只是就一些主要的指令做一些说明。
2.1 几个主要指令
2.1.1 add 和 copy
add:将 host 上的文件拷贝到或者将网络上的文件下载到容器中的指定目录
例子:
add 指令会将本地 temp 目录中的文件拷贝到容器的 dockfile 目录下面,从而在镜像中增加一个 layer。在未指定绝对路径的时候,会放到 workdir 目录下面。
那两者有什么区别呢?
add 多了2个功能, 下载url和对支持的压缩格式的包进行解压. 其他都一样。比如 http://foo.com/bar.go/tmp/main.go 会将文件从因特网上方下载下来,add /foo.tar.gz /tmp/ 会将压缩文件解压再copy过去 如果你不希望压缩文件拷贝到container后会被解压的话, 那么使用copy。 如果需要自动下载url并拷贝到container的话, 请使用add2.1.2 cmd
cmd:在容器被创建后执行的命令,和 run 不同,它是在构造容器时候所执行的命令
cmd 有三种格式:
cmd [“executable”,”param1″,”param2″] (like an exec, preferred form) cmd [“param1″,”param2”] (作为 entrypoint 的参数) cmd command param1 param2 (作为 shell 运行)一个dockerfile里只能有一个cmd,如果有多个,只有最后一个生效。
2.1.3 entrypoint
entrypoint :设置默认应用,会保证每次容器被创建后该应用都会被执行。cmd 和 entrypoint 的关系会在下面详细解释。
2.1.4 env:设置环境变量,可以使用多次
设置了后,后续的run命令都可以使用,并且会作为容器的环境变量。举个例子,下面是 dockfile:
生成镜像:docker build -t envimg4 -f dockerfile-env . 其元数据包括了这两个环境变量:
启动容器:docker run -it –name envc41 envimg4。也能看到:
进入容器:能看到定义的 abc 和 def 变量
2.1.5 expose :向容器外暴露一个端口
2.1.6 from:指定进行的基础镜像,必须是第一条指令
2.1.7 maintainer:可以在任意地方使用,设置镜像的作者
2.1.8 run:运行命令,结果会生成镜像中的一个新层
2.1.9 user:设置该镜像的容器的主进程所使用的用户,以及后续 run, cmd 和 entrypoint 指令运行所使用的用户
语法:
dockerfile 中的默认用户是基础镜像中所使用的用户。比如,你的镜像是从一个使用非 root 用户 sammy 的镜像继承而来的,那么你的 dockerfile 中 run 指定运行的命令的用户就会使用 sammy 用户。
举例:
(1)创建 dockerfile 文件
(2)创建镜像:docker build -t dockerfile-user-1000 -f dockerfile-user .
(3)启动容器:docker run -it –name c-user-1000-3 dockerfile-user-1000 top
能看出来当前用户id 为 1000:
(4)基于该镜像再创造一个镜像,然后再启动一个容器,可以发现容器中进程所使用的用户id 同样为 1000.
2.1.10 volume:允许容器访问host上某个目录
2.1.11 workdir:设置 cmd 所指定命令的执行目录
2.1.12 healthcheck: 容器健康检查
这是 docker 1.12 版本中新引入的指令,其语法为 healthcheck [options] cmd command。 来看一个例子:
在启动容器后,其health 状态首先是 starting,然后在过了10秒做了第一次健康检查成功后,变为 healthy 状态。
需要注意的是 cmd 是在容器之内运行的,因此,你需要确保其命令或者脚本存在于容器之内并且可以被运行。
2.2 几个比较绕的地方
2.2.1 expose 和 docker run -p -p 之间的关系
容器的端口必须被发出(publish)出来后才能被外界使用。dockerfile 中的 expose 只是“标记”某个端口会被暴露出来,只有在使用了 docker run -p 或者 -p 后,端口才会被“发出”出来,此时端口才能被使用。
举例:
(1)dockerfile
(2)创建镜像:docker build -t no-exposed-ports -f dockerfile-ports .
(3)启动容器1:docker run -d –name no-exposed-ports1 no-exposed-ports。此容器没有 exposed 和 published 任何端口。
(4)启动容器2:docker run -d –name no-exposed-ports2 -p 8888:8888 no-exposed-ports
此时容器的 8888 端口被发布为主机上的 8888 端口:
该端口会正确返回:
(5)使用 -p 参数:docker run -d –name no-exposed-ports3 -p no-exposed-ports
此时没有任何端口被 published,说明 docker 在使用了 “-p” 情形下只是自动将 exposed 的端口 published。
(6)使用 -p 加上一个不存在的端口:docker run -d –name no-exposed-ports4 -p 8889:8889 no-exposed-ports
此时,8889 端口会被暴露,但是没法使用。说明 -p 会将没有 exposed 的端口自动 exposed 出来。
(7)修改 dockerfile 为:
创建镜像exposed-ports, 再运行 docker run -d –name exposed-ports1 -p exposed-ports 创建一个容器,此时 8888 端口自动被 published 为主机上的 32776 端口:
可见:
expose或者–expose只是为其他命令提供所需信息的元数据,或者只是告诉容器操作人员有哪些已知选择。它只是作为记录机制,也就是告诉用户哪些端口会提供服务。它保存在容器的元数据中。 使用 -p 发布特定端口。如果该端口已经被 exposed,则发布它;如果它还没有被 exposed,则它会被 exposed 和 published。docker 不会检查容器端口的正确性。 使用 -p 时 docker 会自动将所有已经被 exposed 的端口发出出来。2.2.2 cmd 和 entrypoint
这两个指令都指定了运行容器时所运行的命令。以下是它们共存的一些规则:
dockerfile 至少需要指定一个 cmd 或者 entrypoint 指令 cmd 可以用来指定 entrypoint 指令的参数 没有 entrypoint entrypoint exec_entry p1_entry entrypoint [“exec_entry”, “p1_entry”] 没有 cmd 错误,不允许 /bin/sh -c exec_entry p1_entry exec_entry p1_entry cmd [“exec_cmd”, “p1_cmd”] exec_cmd p1_cmd /bin/sh -c exec_entry p1_entry exec_cmd p1_cmd exec_entry p1_entry exec_cmd p1_cmd cmd [“p1_cmd”, “p2_cmd”] p1_cmd p2_cmd /bin/sh -c exec_entry p1_entry p1_cmd p2_cmd exec_entry p1_entry p1_cmd p2_cmd cmd exec_cmd p1_cmd /bin/sh -c exec_cmd p1_cmd /bin/sh -c exec_entry p1_entry /bin/sh -c exec_cmd p1_cmd exec_entry p1_entry /bin/sh -c exec_cmd p1_cmd 备注 只有 cmd 时,执行 cmd 定义的指令 cmd 和 entrypoint 都存在时,cmd 的指令作为 entrypoint 的参数举例:
(1)同时有 cmd 和 entrypoint
此时会运行的指令为 /bin/sh -c ps /bin/sh -c top
但是实际上只是运行了 ps:
(2)cmd 作为 entrypoint 的参数
启动容器后运行的命令为 /bin/sh -c top -n 10.
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。