docker(1)

镜像

操作系统分为 内核用户空间。对于 Linux 而言,内核启动后,会挂载 root 文件系统为其提供用户空间支持而docker镜像相当于一个root文件系统,并且使用分层储存

容器

镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的 实例 一样,镜像是静态的定义,容器是镜像运行时的实体,其实质其实是进程,容器不是虚拟机

每一个容器运行时,是以镜像为基础层,在其上创建一个当前容器的存储层,我们可以称这个为容器运行时读写而准备的存储层为 容器存储层。容器不应该向其存储层内写入任何数据,容器存储层要保持无状态化。所有的文件写入操作,都**应该使用 数据卷(Volume)、或者 绑定宿主目录**,在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高。

仓库

仓库里面储存,分发镜像

例子:

Ubuntu 镜像 为例,ubuntu 是仓库的名字,其内包含有不同的版本标签,如,16.04, 18.04。我们可以通过 ubuntu:16.04,或者 ubuntu:18.04 来具体指定所需哪个版本的镜像。如果忽略了标签,比如 ubuntu,那将视为 ubuntu:latest

获取镜像

1
$ docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签]

列出镜像

1
docker image ls

列表包含了 仓库名标签镜像 ID创建时间 以及 所占用的空间镜像 ID 则是镜像的唯一标识,一个镜像可以对应多个 标签

由于 Docker 镜像是多层存储结构,并且可以继承、复用,因此不同镜像可能会因为使用相同的基础镜像,从而拥有共同的层。由于 Docker 使用 Union FS,相同的层只需要保存一份即可,因此实际镜像硬盘占用空间很可能要比这个列表镜像大小的总和要小的多

虚悬镜像

上面的镜像列表中,还可以看到一个特殊的镜像,这个镜像既没有仓库名,也没有标签,均为 <none>由于新旧镜像同名,旧镜像名称被取消,从而出现仓库名、标签均为 <none> 的镜像

中间层镜像

相同的层只会存一遍,而这些镜像是别的镜像的依赖,因此并不会因为它们被列出来而多存了一份,为了加速镜像构建、重复利用资源,Docker 会利用 中间层镜像。所以在使用一段时间后,可能会看到一些依赖的中间层镜像

删除本地镜像

1
$ docker image rm [选项] <镜像1> [<镜像2> ...]

可以用镜像的完整 ID,也称为 长 ID,来删除镜像。使用脚本的时候可能会用长 ID,但是人工输入就太累了,所以更多的时候是用 短 ID 来删除镜像,一般取3个字符以上更精确的是使用 镜像摘要digests 删除镜像。

Untagged 和 Deleted

因为一个镜像可以对应多个标签,因此当我们删除了所指定的标签后,可能还有别的标签指向了这个镜像,如果是这种情况,那么 Delete 行为就不会发生

所以并非所有的 docker image rm 都会产生删除镜像的行为,有可能仅仅是取消了某个标签而已。

只要这层有依赖就不会触发删除行为,容器依赖也不行

用 docker image ls 命令来配合

1
2
$ docker image rm $(docker image ls -q redis)
$ docker image rm $(docker image ls -q -f before=mongo:3.2)

利用 commit 理解镜像构成

每次执行 docker run 的时候都会指定哪个镜像作为容器运行的基础

1
$ docker run --name webserver -d -p 80:80 nginx

这条命令会用 nginx 镜像启动一个容器,命名为 webserver,并且映射了 80 端口,这样我们可以用浏览器去访问这个 nginx 服务器

1
2
3
4
$ docker exec -it webserver bash
root@3729b97e8226:/# echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
root@3729b97e8226:/# exit
exit

我们可以通过 docker diff 命令看到具体的改动。

而 Docker 提供了一个 docker commit 命令,可以将容器的存储层保存下来成为镜像。换句话说,就是在原有镜像的基础上,再叠加上容器的存储层,并构成新的镜像。以后我们运行这个新镜像的时候,就会拥有原有容器最后的文件变化

就是形成新的镜像文件,此镜像保留了原本镜像的最后的状态

1
docker commit [选项] <容器ID或容器名> [<仓库名>[:<标签>]]

我们还可以用 docker history 具体查看镜像内的历史记录

慎用docker commit

因为会添加许多的无关文件进来,相当于对镜像进行黑箱操作

Dockerfile 指令详解

COPY 复制文件

1
2
3
4

COPY [--chown=<user>:<group>] <源路径>... <目标路径>
COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]
COPY package.json /usr/src/app/

<目标路径> 可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR 指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用

在使用该指令的时候还可以加上 --chown=<user>:<group> 选项来改变文件的所属用户及所属组

ADD 更高级的复制文件

基本格式一样但是增加了一些功能

尽可能的使用 COPY,因为 COPY 的语义很明确,一般使用add的场所在需要自动解压的场合使用add

在使用该指令的时候还可以加上 --chown=<user>:<group> 选项来改变文件的所属用户及所属组

1
2
3
4
ADD --chown=55:mygroup files* /mydir/
ADD --chown=bin files* /mydir/
ADD --chown=1 files* /mydir/
ADD --chown=10:11 files* /mydir/

CMD 容器启动命令

Shell格式

1
CMD echo $HOME

在实际执行中,会将其变更为:

1
CMD [ "sh", "-c", "echo $HOME" ]

在shell格式中,Docker将在一个shell中执行这个命令,这就是为什么你能够使用环境变量($HOME)。这个命令实际上会被包装成/bin/sh -c的参数执行(如果你在Windows下运行Docker,这个命令会被转化成cmd /S /C的参数执行)。

Exec格式

1
CMD ["executable", "param1", "param2"]

在exec格式就是一个json数组因此一定要使用双引号 ",而不要使用单引号,首元素是被调用的命令,剩下的命令就是参数中,Docker将直接执行这个命令,而不会通过shell。这就是为什么你不能够在这个格式中使用环境变量或shell的命令 substitution(比如$HOME)。

1
CMD service nginx start

失败的原因是因为容器,是为了主进程存在而存在的,主进程结束容器也会自然退出,

而刚才说了 CMD service nginx start 会被理解为 CMD [ "sh", "-c", "service nginx start"],那么sh就是主程序shell在运行过后就结束了当然容器也会退出

正确的做法是直接执行 nginx 可执行文件,并且要求以前台形式运行。比如:直接将nginx当作主程序daemon off; 是为了使 Nginx 保持在前台运行,不会成为后台进程。

1
CMD ["nginx", "-g", "daemon off;"]

Docker的工作机制是,容器运行的主进程必须在前台运行,不然Docker会认为主进程已经结束,然后容器就会退出