理解Docker(2):Docker 镜像

本系列文章将介绍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. 镜像有关的几个基础概念

1.1 Host OS VS Guest OS VS Base image

比如,一台主机安装的是 Centos 操作系统,现在在上面跑一个 Ubuntu 容器。此时,Host OS 是 Centos,Guest OS 是 Ubuntu。Guest OS 也被成为容器的 Base Image。

理解Docker(2):Docker 镜像_第1张图片

 

一些说明:

  • 关于 linux 内核和版本:所有 Linux 发行版都采用相同的 Linux 内核(kernel),然后所有发行版对内核都有轻微改动。这些改动都会上传回 linux 社区,并被合并。
  • 关于Linux 容器环境:因为所有Linux发行版都包含同一个linux 内核(有轻微修改),以及不同的自己的软件,因此,会很容易地将某个 userland 软件安装在linux 内核上,来模拟不同的发行版环境。比如说,在 Ubuntu 上运行 Centos 容器,这意味着从 Centos 获取 userland 软件,运行在 Ubuntu 内核上。因此,这就像在同一个操作系统(linux 内核)上运行不同的 userland 软件(发行版的)。这就是为什么Docker 不支持在 Linux 主机上运行 FreeBSD 或者windows 容器。

可见,容器的 base image 并不真的是 base OS。Base image 会远远比 base OS 更轻量。它只安装发行版特殊的部分(userland 软件)。

理解Docker(2):Docker 镜像_第2张图片

那为什么还需要 base image 呢?这是因为,docker 容器文件系统与 host OS 是隔离的。容器镜像中的应用软件无法看到主机文件系统,除非将主机文件系统挂载为容器的卷。因此,可以想像一下,你容器中的应用依赖于各种操作系统库,因此我们不得不将这些库打包到镜像之中。另外,base image 会让我们使用到各个发行版的包管理系统,比如 yum 和 apt-get。而且,各个linux 发行版的 base image 也不是普通的发行版,而是一个简化了的版本。而且,base image 并不带有 linux 内核,因为容器会使用主机的内核。

因此,需要注重理解 image 和 OS 这两个概念。之所以成为 base image,而不是 base OS,是因为 base image 中并不包括完整的 OS。而这一点,是容器与虚拟机之前的本质区别之一。那就是,容器并没有虚拟化,而是共享主机上的linux 内核。

1.2 关于Container Base image

从上面内容可以看出,容器把 linux 镜像从内核空间和用户空间进行了分开管理。对 host OS 来说,它更侧重于内核,再加上少量的用户空间内容;对 Guest OS 来说,它侧重于(只有)用户空间,只包括库文件、编译器、配置文件,以及用户代码。

理解Docker(2):Docker 镜像_第3张图片

常见的容器基础镜像:

理解Docker(2):Docker 镜像_第4张图片

因此,用户需要仔细选择容器的 base image,不仅从上表中的几个方面,还包括性能、安全性等一些因素。

Ubuntu 更是推出了只有 29M 的 Minimal Ubuntu 容器镜像,具体在这里 https://blog.ubuntu.com/2018/07/09/minimal-ubuntu-released。 

2. docker build 生成镜像

2.1 生成过程实例

 在使用 Dockerfile 创建容器之前,需要先准备一个 Dockerfile 文件,然后运行 docker build 命令来创建镜像。我们通过下面的例子来看看Docker 创建容器的过程。

FROM ubuntu:14.04

MAINTAINER sammy "[email protected]"

RUN apt-get update

RUN apt-get -y install ntp

EXPOSE 5555

CMD ["/usr/sbin/ntpd"]

这是一个非常简单的Dockerfile,它的目的是基于 Ubuntu 14.04 基础镜像安装 ntp 从而生成一个新的镜像。看看其过程:

root@devstack:/home/sammy/ntponubuntu# docker build -t sammy_ntp2 .
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM ubuntu:14.04
 ---> 4a725d3b3b1c
Step 2 : MAINTAINER sammy "[email protected]"
 ---> Using cache
 ---> c4299e3f774c
Step 3 : RUN apt-get update
 ---> Using cache
 ---> 694a19d54103
