Docker 原理篇(四)Docker 镜像和容器

前言

此篇博文是笔者所总结的 Docker 系列之一;

本文为作者的原创作品,转载需注明出处;

Docker 镜像概述

在传统环境中,软件在运行之前也需要经过代码开发 -> 运行环境准备 -> 安装软件 -> 运行软件等环节,开发人员往往要负责准备测试环境,系统架构师往往需要准备生产环境,安装和配置繁杂的软件,以至于在大规模应用部署过程中,开发人员和系统架构师的过多精力都消耗在了测试和生产环境的部署上;而通过 Docker 镜像,将中间两个步骤,准备运行环境,安装软件的过程给封装成了镜像。这样,开发人员就可以专注于开发工作,系统架构师就可以专注于系统扩展,高可用,安全性等等的本职工作上了。

生成 Docker 镜像

Docker 的镜像生成常用的有两种方式

  1. 创建一个容器,运行若干命令,再使用 docker commit 来生成一个新的镜像。不建议使用这种方案。
  2. 创建一个 Dockerfile 然后再使用 docker build 来创建一个镜像。大多人会使用 Dockerfile 来创建镜像。

Docker commit 生成镜像

Docker build 使用 Dockerfile 生成镜像

准备一份 Dockerfile

创建一份 Dockerfile Dockerfile

1
root@ubuntu:~# touch Dockerfile

准备如下内容,

1
2
3
4
5
6
7
8
9
10
11
FROM ubuntu:14.04

MAINTAINER mac "mac@mac.com"

RUN apt-get update

RUN apt-get -y install ntp

EXPOSE 5555

CMD ["/usr/sbin/ntpd"]

这是一个非常简单的Dockerfile,试验的目的是基于 Ubuntu 14.04 基础镜像安装 ntp 基础软件,从而生成一个新的镜像

通过 docker build 进行构建的过程

特别注意,运行 docker build 命令的时候最后要接一个.符号;
docker build -t dockerfile . -t 表示给镜像一个 REPOSITORY 和 TAG 名称,如果只输入 REPOSITORY,那么 TAG 默认为 lastest。-f 表示指定构建文件,如果不使用 -f,那么默认在当前目录使用名为Dockfile的构建文件。更多 docker build 的解释参考 https://docs.docker.com/engine/reference/builder/

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
root@ubuntu:~# ls
Dockerfile
root@ubuntu:~# docker build -t dockerfile .
Sending build context to Docker daemon 9.728 kB
Step 1 : FROM ubuntu:14.04
14.04: Pulling from library/ubuntu
16da43b30d89: Pull complete
1840843dafed: Pull complete
91246eb75b7d: Pull complete
7faa681b41d7: Pull complete
97b84c64d426: Pull complete
Digest: sha256:881befbe6f54c1e85029fe3a11554342bf765a0849600ecb8fa2f922798b4925
Status: Downloaded newer image for ubuntu:14.04
---> 3f755ca42730
Step 2 : MAINTAINER mac "mac@mac.com"
---> Running in f0765b7bdf80
---> 0d4795488869
Removing intermediate container f0765b7bdf80
Step 3 : RUN apt-get update
---> Running in 0e86dcebf6ba
Ign http://archive.ubuntu.com trusty InRelease
Get:1 http://archive.ubuntu.com trusty-updates InRelease [65.9 kB]
Get:2 http://archive.ubuntu.com trusty-security InRelease [65.9 kB]
...
Get:22 http://archive.ubuntu.com trusty/universe amd64 Packages [7589 kB]
Fetched 22.3 MB in 2min 7s (175 kB/s)
Reading package lists...
---> 413240c5a24e
Removing intermediate container 0e86dcebf6ba
Step 4 : RUN apt-get -y install ntp
---> Running in 70c3eac319ae
Reading package lists...
Building dependency tree...
...
invoke-rc.d: policy-rc.d denied execution of start.
Processing triggers for libc-bin (2.19-0ubuntu6.9) ...
Processing triggers for ureadahead (0.100.0-16) ...
---> a22a846114ed
Removing intermediate container 70c3eac319ae
Step 5 : EXPOSE 5555
---> Running in d749f2d262e8
---> af92f237e0ce
Removing intermediate container d749f2d262e8
Step 6 : CMD /usr/sbin/ntpd
---> Running in e351a8284085
---> a1ed5e6093dc
Removing intermediate container e351a8284085
Successfully built a1ed5e6093dc

从日志中可以看到,每一行命令都被当做一个Step执行,下面我们根据每个步骤来逐一分析

Step 1: FROM ubuntu:14.04

获取基础镜像 ubuntu:14.04. Docker 首先会在本地查找,如果找到了,则直接利用;否则从 Docker registry 中下载

1
2
3
4
5
6
7
8
9
10
Step 1 : FROM ubuntu:14.04
14.04: Pulling from library/ubuntu
16da43b30d89: Pull complete
1840843dafed: Pull complete
91246eb75b7d: Pull complete
7faa681b41d7: Pull complete
97b84c64d426: Pull complete
Digest: sha256:881befbe6f54c1e85029fe3a11554342bf765a0849600ecb8fa2f922798b4925
Status: Downloaded newer image for ubuntu:14.04
---> 3f755ca42730

