基本概念
操作系统分为内核和用户空间。对于 linux 而言,内核启动后,会挂载root 文件系统为其提供用户空间支持。而 Docker 镜像,就相当于是一个 root 文件系统。 Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序,库,资源,配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷,环境变量,用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。
分层存储
因为镜像包含操作系统完整的 root 文件系统,其体积往往是庞大的,因此在 docker 设计时,就充分利用Union FS的技术,将其设计为分层存储的架构。所以严格来说,镜像并非是一个像 ISO 那样的打包文件,镜像只是一个虚拟的概念,其实际体现并非由一个文件组成,而是由一组文件系统组成,或者说,由多层文件系统联合组成。 镜像构建时,会一层层构建,前一层是后一层的基础。每一层构建完就不会在发生改变,后一层上的任何改变只发生在自己这一层。 比如,删除前一层文件的操作,实际不是真的删除前一层的文件,而是仅在当前层标记为该文件已删除。在最终容器运行的时候,虽然不会看到这个文件,但是实际上该文件会一直跟随镜像。因此,在构建镜像的时候,需要额外小心,每一层尽量只包含该层需要添加的东西,任何额外的东西应该在该层构建结束前清理掉。 分层存储的特征还使得镜像的复用,定制变得更为容易。甚至可以用之前构建好的镜像作为基础层,然后进一步添加新的层,以定制自己所需的内容,构建新的镜像。
关于UnionFs
联合文件系统,它可以把多个目录(也叫分支)内容联合挂载到同一个目录下,而目录的物理位置是分开的。UnionFS 允许制度和可读写目录并存,就是说可同时删除和增加内容。
Docker容器
镜像和容器的关系,就像是面向对对象程序设计中的类和实例一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建,启动,停止,删除,暂停等。 容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的命名空间,因此容器可以拥有自己的 root 文件系统,自己的网络配置,自己的进程空间,甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样。这种特性使得容器封装的应用比直接在宿主运行更加安全。 每一个容器运行时,是以镜像为基础层,在其上创建一个当前容器的存储层,我们可以称这个为容器运行时读写而准备的存储层为容器存储层。 容器存储层的生命周期和容器一样,容器消亡时,容器存储层也随之消亡。因此,任何保存于容器存储层的信息读会随容器删除而丢失。
Docker Registry
镜像构建完成后,可以很容易的在当前宿主机上运行,但是,如果需要在其它服务器上使用这个镜像,我们需要一个集中的存储,分发镜像的服务,docker registry 就是这样的服务。 一个 docker registry 中可以包含多个仓库;每个仓库可以包含多个标签;每个标签对应一个镜像。 通常,一个仓库会包含同一个软件不同版本呢的镜像,而标签就常用于对应该软件的各个版本。我们可以通过\<仓库名>:\<标签>的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 latest 作为默认标签。
使用Docker镜像
镜像是 docker 的三大组件之一。 docker 运行容器前需要本地存在对应的镜像,如果本地不存在该镜像,docker 会从镜像仓库下载该镜像。
获取镜像
从docker镜像仓库获取镜像的命令是 docker pull 。其命令格式为:
具体的选项可以通过docker pull --help
命令看到。
- Docker 镜像仓库地址:地址的格式一般是<域名/ip>[:端口号]
。默认地址是docker hub(docker.io)。
- 仓库名:<用户名>/<软件名>
。对于 Docker Hub ,如果不给出用户名,则默认为library
,也就是官方镜像。
$ docker pull ubuntu:18.04
18.04: Pulling from library/ubuntu
92dc2a97ff99: Pull complete
be13a9d27eb8: Pull complete
c8299583700a: Pull complete
Digest: sha256:4bc3ae6596938cb0d9e5ac51a1152ec9dcac2a1c50829c74abd9c4361e321b26
Status: Downloaded newer image for ubuntu:18.04
docker.io/library/ubuntu:18.04
sha256
的摘要,以确保下载一致性。
列出镜像
列出已经下载下来的镜像,可以使用docker image ls
命令
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
redis latest 5f515359c7f8 5 days ago 183 MB
nginx latest 05a60462f8ba 5 days ago 181 MB
mongo 3.2 fe9198c04d62 5 days ago 342 MB
<none> <none> 00285df0df87 5 days ago 342 MB
ubuntu 18.04 329ed837d508 3 days ago 63.3MB
ubuntu bionic 329ed837d508 3 days ago 63.3MB
仓库名
,标签
,镜像ID
,创建时间
以及所占用空间
。
镜像体积
如果仔细观察,会注意到,这里表示的所占用空间和在Docker Hub上看到的镜像大小不同。这是因为Docker Hub中显示的大小是网络传输中更关心的流量大小。而docker image ls
显示的是镜像下载到本地后,展开的大小,准确说,是展开后的各层所占空间的总和,因为镜像到本地后,查看空间的时候,更关心的是本地磁盘空间占用的大小。
另外一个需要注意的问题是,docker image ls
列表中的镜像体积总和并非是所有镜像实际硬盘消耗。
Docker镜像是多层存储结构,并且可以继承,复用,因此不同镜像可能会因为相同的基础镜像,从而拥有共同的层。由于Docker使用Union FS,相同的层只需要保存一份即可,因此实际镜像硬盘占用空间很可能要比这个列表镜像大小的总和要小的多。
可以通过docker system df
命令来查看镜像,容器,数据卷所占用的空间。
$ docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 24 0 1.992GB 1.992GB (100%)
Containers 1 0 62.82MB 62.82MB (100%)
Local Volumes 9 0 652.2MB 652.2MB (100%)
Build Cache 0B 0B
虚悬镜像
上面的镜像列表中,还可以看到一个特殊的镜像,这个镜像既没有仓库名,也没有标签,均为<none>
。:
docker pull
新的版本后,镜像名被转移到新下载的镜像上,而旧的镜像上的名称被取消,从而成为了<none>
2. docker build
也同样可以导致这种现象
可以使用下面命令来查看这类镜像:
$ docker image ls -f dangling=true
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> 00285df0df87 5 days ago 342 MB
中间层镜像
为了加速镜像构建,重复利用资源,docker会利用中间层镜像。所以在使用一段时间后,可能会看到一些依赖的中间层镜像。默认的docker image ls
列表中只会显示顶层镜像,如果希望显示包括中间层镜像在内的所有镜像的话,需要加-a
参数
删除本地镜像
如果要删除本地的镜像,可以使用docker image rm
命令,其格式为:
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
centos latest 0584b3d2cf6d 3 weeks ago 196.5 MB
redis alpine 501ad78535f0 3 weeks ago 21.03 MB
docker latest cf693ec9b5c7 3 weeks ago 105.1 MB
nginx latest e43d811ce2f4 5 weeks ago 181.5 MB
用 ID,镜像名,摘要删除镜像
其中,<镜像>
可以是镜像短 ID
,镜像长 ID
,镜像名
或者镜像摘要
。
Untagged 和 Deleted
删除行为分为两类,一类是 Untagged
,另一类是 Deleted
。一个镜像可以有多个标签。
因此当我们使用上面命令删除镜像的时候,实际上实在要求删除某个标签的镜像。因为一个镜像可以对应多个标签,因此当我们删除了所指定的标签后,可能还有别的标签指向了这个镜像,如果是这种情况,那么 Delete
行为就不会发生。所以并非所有的docker image rm
都会产生删除镜像的行为,有可能仅仅是取消了某个标签而已。
当镜像所有的标签都被取消了,该镜像很可能会失去了存在的意义,因此会触发删除行为。镜像是多层存储结构,因此在删除的时候也是从上层向基础层方向依次进行判断删除。镜像的多层结构让镜像复用变得非常容易,因此很有可能某个其他镜像正依赖于当前镜像的某一层。这种情况,依旧不会触发删除该层的行为,直到没有任何层依赖当前层时,才会真实的删除当前层。
用 docker image ls 命令来配合
像其它可以承接多个实体的命令一样,可以使用 docker image ls -q
来配合使用 docker image rm
,这样可以成批的删除希望删除的镜像。
利用 commit 理解镜像构成
镜像是容器的基础,每次执行 docker run
的时候都会指定那个镜像作为容器运行的基础。当我们运行一个容器的时候,我们做的任何文件修改都会被记录于容器存储层里。而 Docker 提供来一个 docker commit
命令,可以将容器的存储层保存下来成为镜像。
docker commit
的语法格式为:
慎用 docker commit
使用 docker commit
命令虽然可以比较直观的帮助理解镜像分层存储的概念,但是实际环境中并不会这样使用。由于命令的执行,还有很多文件被改动或添加来。这还仅仅是最简单的操作,如果是安装软件包,编译构建,那会有大量的无关内容被添加进来,将会导致镜像极为臃肿。
此外,使用 docker commit
意味着所有对镜像的操作都是黑箱操作,生成的镜像也被称为黑箱镜像。
使用 Dockerfile 定制镜像
Dockerfile 是一个文本文件, 其内包含了一条条的指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。
FROM 指定基础镜像
所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。FROM
就是指定基础镜像,因此一个 Dockerfile
中 FROM
是必备的指令,并且必须是第一条指令。
除了选择现有镜像为基础镜像外,Docker还存在一个特殊的镜像,名为 scratch
。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见,对于Linux下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接 FROM scratch
会让镜像体积更加小巧。
RUN执行命令
RUN
指令是用来执行命令行命令的。由于命令行的强大执行能力,RUN
指令在定制镜像是是最常用的指令之一。其格式有两种:
- shell格式:RUN <命令>
,就像直接在命令行中输入的命令一样。
- exec格式:RUN ["可执行文件", "参数1", "参数2"]
Union FS 是有最大层数限制的,比如 AUFS,曾经是最大不得超过 42 层,现在是不得超过 127 层。
构建镜像
$ docker build -t nginx:v3 .
Sending build context to Docker daemon 2.048 kB
Step 1 : FROM nginx
---> e43d811ce2f4
Step 2 : RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
---> Running in 9cdc27646c7b
---> 44aa4490ce2c
Removing intermediate container 9cdc27646c7b
Successfully built 44aa4490ce2c
镜像构建上下文(Context)
Docker 在运行时分为 Docker 引擎(也就是服务端守护进程)和客户端工具。当构建的时候,用户会指定构建镜像上下文的路径,docker build
命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。
COPY ../package.json /app
或者 COPY /opt/xxx /app
无法工作的原因,是因为这些路径超出了上下文的范围,Docker 引擎无法获得这些位置的文件。如果需要这些文件,应该将它们复制到上下文目录中去。
镜像的实现原理
Docker 镜像是怎么实现增量的修改和维护的? 通常 Union Fs 有两个用途: 1. 实现不借助 LVM, RAID 将多个 disk 挂到同一个目录下 2. 将一个只读的分支和一个可写的分支联合在一起 Live CD 正是基于此方法可以允许在镜像不变的基础上允许用户在其上进行一些写操作。 Docker 在 OverlayFS 上构建的容器也是利用了类似的原理。