Step 4 : RUN apt-get -y install ntp
 ---> Running in 9bd153c65a76
Reading package lists...
...
Fetched 561 kB in 10s (51.1 kB/s)
Selecting previously unselected package libedit2:amd64.
(Reading database ... 11558 files and directories currently installed.)
...
Processing triggers for libc-bin (2.19-0ubuntu6.9) ...
Processing triggers for ureadahead (0.100.0-16) ...
 ---> 9cc05cf6f48d
Removing intermediate container 9bd153c65a76
Step 5 : EXPOSE 5555
 ---> Running in eb4633151d98
 ---> f5c96137bec9
Removing intermediate container eb4633151d98
Step 6 : CMD /usr/sbin/ntpd
 ---> Running in e81b1eae3678
 ---> af678df648bc
Removing intermediate container e81b1eae3678
Successfully built af678df648bc

Dockerfile 中的每个步骤都会对应每一个 docker build 输出中的 step。

Step 1:FROM ubuntu:14.04

获取基础镜像 ubuntu:14.04. Docker 首先会在本地查找,如果找到了,则直接利用;否则从 Docker registry 中下载。在第一次使用这个基础镜像的时候,Docker 会从 Docker Hub 中下载这个镜像,并保存在本地:

Step 1 : FROM ubuntu:14.04
14.04: Pulling from library/ubuntu
862a3e9af0ae: Pull complete
6498e51874bf: Pull complete
159ebdd1959b: Pull complete
0fdbedd3771a: Pull complete
7a1f7116d1e3: Pull complete
Digest: sha256:5b5d48912298181c3c80086e7d3982029b288678fccabf2265899199c24d7f89
Status: Downloaded newer image for ubuntu:14.04
 ---> 4a725d3b3b1c

以后再使用的时候就直接使用这个镜像而不再需要下载了。

Step 2:MAINTAINER sammy "[email protected]"

本例中依然是从 Cache 中环境新的镜像。在第一次的时候,Docker 会创建一个临时的容器 1be8f33c1846,然后运行 MAINTAINER 命令,再使用 docker commit 生成新的镜像

Step 2 : MAINTAINER sammy "[email protected]"
 ---> Running in 1be8f33c1846
 ---> c4299e3f774c

通过这个临时容器的过程(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 发生了改变:

"Cmd": [
                "/bin/sh",
                "-c",
                "#(nop) ",
                "MAINTAINER sammy \"[email protected]\""
            ]

因此可以认为只是镜像的元数据发生了改变。生成的新的镜像作为中间镜像会被保存在 cache 中。

 Step 3: RUN apt-get update

本例中Docker 仍然从缓存中获取了镜像。在第一次的时候,Docker 仍然是通过创建临时容器在执行 docker commit 的方式来创建新的镜像:

Step 3 : RUN apt-get update
 ---> Running in 8b3b97af3bd7
Ign http://archive.ubuntu.com trusty InRelease
Get:1 http://archive.ubuntu.com trusty-updates InRelease [65.9 kB]
...
Get:22 http://archive.ubuntu.com trusty/universe amd64 Packages [7589 kB]
Fetched 22.2 MB in 16min 21s (22.6 kB/s)
Reading package lists...
 ---> 694a19d54103
Removing intermediate container 8b3b97af3bd7

通过以上步骤,生成了新的中间镜像 694a19d54103,它也会被保存在缓存中。你可以使用 docker inspect 694a19d54103 命令查看该中间镜像,但是无法在docker images 列表中找到它,这是因为 docker images 默认隐藏了中间状态的镜像,因此你需要使用 docker images -a 来获取它:

root@devstack:/home/sammy# docker images -a | grep 694a19d54103
                                694a19d54103        11 hours ago        210.1 MB

该镜像和原始镜像相比,多了一个 layer,它保存的是 apt-get update 命令所带来的变化:

"RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:102fca64f92471ff7fca48e55807ae2471502822ba620292b0a06ebcab907cf4",
                "sha256:24fe29584c046f2a88f7f566dd0bf7b08a8c0d393dfad8370633b0748bba8cbc",
                "sha256:530d731d21e1b1bbe356d70d3bca4d72d76fed89e90faab271d29bd58c8ccea4",
                "sha256:344f56a35ff9fc747ada7d2b88bd21c49b2ec404872662cbaf0a65201873c0c6",
                "sha256:ffb6ddc7582aa7e2e73f102df3ffcd272e59b7cf3f7abefe08d11a7c85dea53a",
                "sha256:a1afe95c99b39c30b5c1d3e8fda451bd3f066be304616197f1046e64cf6cda93" #这一层是新加的
            ]
        }

