Dockerfile-Notebook

关于Docker有太多太多值得去说,以我目前的水平,还无法在一个完全系统化的角度去解读。但Docker还是要用的,故姑且先把Dockerfile的格式小小地记录一下。

Dockerfile

长话短说,Dockerfile即用于告诉Docker如何构建容器的配置文件,其中记录了容器的各种配置信息,包括容器的名称、容器的根目录、容器的环境变量、容器的镜像、容器的端口映射等。

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

#指定基础镜像
FROM basic image

#添加标签
LABEL key=value

#ENV 设置一些环境变量
ENV <key>=<value> ...

#复制文件
COPY src(which in build context) dest(which in container)

#执行类指令 RUN CMD ENTRYPOINT

#容器构建时执行的指令,不过要注意每个RUN都将给镜像增加一层
RUN command

#容器运行时的指令,区别在于,对于docker run 后的command,CMD将被覆盖之;而在ENTRYPOINT中,command则是作为参数,追加到ENTRYPOINT尾部。
CMD command
ENTRYPOINT command

#挂载匿名数据卷——即在/path/to/volume位置挂载一个匿名数据卷,对其读写将不影响镜像的永久存储。当然,可以使用执行时参数docker run -v mydata:/path/to/volume进行覆盖
VOLUME /path/to/volume

#定义容器默认需要使用的port——即将在docker run指令中使用 -P时,随机分配一个host端口,映射到容器端口
EXPOSE <port> [<port>/<protocol>...]

#指定该指令之后的执行类指令所处的上下文,包括RUN、CMD、ENTRYPOINT。
WORKDIR /path/to/workdir

#指定该指令之后的执行类指令所使用的角色,包括RUN、CMD、ENTRYPOINT。
USER user

#指定该指令之后的执行类指令所使用的shell,包括RUN、CMD、ENTRYPOINT。
SHELL [shell]

ENTRYPOINT 与 CMD

对于没构建过镜像,仅仅只是下载封装好的镜像,docker run执行的初心者而言,容器的行为与镜像似乎是绑定的,所谓用什么镜像、做什么事。这确乎是事实,因为镜像的搭建目的也就在于为特定应用的执行提供环境,但是正确地认识镜像与容器还是十分有必要的。

我们知道,容器的实现其实是基于container-shim进程为其所服务的主进程提供一个虚拟的环境,你需要CPU执行,container-shim向OS申请时隙,但是确实执行容器内的进程;需要内存,container-shim向OS申请,等等。容器的生命周期也就围绕着其内部的主进程展开,一旦主进程结束,容器也就不再活跃。那么,这个进程是如何决定、设定的呢?

最简单的形式

1
docker run image "command"

即在生成容器的指令中在镜像后追加。

当然,这样的指令较为灵活,但是导致使用起来不太便利,因为许多应用的启动指令涉及到不少参数,且往往是固定不变的,反复输入不得不说是一种折磨。

CMD

在编写dockerfile时,我们认识到,可以使用CMD指令设置镜像的初始指令。

1
2
3
4
5
CMD ["bash","-c","command"]

or

CMD command

这样配置后,在使用docker run启动容器时,若不设置command,就将默认使用dockerfile中CMD后的指令;当然,若是需要执行与默认不同的指令,还是需要全量输入。

当然,这还远远不足。容器的使用场景,往往是一个固定的应用,但又需要根据具体使用环境微调参数,无论是全量的输入,还是固定的默认,都难以满足要求。

那么,有没有在利用默认机制的同时,又保留一定灵活性的方案呢?

ENTRYPOINT

解决方案就是ENTRYPOINT

1
2
3
4
5
6
7

ENTRYPOINT entrypoint

or

ENTRYPOINT ["entrypoint"]

ENTRYPOINT同CMD一样,其目的在于为镜像构建容器时指定默认初始指令,但是,ENTRYPOINT所执行的指令,不仅仅是我们为其设定的entrypoint,而是entrypoint+command,即我们在docker run中指定的command,将被追加在entrypoint后,这样,我们就可以将指令固定部分放在entrypoint中,而在构建容器时在决定参数了,非常方便。

值得一提,ENTRYPOINT与CMD可以同时使用,这种情况下,CMD的command将被作为默认参数使用;entrypoint也可以借由在docker run时使用–entrypoint重新指定。

题外话

容器追踪其主进程生命周期的方式很好的表现了其设计思想,但是,却也造成了许多不便。

许多时候,在在容器中运行主要指令前,我们需要对环境进行一定的调整,如果我们将调整的指令写入command,那么在容器执行调整后,就将结束,这显然不是我们想要的;我们也可以使用bash作为指令,再进入容器手动调整与启动,这当然可行,但对于自动化来说却是不可接受的;我们也可以将所有指令写入脚本,直接将脚本作为entrypoint,但这样又将失去灵活性。那么成熟的容器是怎么做的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ENTRYPOINT ["entrypoint.sh"]

CMD ["redis-server"]

entrypoint.sh
---
#!/bin/bash
set -e

... code ...

exec "$@"
---

观察redis可以发现,redis将entrypoint设置为entrypoint.sh脚本,在脚本前段执行环境初始化设置指令,最后,以exec “$@”结束。exec 指令将替换当前进程bash的代码,而去执行其后的程序,$@指代全部参数组成的列表,也就是说,容器的主进程bash在执行完初始化后,将以我们给入的参数为可执行程序,重置进程内容,此处我们往往给出程序的启动程序,如redis-server。

当然,我们呢又不得不面对全量拼写与固定默认的抉择了,这也算是一种妥协吧。