这里下载了一个新的镜像,ubuntu:14.04,image id 3f755ca42730,使用命令docker images可以看到 docker 加载了该镜像。

1
2
3
4
5
6
7
mac@ubuntu:~$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
dockerfile latest a1ed5e6093dc About an hour ago 213 MB
new_web31 latest 81a66be74de5 2 hours ago 323.7 MB
ubuntu 14.04 3f755ca42730 4 days ago 188 MB
hello-world latest c54a2cc56cbb 5 months ago 1.848 kB
training/webapp latest 6fae60ef3446 19 months ago 348.8 MB

可见,该镜像由 5 层只读镜像所构成。

1
2
3
4
5
6
mac@ubuntu:~$ docker inspect --format={{'.RootFS.Layers'}} 3f755ca42730
[sha256:bc224b1b676d12be2a49f99778dda08b90d22747244d0a0afcdf4cfeb7db5d89
sha256:53edc9780c07c9c8074f2d05064df0b11bbed9a69082fb613e690bc2a290983d
sha256:738d3f35b582973d1fc86ff87edb9b84c90d6a33c7901a7d670278eecc2e6ad9
sha256:4375cecd293e9455903aeb7b5de2287544f7cefa482eda989e73ad0ca908c51b
sha256:4fcb79d431cc1198f07561d015f88bb492fe399515a21c5636cfb89064423d79]

3f755ca42730将作为基础镜像构建自定义的镜像。

Step 2: MAINTAINER mac “mac@mac.com

此步是设置作者的相关信息

1
2
3
4
Step 2 : MAINTAINER mac "mac@mac.com"
---> Running in f0765b7bdf80
---> 0d4795488869
Removing intermediate container f0765b7bdf80

Docker 会创建一个临时容器f0765b7bdf80,然后运行 MAINTAINER 命令,再使用 docker commit 生成新的镜像0d4795488869,最后删除临时容器f0765b7bdf80,然后运行。
可惜我没能打印出临时容器 f0765b7bdf80 的日志,下面是别人打印出来的另外一个相同步骤的临时容器 1be8f33c1846 的相关日志

1
2
3
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)

从日志中,我们可以清晰的看到,该临时容器的createcommitdestory的整个生命周期的过程。而,正是通过该临时的容器f0765b7bdf80,Docker 通过命令docker commit生成了一个新的镜像0d4795488869,而该镜像,只是一个中间过度的镜像, 用于保存镜像的中间状态;需要注意的是,该中间镜像0d4795488869是不能通过命令docker images查找得到的,必须使用docker images -a

1
2
3
4
5
6
7
8
9
10
11
mac@ubuntu:~$ docker images -a
REPOSITORY TAG IMAGE ID CREATED SIZE
dockerfile latest a1ed5e6093dc About an hour ago 213 MB
<none> <none> a22a846114ed About an hour ago 213 MB
<none> <none> af92f237e0ce About an hour ago 213 MB
<none> <none> 413240c5a24e About an hour ago 210.3 MB
<none> <none> 0d4795488869 About an hour ago 188 MB
new_web31 latest 81a66be74de5 3 hours ago 323.7 MB
ubuntu 14.04 3f755ca42730 4 days ago 188 MB
hello-world latest c54a2cc56cbb 5 months ago 1.848 kB
training/webapp latest 6fae60ef3446 19 months ago 348.8 MB

可见,Docker 使用了一个 REPOSITORY 为 ,TAG 为 作为标识的中间镜像0d4795488869来保存中间的状态,有趣的是,你可以观察到它的大小与 ubuntu 基础镜像3f755ca42730一样,没有发生任何的变化,也就是说,该步骤并没有新增任何的文件,所以大小一致。我们可以通过docker inspect来验证

1
2
3
4
5
6
docker inspect --format={{'.RootFS.Layers'}} 0d4795488869
[sha256:bc224b1b676d12be2a49f99778dda08b90d22747244d0a0afcdf4cfeb7db5d89
sha256:53edc9780c07c9c8074f2d05064df0b11bbed9a69082fb613e690bc2a290983d
sha256:738d3f35b582973d1fc86ff87edb9b84c90d6a33c7901a7d670278eecc2e6ad9
sha256:4375cecd293e9455903aeb7b5de2287544f7cefa482eda989e73ad0ca908c51b
sha256:4fcb79d431cc1198f07561d015f88bb492fe399515a21c5636cfb89064423d79]

可见,它的镜像的层次与 ubuntu 基础镜像3f755ca42730一致,表示,没有新增任何的文件。那问题是,这个步骤,在什么地方发生了改变呢?
好的,我们来对比Step 1Step 2所产生的镜像的异同,我们发现只是在配置文件上发生了细微的变化。