Step 4: RUN apt-get -y install ntp

和上面 Step 3 过程一样,这个步骤也会通过创建临时容器,执行该命令,再使用 docker commit 命令生成一个中间镜像 9cc05cf6f48d 。和上面步骤生成的镜像相比,它又多了一层:

root@devstack:/home/sammy# docker images -a | grep 9cc05cf6f48d
                                9cc05cf6f48d        10 hours ago        212.8 MB
root@devstack:/home/sammy# docker inspect --format={{'.RootFS.Layers'}} 9cc05cf6f48d
[sha256:102fca64f92471ff7fca48e55807ae2471502822ba620292b0a06ebcab907cf4 
sha256:24fe29584c046f2a88f7f566dd0bf7b08a8c0d393dfad8370633b0748bba8cbc
sha256:530d731d21e1b1bbe356d70d3bca4d72d76fed89e90faab271d29bd58c8ccea4
sha256:344f56a35ff9fc747ada7d2b88bd21c49b2ec404872662cbaf0a65201873c0c6
sha256:ffb6ddc7582aa7e2e73f102df3ffcd272e59b7cf3f7abefe08d11a7c85dea53a
sha256:a1afe95c99b39c30b5c1d3e8fda451bd3f066be304616197f1046e64cf6cda93
sha256:a93086f33a2b7ee18eec2454b468141f95a403f5081284b6f177f83cdb3d54ba]

Step 5: EXPOSE 5555

 这一步和上面的 Step 2 一样,Docker 生成了一个临时容器,执行 EXPOSE 55 命令,再通过 docker commit 创建了中间镜像 f5c96137bec9。该镜像的 layers 没有变化,但是元数据发生了一些变化,包括:

"ExposedPorts": {
                "5555/tcp": {}
            }
"Cmd": [
                "/bin/sh",
                "-c",
                "#(nop) ",
                "EXPOSE 5555/tcp"
            ]

Step 6: CMD ["/usr/sbin/ntpd"]

这一步和上面的步骤相同,最终它创建了镜像 af678df648bc,该镜像只是修改了 CMD 元数据:

 "Cmd": [
                "/bin/sh",
                "-c",
                "#(nop) ",
                "CMD [\"/usr/sbin/ntpd\"]"
            ]

该镜像也是Docker 根据本 Dockerfile 生成的最终镜像。它也出现在了 docker images 结果中:

root@devstack:/home/sammy# docker images | grep af678df648bc
sammy_ntp2              latest              af678df648bc        11 hours ago        212.8 MB

我们可以使用 docker history 命令查看该镜像中每一层的信息:

