带你了解docker是什么----基础原理篇(一)
docker
容器是如何工作的?
docker的架构
docker的核心组件如下:
- docker的客户端:client。我们最常用的客户端就是docker命令。
- docker服务器:docker daemon ,一Linux后台服务的方式运行。运行在docker host 上,负责创建、运行、监控容器,构建、存储镜像。默认下,智能响应本地host的客户端的请求。
- docker镜像:image
- registry
- docker容器:container
组件之间的联系如下图
docker采用的是client/server的架构。客户端向服务器发送请求,服务器负责构建、运行和分发容器。
客户端和服务器可以运行在同一个host上。(我们电脑中一般使用的就是这种情况),当然客户端也可以通过socket与远程的服务器通信。
docker 镜像
常用命令
- docker images :查看镜像的信息
- docker run -it dockerID :运行指定ID的docker 镜像
- build :从 dockerfile 构建镜像
- tag:给镜像打tag
- pull : 从registry下载镜像
- push:将镜像上传到registry
- rmi:删除docker host 中的镜像
- search : 搜索docker hub 中的镜像
- images:显示镜像列表
- history:显示镜像构建历史
- commit :从容器创建新镜像
base 镜像
base镜像含义为:(1)不依赖其他镜像,从scratch构建;(2)其他镜像可以在该镜像基础上进行扩展。
通常情况下,能称为base镜像的是各种Linux发行版的docker镜像,如Ubuntu、centOS等
一般基础镜像会与我们实际的大小有所出入。比如一个centOS才200MB左右,当时平时我们按照一个centOS至少都有几个G。原因如下:
- Linux操作系统大致是由内核空间和用户空间组成。如下图,
- 内核指的是一个提供设备驱动、文件系统、进程管理、网络通信等功能的系统软件,但一个内核并不是一套完整的操作系统,它只是操作系统的核心。一些组织或厂商将Linux内核与各种软件和文档包装起来,并提供系统安装界面和系统配置、设定与管理工具,就构成了Linux的发行版本。
- 内核空间是kernel,Linux刚启动时会加载bootfs文件系统,之后bootfs会被卸载掉。用户空间的文件系统是rootfs,包含我们熟悉的 /dev 、 /proc、/bin等目录。如下图
- 对于base镜像来说,底层直接使用host的kernel,自己只需要提供rootfs就可以了。(所以这也就解释了按照docker的时候,对于Windows操作系统需要安装一下一个Linux的一个发行版)。而对于一个精简的OS,rootfs可以很小,只需要包括最基本的命令、工具和程序就可以了。
- 不同Linux发行版的区别主要就是rootfs,因此docker可以同时支持多种Linux镜像。同时,这也从侧面反映了主流的Linux发行版的内核是兼容的。什么说呢?比如我们在docker中有多个不同发行版的Linux,但是host中只有Ubuntu 的一个内核,那么其实,这些在docker中的Linux镜像使用的都是这个host中的内核。
镜像的分层结构
docker支持通过扩展现有镜像,创建新的镜像。
下图为一个具体的例子:
- 首先,这么做的一个好处就是共享资源。 比如,有多个镜像都从相同的base镜像构建而来的,那么docker host 只需要在磁盘上保存一份base镜像;同时内存中也需要加载一份base镜像就可以为所有容器服务了。而且,镜像的每一层都可以被共享。
- 多个容器共享一份基础镜像,那么在修改的时候就需要进行一些技巧:在docker中使用的就是创建一个容器层。 所有对容器的改动,无论添加、删除,还是修改文件都只会发生在容器层中。只有容器层是可写的,容器层下面的所有镜像都是只读的。
构建镜像
对于Docker用户来说,最好的情况是不需要自己创建镜像。几乎所有常用的数据库、中间件、应用软件等都有现成的Docker官方镜像或其他人和组织创建的镜像,我们只需要稍作配置就可以直接使用。
当然,有的情况下我们还是需要自己来创建镜像的。docker提供了两种创建镜像的方法:①docker commit 命令(命令行方式);②使用dockerfile 构建文件。一般使用的是后者,因此,这篇介绍的是后者。
dockerfile
dockerfile 是一个文本文件,记录了镜像创建的所有步骤。
下面列举例子来说明使用dockerfile来创建镜像的一些细节
一个dockerfile文件中的内容如下
运行docker build 命令构建镜像
root@ubuntu:~# pwd ①
/root
root@ubuntu:~# ls ②
Dockerfile
root@ubuntu:~# docker build -t ubuntu-with-vi-dockerfile . ③
Sending build context to Docker daemon 32.26 kB ④
Step 1 : FROM ubuntu ⑤
---> f753707788c5
Step 2 : RUN apt-get update && apt-get install -y vim ⑥
---> Running in 9f4d4166f7e3 ⑦
......
Setting up vim (2:7.4.1689-3ubuntu1.1) ...
---> 35ca89798937 ⑧
Removing intermediate container 9f4d4166f7e3 ⑨
Successfully built 35ca89798937 ⑩
root@ubuntu:~#
- 当前目录为 /root。
- Dockerfile准备就绪。
- 运行docker build命令,-t将新镜像命名为ubuntu-with-vi-dockerfile,命令末尾的.指明build context为当前目录。Docker默认会从build context中查找Dockerfile文件,我们也可以通过-f参数指定Dockerfile的位置。
- 从这步开始就是镜像真正的构建过程。首先Docker将build context中的所有文件发送给Docker daemon。build context为镜像构建提供所需要的文件或目录。
Dockerfile中的ADD、COPY等命令可以将build context中的文件添加到镜像。此例中,build context为当前目录 /root,该目录下的所有文件和子目录都会被发送给Docker daemon。
所以,使用build context就得小心了,不要将多余文件放到build context,特别不要把 /、/usr作为build context,否则构建过程会相当缓慢甚至失败。(也即是说我们创建指定镜像的时候,指定的目录有正确) - Step 1:执行FROM,将Ubuntu作为base镜像。
Ubuntu镜像ID为f753707788c5。 - Step 2:执行RUN,安装vim,具体步骤为 ⑦ ⑧ ⑨。
- 启动ID为9f4d4166f7e3的临时容器,在容器中通过apt-get安装vim。
- 安装成功后,将容器保存为镜像。(底层类似docker commit 命令)
- 删除临时容器。
10.镜像构建成功。
镜像的缓存特性
docker 会缓存已有镜像的镜像层,构建新镜像时候,如果某镜像层已经存在,就直接使用,无需重新创建。
dockerfile 中每一个指令都会依赖一个镜像层,上层是依赖于下层的。无论什么时候,只要某一层发生变化,其上面所有层的缓存都会失效。 简单来说,看下面的两个文件,内容都是一眼的就是指令的顺序有所不同。
对于第一个dockerfile文件在创建的时候,由于前面两行命令得到的镜像与之前创建的相同因此,就会使用缓存中的镜像。而对于第二个就需要重新下载了。(因此,有时候指令的顺序也是我们考虑的一个要点)
dockerfile 构建镜像过程
- 从base镜像运行一个容器。
- 执行一条指令,对容器做修改。
- 执行类似docker commit的操作,生成一个新的镜像层。
- Docker再基于刚刚提交的镜像运行一个新容器。
- 重复2~4步,直到Dockerfile中的所有指令执行完毕。
从这个过程可以看出,如果Dockerfile由于某种原因执行到某个指令失败了,我们也将能够得到前一个指令成功执行构建出的镜像,这对调试Dockerfile非常有帮助。我们可以运行最新的这个镜像定位指令失败的原因。
如下边的一个调试例子
dockerfile中的内容如下
执行docker build 后,结果如下
Dockerfile在执行第三步RUN指令时失败。我们可以利用第二步创建的镜像22d31cc52b3e进行调试,方法是通过docker run -it
启动镜像的一个容器,如图3-31所示。
手工执行RUN指令很容易定位失败的原因是busybox镜像中没有bash。虽然这是个极其简单的例子,但它很好地展示了调试Dockerfile的方法。
dockerfile 常用指令
- form : 指定base镜像
- copy : 将文件 从build context 复制到镜像。支持两种格式:copy src dest 与copy [“srt”,“dest”] .src 只能指定build context 中的文件或目录
- add : 与copy 类似,从build context 复制文件到镜像。不同的是,如果src是归档文件(tar、zip),文件会自动解压到dest
- env : 设置环境变量
- volume : 将文件或目录声明为volume。
- workdir : 为后面的run 、 cmp 、add或copy 指令设置镜像中的当前工作目录
- run :在容器中运行指定的命令
- cmd : 容器启动时运行指定的命令, 可以有多个cmd指令,但是只要最后一个生效。
- maintainer : 设置镜像的作者
下面以一个较为详细地使用 dockerfile 文件来创建镜像来详细说明各个命令的作用
构建镜像,细节如下图
- 构建前确保build context中存在需要的文件。
- 依次执行Dockerfile指令,完成构建。(其中下一行显示,将相关的文件传输到服务器)
运行容器,验证镜像内容,如图所示。
① 进入容器,当前目录即为WORKDIR。
如果WORKDIR不存在,Docker会自动为我们创建。
② WORKDIR中保存了我们希望的文件和目录:
目录bunch:由ADD指令从build context复制的归档文件bunch.tar.gz,已经自动解压。(这就像我们智能解压压缩包一样,得到的是一个压缩包名字的文件夹)
文件tmpfile1:由RUN指令创建。
文件tmpfile2:由COPY指令从build context复制。
③ ENV指令定义的环境变量已经生效。
dockerfile 支持以“#”开头的注释
RUN 、CMD和ENTRYPOINT
RUN、CMD和ENTRYPOINT这三个Dockerfile指令看上去很类似,很容易混淆。这里就详细讨论它们的区别。
简单地说:
(1)RUN:执行命令并创建新的镜像层,RUN经常用于安装软件包。
(2)CMD:设置容器启动后默认执行的命令及其参数,但CMD能够被docker run后面跟的命令行参数替换。
(3)ENTRYPOINT:配置容器启动时运行的命令。
一、 RUN
RUN指令通常用于安装应用和软件包。
RUN在当前镜像的顶部执行命令,并创建新的镜像层。dockerfile 中常常包含多个RUN指令
下面为使用RUN安装多个包的例子:
RUN apt-get update && apt-get install-y\bzr\cvs\git\mercurial\
subversion
apt-get update 和apt-get install 被放在一个RUN指令中执行,这样能够保证每次安装的是最新的包。如果apt-get install 在单独的RUN中执行,则会使用apt-get update 创建镜像层,而这一层可能是很久之前缓存的了。
二、 CMD
CMD 指令允许用户指定容器的默认执行命令。
此命令会在容器启动且docker run 没有指定其他命令时运行。
- 如果docker run 指定了其他命令,cmd 指定的默认命令将被忽略。
- 如果有多个CMD指令,则只有最后一个有效。
shell 和Excel 格式
我们可以用两种方式指定RUN、CMD和ENTRYPOINT要运行的命令。
一、Shell 格式:<instruction><command>
例如:
RUN apt-get install python3
CMD echo "Hello world"
ENTRYPOINT echo "Hello world"
当指令执行时,shell格式底层会调用 /bin/sh -c [command]
二、Excel 格式:<instruction> ["executable", "param1", "param2", ...]
例如:
RUN ["apt-get", "install", "python3"]
CMD ["/bin/echo", "Hello world"]
ENTRYPOINT ["/bin/echo", "Hello world"]
当指令执行时,会直接调用 [command],不会被shell解析。
CMD和ENTRYPOINT推荐使用Excel格式,因为这样指令可读性更强。RUN则两种方式都可以。
分发镜像
分发镜像指的是如何在多个docker host 上使用镜像。
有以下几种可用的方法:
- 用相同的dockerfile 在其他host 构建镜像。
- 将镜像上传到公共registry,host 直接下载使用。
- 搭建私有的registry供本地host 使用。