1
2
mac@ubuntu:~$ docker inspect --format={{'.ContainerConfig.Cmd'}} 3f755ca42730
[/bin/sh -c #(nop) CMD ["/bin/bash"]]

1
2
mac@ubuntu:~$ docker inspect --format={{'.ContainerConfig.Cmd'}} 0d4795488869
[/bin/sh -c #(nop) MAINTAINER mac "mac@mac.com"]

我们发现,Step 2在 Docker 的全局配置文件中添加了作者的身份信息。

Step 3: RUN apt-get update
1
2
3
4
5
6
7
8
9
10
11
Step 3 : RUN apt-get update
---> Running in 0e86dcebf6ba
Ign http://archive.ubuntu.com trusty InRelease
Get:1 http://archive.ubuntu.com trusty-updates InRelease [65.9 kB]
Get:2 http://archive.ubuntu.com trusty-security InRelease [65.9 kB]
...
Get:22 http://archive.ubuntu.com trusty/universe amd64 Packages [7589 kB]
Fetched 22.3 MB in 2min 7s (175 kB/s)
Reading package lists...
---> 413240c5a24e
Removing intermediate container 0e86dcebf6ba

可见,该步骤与Step 2类似,通过临时容器0e86dcebf6ba生成了一个中间镜像413240c5a24e,那么该中间镜像生成了什么呢?我们发现,它新增了一层镜像曾(既添加了一层 AUFS 文件层)

1
2
3
4
5
6
7
mac@ubuntu:~$ docker inspect --format={{'.RootFS.Layers'}} 413240c5a24e
[sha256:bc224b1b676d12be2a49f99778dda08b90d22747244d0a0afcdf4cfeb7db5d89
sha256:53edc9780c07c9c8074f2d05064df0b11bbed9a69082fb613e690bc2a290983d
sha256:738d3f35b582973d1fc86ff87edb9b84c90d6a33c7901a7d670278eecc2e6ad9
sha256:4375cecd293e9455903aeb7b5de2287544f7cefa482eda989e73ad0ca908c51b
sha256:4fcb79d431cc1198f07561d015f88bb492fe399515a21c5636cfb89064423d79
sha256:9f68e6599b8a35ec4a77e6e93232cced6c2bfee34477e48ca64e7bd1cd8303c8]

对比之前的Layers,我们发现,总共六层镜像,新生成的一层镜像为
sha256:9f68e6599b8a35ec4a77e6e93232cced6c2bfee34477e48ca64e7bd1cd8303c8

Step 4: RUN apt-get -y install ntp
1
2
3
4
5
6
7
8
9
10
Step 4 : RUN apt-get -y install ntp
---> Running in 70c3eac319ae
Reading package lists...
Building dependency tree...
...
invoke-rc.d: policy-rc.d denied execution of start.
Processing triggers for libc-bin (2.19-0ubuntu6.9) ...
Processing triggers for ureadahead (0.100.0-16) ...
---> a22a846114ed
Removing intermediate container 70c3eac319ae

与之前类似,通过中间容器70c3eac319ae,生成了一个中间镜像a22a846114ed

1
2
3
4
5
6
7
8
mac@ubuntu:~$ docker inspect --format={{'.RootFS.Layers'}} a22a846114ed
[sha256:bc224b1b676d12be2a49f99778dda08b90d22747244d0a0afcdf4cfeb7db5d89
sha256:53edc9780c07c9c8074f2d05064df0b11bbed9a69082fb613e690bc2a290983d
sha256:738d3f35b582973d1fc86ff87edb9b84c90d6a33c7901a7d670278eecc2e6ad9
sha256:4375cecd293e9455903aeb7b5de2287544f7cefa482eda989e73ad0ca908c51b
sha256:4fcb79d431cc1198f07561d015f88bb492fe399515a21c5636cfb89064423d79
sha256:9f68e6599b8a35ec4a77e6e93232cced6c2bfee34477e48ca64e7bd1cd8303c8
sha256:3ebffa962b3d30e5691762a4445559e5f1608ede1c22a60fb51a98587d74990a]

新生成的一层镜像为sha256:3ebffa962b3d30e5691762a4445559e5f1608ede1c22a60fb51a98587d74990a

Step 5: EXPOSE 5555
1
2
3
4
Step 5 : EXPOSE 5555
---> Running in d749f2d262e8
---> af92f237e0ce
Removing intermediate container d749f2d262e8

通过临时容器d749f2d262e8生成了一个中间镜像af92f237e0ce, 该镜像在元数据上做了些改动,在元数据上做了两处改动如下,

  1. ContainerConfig.Cmd

    1
    2
    3
    4
    5
    6
    7
    8
    "ContainerConfig": {
    ...
    "Cmd": [
    "/bin/sh",
    "-c",
    "#(nop) ",
    "EXPOSE 5555/tcp"
    ],
  2. Config.ExposedPorts

    1
    2
    3
    4
    5
    "Config": {
    ......
    "ExposedPorts": {
    "5555/tcp": {}
    },
Step 6 : CMD /usr/sbin/ntpd
1
2
3
4
5
Step 6 : CMD /usr/sbin/ntpd
---> Running in e351a8284085
---> a1ed5e6093dc
Removing intermediate container e351a8284085
Successfully built a1ed5e6093dc

通过中间容器e351a8284085,哈哈,生成了我千呼万唤始出来的最终镜像a1ed5e6093dc,同样,该镜像实际上只是改变了相关元数据

1
2
3
4
5
6
"Cmd": [
"/bin/sh",
"-c",
"#(nop) ",
"CMD [\"/usr/sbin/ntpd\"]"
]

1
2
mac@ubuntu:~$ docker images | grep a1ed5e6093dc
dockerfile latest a1ed5e6093dc 19 hours ago 213 MB

镜像的名称dockerfile:latest

总结
1
2
3
4
5
6
7
8
9
10
11
12
13
mac@ubuntu:~$ docker history a1ed5e6093dc
IMAGE CREATED CREATED BY SIZE COMMENT
a1ed5e6093dc 19 hours ago /bin/sh -c #(nop) CMD ["/usr/sbin/ntpd"] 0 B
af92f237e0ce 19 hours ago /bin/sh -c #(nop) EXPOSE 5555/tcp 0 B
a22a846114ed 19 hours ago /bin/sh -c apt-get -y install ntp 2.686 MB
413240c5a24e 19 hours ago /bin/sh -c apt-get update 22.32 MB
0d4795488869 19 hours ago /bin/sh -c #(nop) MAINTAINER sammy "sammy@sa 0 B
3f755ca42730 5 days ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0 B
<missing> 5 days ago /bin/sh -c mkdir -p /run/systemd && echo 'doc 7 B
<missing> 5 days ago /bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$/ 1.895 kB
<missing> 5 days ago /bin/sh -c rm -rf /var/lib/apt/lists/* 0 B
<missing> 5 days ago /bin/sh -c set -xe && echo '#!/bin/sh' > /u 194.6 kB
<missing> 5 days ago /bin/sh -c #(nop) ADD file:b2236d49147fe14d8d 187.8 MB
  1. 容器镜像包括元数据和文件系统,其中文件系统是指对基础镜像的文件系统的修改,元数据不影响文件系统,只是会影响容器的配置
  2. 每个步骤都会生成一个新的镜像,新的镜像与上一次的镜像相比,要么元数据有了变化,要么文件系统有了变化而多加了一层
  3. Docker 在需要执行指令时通过创建临时容器,再通过 docker commit 来生成中间镜像
  4. Docker 会将中间镜像都保存在缓存中,这样将来如果能直接使用的话就不需要再从头创建了。关于镜像缓存,请搜索相关文档。

Docker 容器

docker run

docker run <image> 会依赖指定的image启动一个新的容器,启动过程,

  1. 通过 namespaces 进行进程、网络、用户级别启动一个 PID=1 的进程,通过其可以 fork 出任意多的子进程,从而生成一个基于操作系统之上的虚拟机原型。
  2. 启用 cgroups 规则,限制该虚拟机的 CPU、内存、带宽使用率等
  3. 使用 AUFS 建立一层可写层,保存容器产生的新的数据等。

--rm: 我们通常使用该参数来运行一个容器,一旦容器退出,将会自动删除该容器,同事删除其产生的中间数据。

Docker 容器层

如图,非常形象的描述了容器层其实就是基于相同的基础镜像层之上的一层可写的 AUFS 文件层而已。

COW(Copy On Write) 写时复制

每一次docker run都会新生成一层中间镜像,既生成一层只读的 AUFS 文件层,所以,为了减少中间镜像层的层数,应该尽量将多个命令放置在同一个docker run中,比如,我们修改上述的 Dockerfile 文件

1
2
3
4
5
FROM ubuntu:14.04
MAINTAINER sammy "sammy@sammy.com"
RUN apt-get update && apt-get -y install ntp
EXPOSE 5555
CMD ["/usr/sbin/ntpd"]

我们将RUN apt-get updateRUN apt-get -y install ntp合并成一个docker run命令,那么,这样,我们就可以有效的减少一层镜像中间层

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

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

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

CONTAINER ID & NAMES

1
2
3
4
5
mac@ubuntu:~/docker9$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f94ea7b0c13c exposed_port "/bin/sh -c 'while tr" 42 minutes ago Up 42 minutes 0.0.0.0:8888->8888/tcp exposed_port_self_published
b0bee025fc87 exposed_port "/bin/sh -c 'while tr" 46 minutes ago Up 46 minutes 0.0.0.0:32770->8888/tcp exposed_port_published
6b852fd533ee exposed_port "/bin/sh -c 'while tr" About an hour ago Up About an hour 8888/tcp exposed_port_not_published
  • CONTAINER ID
    随机生成,且全局唯一。
  • NAMES
    如果不使用--name指定容器的名字,docker 将会随机生成一个名字;注意,这里的 NAMES 与 CONTAINER ID 是一一对应的,所以它也是全局唯一的

如何进入一个正在运行的容器并执行 bash 命令

先通过 docker ps 查看当前运行在 docker engine 上的 containers 有哪些,

1
2
3
4
5
mac@ubuntu:~/docker9$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f94ea7b0c13c exposed_port "/bin/sh -c 'while tr" 42 minutes ago Up 42 minutes 0.0.0.0:8888->8888/tcp exposed_port_self_published
b0bee025fc87 exposed_port "/bin/sh -c 'while tr" 46 minutes ago Up 46 minutes 0.0.0.0:32770->8888/tcp exposed_port_published
6b852fd533ee exposed_port "/bin/sh -c 'while tr" About an hour ago Up About an hour 8888/tcp exposed_port_not_published

可以看到,总共有三个 containers 在执行,我们以 CONTAINER ID b0bee025fc87 为例看看如何进入该容器并执行 bash 命令?

  • 通过 NAME 进入

    1
    2
    3
    mac@ubuntu:~/docker9$ docker exec -it exposed_port_published bash
    root@b0bee025fc87:/# exit
    exit
  • 通过 CONTAINER ID 进入

    1
    2
    3
    mac@ubuntu:~/docker9$ docker exec -it b0bee025fc87 bash
    root@b0bee025fc87:/# exit
    exit

Dockerfile

Docker build 官方文档就构建 Docker 镜像做了比较全面的阐述,这里就一些我比较关心的命令做深入的分析和总结。

ADD 和 COPY

ADD

1
2
3
4
5
6
FROM ubuntu:14.04
MAINTAINER mac Liu <mac.liu@unknow.com>
ADD localdir /root/dockerdir
ADD localdir/foo.tar.gz /root/dockerdir
ADD http://foo.com/bar.go /root/dockerdir
CMD ["/bin/bash"]
  1. 将宿主机上的文件拷贝到容器,如果是压缩文件,将自动解压
    • ADD localdir /root/dockerdir
      将宿主机上的localdir路径中的文件全部拷贝到 Docker 容器中的/root/dockerdir
      注意两点,
      • localdir必须和Dockerfile在同一个目录中,否则会找不到指定的路径localdir
      • dockerdir如果在/root路径中没有创建,将会自动创建
    • ADD localdir/foo.tar.gz /root/dockerdir
      注意,拷贝过程中将会自动解压,如果不希望被解压,使用COPY
  2. 将网络上的文件拷贝到容器

测试过程,

我在包含Dockerfile的文件夹内创建了localdir/helloworld文件,将其拷贝到/root/dockerdir中,来进行测试。

  • 创建镜像,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    Sending build context to Docker daemon 3.584 kB
    Step 1 : FROM ubuntu:14.04
    ---> 3f755ca42730
    Step 2 : MAINTAINER mac Liu <mac.liu@unknow.com>
    ---> Running in 29628aeed6f0
    ---> a2174fd3f262
    Removing intermediate container 29628aeed6f0
    Step 3 : ADD localdir /root/dockerdir
    ---> c26364f10cc5
    Removing intermediate container 1e9a3528dc32
    Step 4 : CMD /bin/bash
    ---> Running in 116514d4bd1c
    ---> 58239e98659e
    Removing intermediate container 116514d4bd1c
    Successfully built 58239e98659e
  • 检查容器

    1
    2
    3
    4
    mac@ubuntu:~/docker1$ docker run -it test_add /bin/bash
    root@b734658f162a:/# cd /root/dockerdir/
    root@b734658f162a:~/dockerdir# ls
    helloworld

COPY

使用的方式和ADD基本一致,只是,如果是压缩文件的时候,如果不希望在拷贝过程中自动被解压,那么使用COPY

CMD 和 ENTRYPOINT

CMD

官网解释,

The main purpose of a CMD is to provide defaults for an executing container.
为一个执行中的容器提供默认的执行命令;

CMD 的三种用法如下,
1
2
3
CMD ["executable","param1","param2"] (exec form, this is the preferred form) 
CMD ["param1","param2"] (as default parameters to ENTRYPOINT)
CMD command param1 param2 (shell form)
  • 第一种用法:运行一个可执行的文件并提供参数。
  • 第二种用法:为ENTRYPOINT指定参数。
  • 第三种用法(shell form):是以/bin/sh -c的方法执行的命令。
CMD 用例剖析

如果在Dockerfile中指定CMD [“/bin/echo”, “this is a echo test ”],那么在 build 后,运行容器,docker run ec,假设镜像的名称为 ec,那么输出this is a echo test。那么让我们来构建这个测试用例,

  1. 创建 Dockerfile

    1
    2
    3
    FROM ubuntu:14.04
    MAINTAINER mac Liu <mac.liu@unknow.com>
    CMD ["/bin/echo", "this is a echo test"]
  2. 创建镜像

    1
    mac@ubuntu:~/docker2$ docker build -t ec .
  3. 测试

    1
    2
    mac@ubuntu:~/docker2$ docker run ec
    this is a echo test

    可见,CMD ["/bin/echo", "this is a echo test"]指定的就是 docker 容器启动后,默认执行的命令;

注意事项,
  1. 只有一个CMD指令生效,如果指定了多个CMD指令,那么只有最后一个生效
  2. 如果docker run中指定了运行参数,那么会把CMD命令给覆盖掉

    1
    2
    mac@ubuntu:~/docker2$ docker run -it ec /bin/bash
    root@642b4bb0a99e:/#

    可见,这里使用/bin/bash覆盖了CMD指定的命令/bin/echo

ENTRYPOINT

官网解释

An ENTRYPOINT allows you to configure a container that will run as an executable,它让你的容器表现得像一个可执行的程序一样。

可执行的程序?

下面举例来说明吧,

1
2
3
FROM ubuntu:14.04
MAINTAINER mac <mac@unknow.com>
ENTRYPOINT ["/bin/echo"]

1
mac@ubuntu:~/docker3$ docker build -t echo .

将容器当做可执行程序一样

1
2
mac@ubuntu:~/docker3$ docker run -it echo "hello world"
hello world

"hello world"被解析成ENTRYPOINT指定的命令/bin/echo的执行参数,所以,直接打印出字符串“hello world”。

可能你会有疑问,难道CMD不可以做一样的事情吗?那我们来试试吧,

1
2
3
FROM ubuntu:14.04
MAINTAINER mac Liu <mac.liu@unknow.com>
CMD ["/bin/echo"]

1
mac@ubuntu:~/docker2$ docker build -t echo_by_cmd .
1
2
3
mac@ubuntu:~/docker2$ docker run -it echo_by_cmd "hello world"
docker: Error response from daemon: invalid header field value "oci runtime error: container_linux.go:247: starting container process caused \"exec: \\\"hello world\\\": executable file not found in $PATH\"\n".
mac@ubuntu:~/docker2$ docker run -it ec

很明显,这里将"hello world"解析为执行命令,用来覆盖cmd指定的命令,所以,无法找到"hello world"这样一个命令,所以会报错!

ENTRYPOINT 的两种写法
1
2
ENTRYPOINT ["executable", "param1", "param2"] (the preferred exec form) 
ENTRYPOINT command param1 param2 (shell form)
  1. ENTRYPOINT [“executable”, “param1”, “param2”] (the preferred exec form)
    可执行的程序?使用的是第一种的exec的写法,也是官方所推荐的写法,这里就不再继续阐述了;
  2. ENTRYPOINT command param1 param2 (shell form)
    将会默认使用/bin/sh -c来执行command

    • 例一

      1
      2
      3
      FROM ubuntu:14.04
      MAINTAINER mac Liu <mac.liu@unknow.com>
      ENTRYPOINT ps
      1
      mac@ubuntu:~/docker4$ docker build -t ps .
      1
      2
      3
      4
      mac@ubuntu:~/docker4$ docker run -it ps
      PID TTY TIME CMD
      1 ? 00:00:00 sh
      5 ? 00:00:00 ps

      这里执行的正好是/bin/sh -c ps,所以可以看到 PID = 1 的 CMD 为 sh,而 ps 正好是其子进程。

    • 例二,CMDENTRYPOINT同时使用shell的方式

      1
      2
      3
      4
      FROM ubuntu:14.04
      MAINTAINER mac Liu <mac.liu@unknow.com>
      CMD top
      ENTRYPOINT ps
      1
      mac@ubuntu:~/docker5$ docker build -t both .
      1
      2
      3
      4
      mac@ubuntu:~/docker5$ docker run -it both
      PID TTY TIME CMD
      1 ? 00:00:00 sh
      5 ? 00:00:00 ps

      Docker 容器中执行的命令实际上是/bin/sh -c ps /bin/sh -c top,也就相当于执行的是ps命令,

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      mac@ubuntu:~/docker5$ /bin/sh -c ps /bin/sh -c top
      PID TTY TIME CMD
      23176 pts/3 00:00:00 bash
      23322 pts/3 00:00:00 sh
      23323 pts/3 00:00:00 bash
      26840 pts/3 00:00:00 sh
      26841 pts/3 00:00:00 ps
      mac@ubuntu:~/docker5$ /bin/sh -c ps
      PID TTY TIME CMD
      23176 pts/3 00:00:00 bash
      23322 pts/3 00:00:00 sh
      23323 pts/3 00:00:00 bash
      26842 pts/3 00:00:00 sh
      26843 pts/3 00:00:00 ps

      可见,docker 容器先执行 ENTRYPOINT 的 shell command,然后再执行 CMD 的 shell command

    • 例三、CMD 作为 ENTRYPOINT 的参数

      1
      2
      3
      4
      FROM ubuntu:14.04
      MAINTAINER comedsh <comedsh@unknow.com>
      CMD ["-n", "10"]
      ENTRYPOINT top

      CMD 指定的参数将作为 top 的执行参数

  3. 最佳实践是,ENTRYPOINT 指定不可变的参数,使用 CMD 指定可变参数

    1
    2
    3
    FROM ubuntu:14.10  
    ENTRYPOINT ["top", "-b"]
    CMD ["-c"]

    注意,这种方式必须使用EXEC的方式,如果 ENTRYPOINT 使用的是SHELL COMMAND的方式,ENTRYPOINT 的命令指定的参数将会同时覆盖 docker run 里面所指定的参数和 CMD 所指定的参数。

WORKDIR

设置CMD, ENTRYPOINT, RUN命令的当前执行路径;

  1. RUN

    1
    2
    3
    4
    5
    6
    mac@ubuntu:~/docker8$ docker run --name temp -w /etc/fonts ubuntu:14.04 /bin/pwd
    /etc/fonts
    mac@ubuntu:~/docker8$ docker rm temp
    temp
    mac@ubuntu:~/docker8$ docker run --name temp --workdir /etc/fonts ubuntu:14.04 /bin/pwd
    /etc/fonts
  2. CMD / ENTRYPOINT

    1
    2
    3
    FROM ubuntu:14.04
    WORKDIR /etc/fonts
    ENTRYPOINT pwd
    1
    mac@ubuntu:~/docker9$ docker build -t workdir .
    1
    2
    mac@ubuntu:~/docker9$ docker run --name workdir_c workdir
    /etc/fonts

ENV

设置环境变量
语法

1
2
ENV <key> <value>
ENV <key>=<value> ...

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

生成镜像以后,其元数据如下,

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

对应的容器也能看到对应的环境变量

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

进入容器后,

1
2
3
4
root@ba460e0e9dc4:/# echo $abc
1
root@ba460e0e9dc4:/# echo $def
2

EXPOSE,以及 EXPOSE 与 docker run -p -P 之间的关系

EXPOSE 只是标记某个端口需要暴露出来

EXPOSE仅仅只是标记某个端口需要被暴露出来;而并不是说,只要构建成功,容器运行,该端口就被PUBLISH出来,并且可以使用了;(备注:PUBLISH这里的意思就是,把容器EXPOSE的端口与主机的某个端口进行映射,这样,就可以通过访问主机的端口访问到 Docker 的服务了。)
举个例子,

1
2
3
4
FROM ubuntu:14.04
MAINTAINER mac <mac@unknow.com>
EXPOSE 8888
CMD while true; do echo 'hello world' | nc -l -p 8888; done

1
mac@ubuntu:~/docker7$ docker build -t exposed_port .

直接运行容器exposed_port_not_published

1
2
mac@ubuntu:~/docker7$ docker run -d --name exposed_port_not_published exposed_port
6b852fd533ee1433309a155ca111ff22a49fe46dd3c1a337a907c11c8ecff726

1
2
3
mac@ubuntu:~/docker7$ docker ps 
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6b852fd533ee exposed_port "/bin/sh -c 'while tr" 8 seconds ago Up 7 seconds 8888/tcp exposed_port_not_published
1
2
3
4
5
6
7
8
9
10
11
12
mac@ubuntu:~/docker7$ docker inspect exposed_port_not_published
......
"NetworkSettings": {
"Bridge": "",
"SandboxID": "3eb3215f6799010854d7ac9e68ab3d3fecaf0440bb3f1d341a655ec26ac266df",
"HairpinMode": false,
"LinkLocalIPv6Address": "",
"LinkLocalIPv6PrefixLen": 0,
"Ports": {
"8888/tcp": null
},
......

端口并没有与主机的端口进行任何的映射,可见EXPOSE仅仅只是标识一个port 8888需要被PUBLISH出来,并不会在容器启动的时候PUBLISH

使用 docker run -P 使其暴露出来

1
2
mac@ubuntu:~/docker7$ docker run -d --name exposed_port_published -P exposed_port
b0bee025fc87643cbda1375ab56f074724f0ad6eac9047564983b48404865b03
1
2
3
4
mac@ubuntu:~/docker7$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b0bee025fc87 exposed_port "/bin/sh -c 'while tr" 14 seconds ago Up 14 seconds 0.0.0.0:32770->8888/tcp exposed_port_published
6b852fd533ee exposed_port "/bin/sh -c 'while tr" 34 minutes ago Up 34 minutes 8888/tcp exposed_port_not_published

可见主机使用一个随机端口 32770 与容器的 8888 端口进行了映射,0.0.0.0:32770->8888/tcp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"NetworkSettings": {
"Bridge": "",
"SandboxID": "204db312c5cbd1813e468ca1d50a3b7ff3283d67d1bd07fa2d44d0e254c92e12",
"HairpinMode": false,
"LinkLocalIPv6Address": "",
"LinkLocalIPv6PrefixLen": 0,
"Ports": {
"8888/tcp": [
{
"HostIp": "0.0.0.0",
"HostPort": "32770"
}
]
},
1
2
3
4
5
6
mac@ubuntu:~/docker7$ telnet 0.0.0.0 32770
Trying 0.0.0.0...
Connected to 0.0.0.0.
Escape character is '^]'.
hello world
Connection closed by foreign host.

使用 docker run -p 自定义暴露方式

1
2
mac@ubuntu:~/docker7$ docker run -d --name exposed_port_self_published -p 8888:8888 exposed_port
f94ea7b0c13cfe77bdea9ce45044c65ca728943719c5e83782f3879cf0c37b5e
1
2
3
4
5
mac@ubuntu:~/docker7$ docker ps 
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f94ea7b0c13c exposed_port "/bin/sh -c 'while tr" 5 seconds ago Up 4 seconds 0.0.0.0:8888->8888/tcp exposed_port_self_published
b0bee025fc87 exposed_port "/bin/sh -c 'while tr" 3 minutes ago Up 3 minutes 0.0.0.0:32770->8888/tcp exposed_port_published
6b852fd533ee exposed_port "/bin/sh -c 'while tr" 38 minutes ago Up 38 minutes 8888/tcp exposed_port_not_published

0.0.0.0:8888->8888/tcp,可见是按照自定义的方式进行的映射

1
2
3
4
5
6
mac@ubuntu:~/docker7$ 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.

USER

语法

1
2
3
# Usage: USER [UID|USERNAME]
USER 751
USER mac

默认用户

Dockerfile 中执行 RUN 命令所使用的用户,默认是继承自基础镜像中的用户,例如FROM ubuntu:14.04,那么默认使用的是镜像ubuntu:14.04中的用户;当然,如果你使用的基础镜像是使用的非 root 用户,那么在 Dockerfile 构建过程中使用的也就是非 root 用户

自定义用户

如果你需要新建一个用户来执行 Docker 容器,可以先创建RUN groupadd -r macgroup && useradd -r -g macgroup mac

举例,使用mac用户来执行top命令

1
2
3
4
FROM ubuntu:14.04
RUN groupadd -r macgroup && useradd -r -g macgroup mac
USER mac
ENTRYPOINT ["/usr/bin/top","-bcn","1"]

1
mac@ubuntu:~/docker8$ docker build -t spec_user .
1
2
3
4
5
6
7
8
9
mac@ubuntu:~/docker8$ docker run --name spec_user_c spec_user
top - 09:33:33 up 12:42, 0 users, load average: 0.00, 0.02, 0.00
Tasks: 1 total, 1 running, 0 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.2 us, 0.2 sy, 0.0 ni, 98.6 id, 0.9 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem: 757612 total, 667984 used, 89628 free, 95068 buffers
KiB Swap: 786428 total, 12580 used, 773848 free. 284956 cached Mem

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1 mac 20 0 19740 2180 1928 R 0.0 0.3 0:00.02 /usr/bin/to+

可以看到,执行的用户是mac

MAINTAINER

设置镜像的作者

1
2
# Usage: MAINTAINER [name]
MAINTAINER authors_name

RUN

由前面的通过 docker build 进行构建的分析可知,RUN命令在构建过程中的临时容器中执行相关的 shell 命令;Docker 将改动保存到当前容器中,随后 commit 生成一个中间镜像。

VOLUMN

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

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

HEALTHCHECK

定期检查容器的运行状态,语法HEALTHCHECK [OPTIONS] CMD command,举例

1
2
3
4
5
6
7
FROM ubuntu:14.04
MAINTAINER mac <comedsh.mac@unknow.com>
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

HEALTHCHECK --interval=10s --timeout=2s CMD curl -f http://localhost:8888/ || exit 1 每隔10秒执行一次CMD命令,CMD通过curl执行 HTTP 操作来检测 docker 的8888服务是否正常。

1
mac@ubuntu:~/docker10$ docker build -t checkhealth .
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mac@ubuntu:~/docker10$ docker run --name checkhealth_c checkhealth
GET / HTTP/1.1
User-Agent: curl/7.35.0
Host: localhost:8888
Accept: */*

GET / HTTP/1.1
User-Agent: curl/7.35.0
Host: localhost:8888
Accept: */*

GET / HTTP/1.1
User-Agent: curl/7.35.0
Host: localhost:8888
Accept: */*

可以看到,每隔 10 秒,通过HEALTHCHECK输出curl的执行结果,表示docker所暴露的服务是正常的。同样,我们可以通过docker ps来查看当前容器的健康状态

1
2
3
4
5
6
7
8
9
10
11
12
mac@ubuntu:~$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
2ef5d131256c checkhealth "/bin/sh -c 'while tr" 49 seconds ago Up 48 seconds (healthy) 8888/tcp checkhealth_c
f94ea7b0c13c exposed_port "/bin/sh -c 'while tr" 18 hours ago Up 18 hours 0.0.0.0:8888->8888/tcp exposed_port_self_published
mac@ubuntu:~$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
acef582a8636 checkhealth "/bin/sh -c 'while tr" About a minute ago Up About a minute (healthy) 8888/tcp checkhealth_c
f94ea7b0c13c exposed_port "/bin/sh -c 'while tr" 18 hours ago Up 18 hours 0.0.0.0:8888->8888/tcp exposed_port_self_published
mac@ubuntu:~$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
acef582a8636 checkhealth "/bin/sh -c 'while tr" About a minute ago Up About a minute (healthy) 8888/tcp checkhealth_c
f94ea7b0c13c exposed_port "/bin/sh -c 'while tr" 18 hours ago Up 18 hours 0.0.0.0:8888->8888/tcp exposed_port_self_published

STATUS上可以看到docker当前的健康状态。