root@devstack:/home/sammy/ntponubuntu# docker history af678df648bc
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
af678df648bc        16 hours ago        /bin/sh -c #(nop)  CMD ["/usr/sbin/ntpd"]       0 B
f5c96137bec9        16 hours ago        /bin/sh -c #(nop)  EXPOSE 5555/tcp              0 B
9cc05cf6f48d        16 hours ago        /bin/sh -c apt-get -y install ntp               2.679 MB
694a19d54103        16 hours ago        /bin/sh -c apt-get update                       22.17 MB
c4299e3f774c        17 hours ago        /bin/sh -c #(nop)  MAINTAINER sammy "sammy@sa   0 B
4a725d3b3b1c        3 weeks ago         /bin/sh -c #(nop) CMD ["/bin/bash"]             0 B
           3 weeks ago         /bin/sh -c mkdir -p /run/systemd && echo 'doc   7 B
           3 weeks ago         /bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$/   1.895 kB
           3 weeks ago         /bin/sh -c rm -rf /var/lib/apt/lists/*          0 B
           3 weeks ago         /bin/sh -c set -xe   && echo '#!/bin/sh' > /u   194.6 kB
           3 weeks ago         /bin/sh -c #(nop) ADD file:ada91758a31d8de3c7   187.8 MB

以上过程说明:

  • 容器镜像包括元数据和文件系统,其中文件系统是指对基础镜像的文件系统的修改,元数据不影响文件系统,只是会影响容器的配置
  • 每个步骤都会生成一个新的镜像,新的镜像与上一次的镜像相比,要么元数据有了变化,要么文件系统有了变化而多加了一层
  • Docker 在需要执行指令时通过创建临时镜像,运行指定的命令,再通过 docker commit 来生成新的镜像
  • Docker 会将中间镜像都保存在缓存中,这样将来如果能直接使用的话就不需要再从头创建了。关于镜像缓存,请搜索相关文档。

2.2 Docker 镜像分层,COW 和 镜像大小(size)

2.2.1 镜像分层和容器层

  从上面例子可以看出,一个 Docker 镜像是基于基础镜像的多层叠加,最终构成和容器的 rootfs (根文件系统)。当 Docker 创建一个容器时,它会在基础镜像的容器层之上添加一层新的薄薄的可写容器层。接下来,所有对容器的变化,比如写新的文件,修改已有文件和删除文件,都只会作用在这个容器层之中。因此,通过不拷贝完整的 rootfs,Docker 减少了容器所占用的空间,以及减少了容器启动所需时间。

2.2.2 COW 和镜像大小

  COW,copy-on-write 技术,一方面带来了容器启动的快捷,另一方也造成了容器镜像大小的增加。每一次 RUN 命令都会在镜像上增加一层,每一层都会占用磁盘空间。举个例子,在 Ubuntu 14.04 基础镜像中运行 RUN apt-get upgrade 会在保留基础层的同时再创建一个新层来放所有新的文件,而不是修改老的文件,因此,新的镜像大小会超过直接在老的文件系统上做更新时的文件大小。因此,为了减少镜像大小起见,所有文件相关的操作,比如删除,释放和移动等,都需要尽可能地放在一个 RUN 指令中进行。

  比如说,通过将上面的示例 Dockerfile 修改为:

FROM ubuntu:14.04
MAINTAINER sammy "[email protected]"
RUN apt-get update && apt-get -y install ntp
EXPOSE 5555
CMD ["/usr/sbin/ntpd"]

结果产生的镜像,不仅层数少了一层(7 -> 6),而且大小减少了 0.001M :),因为这个例子比较特殊,文件都是添加,而没有更新,因此size 的下降非常小。

2.2.3 使用容器需要避免的一些做法

这篇文章 10 things to avoid in docker containers 列举了一些在使用容器时需要避免的做法,包括:

  • 不要在容器中保存数据(Don’t store data in containers
  • 将应用打包到镜像再部署而不是更新到已有容器(Don’t ship your application in two pieces
  • 不要产生过大的镜像 (Don’t create large images
  • 不要使用单层镜像 (Don’t use a single layer image
  • 不要从运行着的容器上产生镜像 (Don’t create images from running containers )
  • 不要只是使用 “latest”标签 (Don’t use only the “latest” tag
  • 不要在容器内运行超过一个的进程 (Don’t run more than one process in a single container )
  • 不要在容器内保存 credentials,而是要从外面通过环境变量传入 ( Don’t store credentials in the image. Use environment variables
  • 不要使用 root 用户跑容器进程(Don’t run processes as a root user )
  • 不要依赖于IP地址,而是要从外面通过环境变量传入 (Don’t rely on IP addresses )

2.3 镜像的内容

容器镜像的内容,其实是一个 json 文件加上 tar 包。以非常小的镜像 kubernetes/pause 为例,我们来做个实验:

(1)将镜像导出为 tar 文件

root@kub-node-1:/home/ubuntu/kub/image# docker save -o pause.tar kubernetes/pause:latest
root@kub-node-1:/home/ubuntu/kub/image# ls
pause.tar

(2)解压 pause.tar 文件

root@kub-node-1:/home/ubuntu/kub/image# tar -xf pause.tar

root@kub-node-1:/home/ubuntu/kub/image/pause# ls -l
total 280
drwxr-xr-x 2 root root 4096 Jan 23 09:02 afa9f35badc97e21193ee701222d9edfc5b0f0e5c518d357eb8b016d8287cda7
drwxr-xr-x 2 root root 4096 Jul 19 2014 e0b1695ad29a961b7e28713942942786692107d7f9087d72ccf9bbc0a3ab133e
drwxr-xr-x 2 root root 4096 Jan 23 09:20 e3caa892ed5297d0c98916b251c5be1d26c3a50b581fe145e3a6516c00531464
-rw-r--r-- 1 root root 1691 Jul 19 2014 f9d5de0795395db6c50cb1ac82ebed1bd8eb3eefcebb1aa724e01239594e937b.json
-rw-r--r-- 1 root root 366 Jan 1 1970 manifest.json
-rw------- 1 root root 258560 Jan 23 09:02 pause.tar
-rw-r--r-- 1 root root 99 Jan 1 1970 repositories

其中的 repositories 文件的内容,就是镜像名称、版本、最上层的layer的名称:

root@kub-node-1:/home/ubuntu/kub/image# cat repositories
{"kubernetes/pause":{"latest":"afa9f35badc97e21193ee701222d9edfc5b0f0e5c518d357eb8b016d8287cda7"}}

而 manifest.json 文件则保持的是镜像的元数据,包括真正元数据 json 文件的名称及每一层的名称,tag 等:

root@kub-node-1:/home/ubuntu/kub/image# cat manifest.json
[{"Config":"f9d5de0795395db6c50cb1ac82ebed1bd8eb3eefcebb1aa724e01239594e937b.json","RepoTags":["kubernetes/pause:latest"],"Layers":["e0b1695ad29a961b7e28713942942786692107d7f9087d72ccf9bbc0a3ab133e/layer.tar","e3caa892ed5297d0c98916b251c5be1d26c3a50b581fe145e3a6516c00531464/layer.tar","afa9f35badc97e21193ee701222d9edfc5b0f0e5c518d357eb8b016d8287cda7/layer.tar"]}]

f9d5de0795395db6c50cb1ac82ebed1bd8eb3eefcebb1aa724e01239594e937b.json 文件则真正包含镜像的所有元数据。

而剩下的3个文件夹则与该镜像的3个layers 一一对应:

"RootFS": {
            "Type": "layers",
            "Layers": [
                "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef",
                "sha256:e16a89738269fec22db26ec6362823a9ec42d0163685d88ba03c4fb5d5e723f6",
                "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
            ]
        }

每个文件夹中的内容为:

root@kub-node-1:/home/ubuntu/kub/image# ls e0b1695ad29a961b7e28713942942786692107d7f9087d72ccf9bbc0a3ab133e -l
total 12
-rw-r--r-- 1 root root  393 Jul 19  2014 json
-rw-r--r-- 1 root root 1024 Jul 19  2014 layer.tar
-rw-r--r-- 1 root root    3 Jul 19  2014 VERSION

因为 pause 镜像比较特殊,解压 layer.tar 后没有文件。如果看 nginx 镜像的某层的 layer.tar 文件,则能看到该layer中包含的文件:

root@kub-node-1:/home/ubuntu/kub/image/nginx/2c9d2d9d91f48573ea451f8d529e88dee79d64782892def6063fdda3f127d33c# ls -l
total 39268
drwxr-xr-x  2 root root     4096 Jan  8 21:49 bin
drwxr-xr-x 13 root root     4096 Jan  8 21:54 etc
-rw-r--r--  1 root root      469 Jan  8 23:32 json
drwxr-xr-x  3 root root     4096 Dec 10 08:00 lib
drwx------  2 root root     4096 Jan  8 21:56 root
drwxr-xr-x  2 root root     4096 Jan  8 21:28 run
drwxr-xr-x  2 root root     4096 Jan  8 21:49 sbin
drwxr-xr-x  7 root root     4096 Dec 10 08:00 usr
drwxr-xr-x  5 root root     4096 Dec 10 08:00 var
-rw-r--r--  1 root root        3 Jan  8 23:32 VERSION

从以上分析可见,

  • docker 镜像中主要就是 tar 文件包和元数据 json 文件
  • docker 镜像的打包过程,其实就是将每一层对应的文件打包过程,最后组成一个单一的 tar 文件
  • docker 镜像的使用过程,其实就是将一层层的 tar 文件接包到文件系统的过程。

3. Dockerfile 语法

上面的步骤说明了 Docker 可以通过读取 Dockerfile 的内容来生成容器镜像。Dockerfile 的每一行都是 INSTRUCTION arguments 格式,即 “指令 参数”。关于 Dockerfile 的预防,请参考 https://docs.docker.com/engine/reference/builder/。下面只是就一些主要的指令做一些说明。

3.1 几个主要指令

3.1.1 ADD 和 COPY

Add:将 host 上的文件拷贝到或者将网络上的文件下载到容器中的指定目录
# Usage: ADD [source directory or URL] [destination directory]
ADD /my_app_folder /my_app_folder

例子:

FROM ubuntu:14.04
MAINTAINER Sammy Liu 
ADD temp dockfile
ENTRYPOINT top

ADD 指令会将本地 temp 目录中的文件拷贝到容器的 dockfile 目录下面,从而在镜像中增加一个 layer。在未指定绝对路径的时候,会放到 WORKDIR 目录下面。

root@cc2a5605f905:/# ls dockfile/
dockerfile-add  dockerfile-cmd  dockerfile-env  dockerfile-ports  dockerfile-user  dockerfile-user-h
root@cc2a5605f905:/# pwd
/

那两者有什么区别呢?

  • ADD 多了2个功能, 下载URL和对支持的压缩格式的包进行解压.  其他都一样。比如 ADD http://foo.com/bar.go /tmp/main.go 会将文件从因特网上方下载下来,ADD /foo.tar.gz /tmp/ 会将压缩文件解压再COPY过去
  • 如果你不希望压缩文件拷贝到container后会被解压的话, 那么使用COPY。
  • 如果需要自动下载URL并拷贝到container的话, 请使用ADD

3.1.2 CMD

CMD:在容器被创建后执行的命令,和 RUN 不同,它是在构造容器时候所执行的命令
# Usage 1: CMD application "argument", "argument", ..
CMD "echo" "Hello docker!"

CMD 有三种格式:

  • CMD ["executable","param1","param2"] (like an exec, preferred form)
  • CMD ["param1","param2"] (作为 ENTRYPOINT 的参数)
  • CMD command param1 param2 (作为 shell 运行)

一个Dockerfile里只能有一个CMD,如果有多个,只有最后一个生效。

3.1.3 ENTRYPOINT

ENTRYPOINT :设置默认应用,会保证每次容器被创建后该应用都会被执行。CMD 和 ENTRYPOINT 的关系会在下面详细解释。

3.1.4 ENV:设置环境变量,可以使用多次

# Usage: ENV key value
ENV SERVER_WORKS 4

设置了后,后续的RUN命令都可以使用,并且会作为容器的环境变量。举个例子,下面是 dockfile:

FROM ubuntu:14.04
ENV abc=1
ENV def=2
ENTRYPOINT top

生成镜像:docker build -t envimg4 -f dockerfile-env . 其元数据包括了这两个环境变量:

"Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                "abc=1",
                "def=2"
            ],

启动容器:docker run -it --name envc41 envimg4。也能看到:

"Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                "abc=1",
                "def=2"
            ]

进入容器:能看到定义的 abc 和 def 变量

root@devstack:/home/sammy/ntponubuntu# docker exec -it envc41 bash
root@ba460e0e9dc4:/# echo $abc
1
root@ba460e0e9dc4:/# echo $def
2

3.1.5 EXPOSE :向容器外暴露一个端口

# Usage: EXPOSE [port]
EXPOSE 8080

3.1.6 FROM:指定进行的基础镜像,必须是第一条指令

# Usage: FROM [image name]
FROM ubuntu

3.1.7 MAINTAINER:可以在任意地方使用,设置镜像的作者

# Usage: MAINTAINER [name]
MAINTAINER authors_name

3.1.8 RUN:运行命令,结果会生成镜像中的一个新层

# Usage: RUN [command]
RUN aptitude install -y ntp

3.1.9 USER:设置该镜像的容器的主进程所使用的用户,以及后续 RUN, CMD 和 ENTRYPOINT 指令运行所使用的用户

语法:

# Usage: USER [UID]
USER 751 

Dockerfile 中的默认用户是基础镜像中所使用的用户。比如,你的镜像是从一个使用非 root 用户 sammy 的镜像继承而来的,那么你的 Dockerfile 中 RUN 指定运行的命令的用户就会使用 sammy 用户。

举例:

(1)创建 dockerfile 文件

root@devstack:/home/sammy/dockerfile# cat dockerfile-user
FROM ubuntu:14.04
USER 1000
ENTRYPOINT top

(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: 

PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND
    1 1000      20   0    4440    648    548 S  0.0  0.0   0:00.00 sh
    5 1000      20   0   19840   1296    984 R  0.0  0.1   0:00.00 top 

(4)基于该镜像再创造一个镜像,然后再启动一个容器,可以发现容器中进程所使用的用户ID 同样为 1000. 

3.1.10 VOLUME:允许容器访问host上某个目录

# Usage: VOLUME ["/dir_1", "/dir_2" ..]
VOLUME ["/my_files"]

3.1.11 WORKDIR:设置 CMD 所指定命令的执行目录

# Usage: WORKDIR /path
WORKDIR ~/

3.1.12 HEALTHCHECK: 容器健康检查

这是 Docker 1.12 版本中新引入的指令,其语法为 HEALTHCHECK [OPTIONS] CMD command。 来看一个例子:

FROM ubuntu:14.04
MAINTAINER Sammy Liu 
RUN apt-get update
RUN apt-get -y install curl
EXPOSE 8888
CMD while true; do echo 'hello world' | nc -l -p 8888; done
HEALTHCHECK --interval=10s --timeout=2s CMD curl -f http://localhost:8888/ || exit 1

在启动容器后,其health 状态首先是 starting,然后在过了10秒做了第一次健康检查成功后,变为 healthy 状态。

root@devstack:/home/sammy/dockerfile# docker ps | grep c-health2
4c459eef1894        img-health2         "/bin/sh -c 'while tr"   7 seconds ago       Up 6 seconds (health: starting)   8888/tcp                  c-health2
root@devstack:/home/sammy/dockerfile# docker ps | grep c-health2
4c459eef1894        img-health2         "/bin/sh -c 'while tr"   9 seconds ago       Up 8 seconds (health: starting)   8888/tcp                  c-health2
root@devstack:/home/sammy/dockerfile# docker ps | grep c-health2
4c459eef1894        img-health2         "/bin/sh -c 'while tr"   11 seconds ago      Up 11 seconds (healthy)     8888/tcp                  c-health2

需要注意的是 CMD 是在容器之内运行的,因此,你需要确保其命令或者脚本存在于容器之内并且可以被运行。

3.2 几个比较绕的地方

3.2.1 EXPOSE 和 docker run -p -P 之间的关系

容器的端口必须被发出(publish)出来后才能被外界使用。Dockerfile 中的 EXPOSE 只是“标记”某个端口会被暴露出来,只有在使用了 docker run -p 或者 -P 后,端口才会被“发出”出来,此时端口才能被使用。

举例:

(1)Dockerfile

FROM ubuntu:14.04
MAINTAINER Sammy Liu 
CMD while true; do echo 'hello world' | nc -l -p 8888; done

(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 端口:

"Ports": {
                "8888/tcp": [
                    {
                        "HostIp": "0.0.0.0",
                        "HostPort": "8888"
                    }
                ]
            }

该端口会正确返回:

root@devstack:/home/sammy/dockerfile# telnet 0.0.0.0 8888
Trying 0.0.0.0...
Connected to 0.0.0.0.
Escape character is '^]'.
hello world
Connection closed by foreign host.

(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 为:

FROM ubuntu:14.04
MAINTAINER Sammy Liu 
EXPOSE 8888
CMD while true; do echo 'hello world' | nc -l -p 8888; done

创建镜像exposed-ports, 再运行 docker run -d --name exposed-ports1 -P exposed-ports 创建一个容器,此时 8888 端口自动被 published 为主机上的 32776 端口:

"Ports": {
                "8888/tcp": [
                    {
                        "HostIp": "0.0.0.0",
                        "HostPort": "32776"
                    }
                ]
            }

可见:

  • EXPOSE或者--expose只是为其他命令提供所需信息的元数据,或者只是告诉容器操作人员有哪些已知选择。它只是作为记录机制,也就是告诉用户哪些端口会提供服务。它保存在容器的元数据中。
  • 使用 -p 发布特定端口。如果该端口已经被 exposed,则发布它;如果它还没有被 exposed,则它会被 exposed 和 published。Docker 不会检查容器端口的正确性。
  • 使用 -P 时 Docker 会自动将所有已经被 exposed 的端口发出出来。

3.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

FROM ubuntu:14.04
MAINTAINER Sammy Liu 
CMD top
ENTRYPOINT ps

此时会运行的指令为 /bin/sh -c ps /bin/sh -c top

但是实际上只是运行了 ps:

root@devstack:/home/sammy/dockerfile# /bin/sh -c ps /bin/sh -c top
  PID TTY          TIME CMD
10789 pts/3    00:00:00 su
10790 pts/3    00:00:00 bash
18479 pts/3    00:00:00 sh
18480 pts/3    00:00:00 ps
root@devstack:/home/sammy/dockerfile# /bin/sh -c ps
  PID TTY          TIME CMD
10789 pts/3    00:00:00 su
10790 pts/3    00:00:00 bash
18481 pts/3    00:00:00 sh
18482 pts/3    00:00:00 ps

(2)CMD 作为 ENTRYPOINT 的参数

FROM ubuntu:14.04
MAINTAINER Sammy Liu 
CMD ["-n", "10"]
ENTRYPOINT top

启动容器后运行的命令为 /bin/sh -c top -n 10.

4. 在 Docker hub 上创建自己的镜像

当我们从docker镜像仓库中下载的镜像不能满足我们的需求时,我们可以通过以下两种方式对镜像进行更改。

  • 从已经创建的容器中更新镜像,并且提交这个镜像
  • 使用 Dockerfile 指令来创建一个新的镜像

通过以下步骤,采用第一种方法,在 docker hub 上创建自己的镜像:

(1)创建 docker hub 帐号。https://hub.docker.com/

(2)基于一个镜像完成某些操作。比如基于 nginx 镜像,安装 ping ifconfig 等网络工具。首先运行 docker run -it nginx /bin/bash 基于 nginx:latest 创建一个容器,然后在容器中执行 apt-get 命令安装软件,然后运行 exit 退出容器。

(3)将容器中的内容保存为一个镜像

docker commit -m="install net tools" -a="sammyliu8" 3f8a4339aadd sammyliu8/nginx:v1

这里的 3f8a4339aadd 为刚才容器的ID。此时,能在本地看到该镜像:

(4)运行 docker login 登录 docker hub

(5)运行 docker push sammyliu8/nginx 将镜像上传到 docker hub。此时在 Docker hub 界面上能看到该镜像了。

 

(6)在其他节点上,可以运行 docker pull sammyliu8/nginx 拉该镜像了。

(7)不过,这样做出来的新nginx有个问题,那就是nginx 服务不会自动起来。这是因为,官方的 nginx 的CMD 为 nginx -g "daemon off;",但是新的镜像的CMD 为 /bin/bash。但是,运行前面命令启动的容器又无法安装软件。因此,只能先按照上面的步骤启动一个容器,制作镜像,然后基于该镜像再不带命令地再启一个容器,在另一个窗口中,使用docker commit 将其保存为新的镜像,并上传到docker hub中。问题解决。

 

 

参考链接

  • http://developers.redhat.com/blog/2016/03/09/more-about-docker-images-size/ 
  • http://developers.redhat.com/blog/2016/02/24/10-things-to-avoid-in-docker-containers/
  • http://techknowblogs.blogspot.in/2017/12/docker-host-os-guest-os-base-image-etc.html
  • http://www.floydhilton.com/docker/2017/03/31/Docker-ContainerHost-vs-ContainerOS-Linux-Windows.html
  • http://crunchtools.com/comparison-linux-container-images/
 

你可能感兴趣的:(理解Docker(2):Docker 镜像)