Docker镜像创建方式
Docker 创建镜像主要有三种:
基于本地模板来导入;
基于已有的镜像创建;
基于 Dockerfile 来创建;
基于本地模板导入(了解)
1 | 通过导入操作系统模板文件可以生成镜像,模板可以从OPENVZ开源项目下载,下载地址为 http://openvz.org/Download/template/precreated |
基于已有的镜像创建(commit方式)
docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]
常用参数
**-a :**提交的镜像作者;
**-c :**使用Dockerfile指令来创建镜像;
**-m :**提交时的说明文字;
**-p :**在commit时,将容器暂停。
我们所使用的一般都来自于 Docker Hub 的镜像。直接使用这些镜像是可以满足一定的需求,而当这些镜像无法直接满足需求时,我们就需要定制这些镜像。
镜像是多层存储,每一层是在前一层的基础上进行的修改;而容器同样也是多层存储,是在以镜像为基础层,在其基础上加一层作为容器运行时的存储层。
自定义镜像我们一般使用Dockfile的方式,虽然commit方式也行,但是不推荐。
commit 可以方便我们理解镜像是如何构成的,下面我们以nginx为例,来介绍一下这个过程。
首先我们启动一个nginx容器:
1 | [root@caojiarui0 ~]# docker run --name nginx -p 80:80 -d nginx |
此时我们就可以通过http://localhost去nginx首页了(服务器或虚拟机测试需要将localhost改为对应的ip地址)。下面我们修改一下这个首页的内容:
1 | [root@caojiarui0 ~]# docker exec -it nginx bash |
再次访问网页,我们可以看到如下内容:
我们修改了容器的文件,也就是改动了容器的存储层。可以通过 docker diff
命令看到具体的改动。
1 | [root@caojiarui0 ~]# docker diff nginx |
现在我们定制好了变化,我们希望能将其保存下来形成镜像。
要知道,当我们运行一个容器的时候(如果不使用卷的话),我们做的任何文件修改都会被记录于容器存储层里。而 Docker 提供了一个 docker commit
命令,可以将容器的存储层保存下来成为镜像。换句话说,就是在原有镜像的基础上,再叠加上容器的存储层,并构成新的镜像。以后我们运行这个新镜像的时候,就会拥有原有容器最后的文件变化。
docker commit
的语法格式为:
1 | docker commit [选项] <容器ID或容器名> [<仓库名>[:<标签>]] |
我们可以用下面的命令将容器保存为镜像:
1 | [root@caojiarui0 ~]# docker commit \ |
此时,我们就能通过docker image ls
查看到这个镜像了:
1 | [root@caojiarui0 ~]# docker image ls nginx |
我们还可以用 docker history
具体查看镜像内的历史记录,如果比较 docker history nginx:latest
的历史记录,会发现新增了我们刚刚提交的这一层。
1 | [root@caojiarui0 ~]# docker history nginx:v2 |
然后启动这个新的镜像,并访问http://localhost:81,可以发现nginx首页内容就是我们之前修改过的内容
1 | docker run --name nginx2 -d -p 81:80 nginx:v2 |
所以我们使用 docker commit
命令来定制镜像是手动操作给旧的镜像添加了新的一层,形成新的镜像,对镜像多层存储应该有了更直观的感觉。
慎用 docker commit
使用 docker commit
命令虽然可以比较直观的帮助理解镜像分层存储的概念,但是实际环境中并不会这样使用。
首先,如果仔细观察之前的 docker diff webserver
的结果,你会发现除了真正想要修改的 /usr/share/nginx/html/index.html
文件外,由于命令的执行,还有很多文件被改动或添加了。这还仅仅是最简单的操作,如果是安装软件包、编译构建,那会有大量的无关内容被添加进来,将会导致镜像极为臃肿。
此外,使用 docker commit
意味着所有对镜像的操作都是黑箱操作,生成的镜像也被称为 黑箱镜像,换句话说,就是除了制作镜像的人知道执行过什么命令、怎么生成的镜像,别人根本无从得知。而且,即使是这个制作镜像的人,过一段时间后也无法记清具体的操作。这种黑箱镜像的维护工作是非常痛苦的。
而且,回顾之前提及的镜像所使用的分层存储的概念,除当前层外,之前的每一层都是不会发生改变的,换句话说,任何修改的结果仅仅是在当前层进行标记、添加、修改,而不会改动上一层。如果使用 docker commit
制作镜像,以及后期修改的话,每一次修改都会让镜像更加臃肿一次,所删除的上一层的东西并不会丢失,会一直如影随形的跟着这个镜像,即使根本无法访问到。这会让镜像更加臃肿。
Dockerfile
Dockerfile简介
工作环境中的需求是不可控的,所以网络上的镜像有时候很难满足实际的需求,对于一些个性化的需求,我们一般需要个性化定制Docker镜像。又因为docker commit命令有一些缺陷,所以我们有必要了解如何使用当前主流的Dockerfile方式定制镜像。
Dockerfile其实就是我们用来构建Docker镜像的源码,当然这不是所谓的编程源码,而是一些命令和参数构成的脚本,只要理解它的逻辑和语法格式,就可以很容易的编写Dockerfile。
它的构建步骤如下:
1、编写Dockerfile文件
2、docker build 构建镜像
3、docker run 运行镜像
Dockerfile的基本结构
Dockerfile是一个包含用于组合映像的命令的文本文档。可以使用在命令行中调用任何命令。 Docker通过读取Dockerfile中的指令自动生成映像。
docker build命令用于从Dockerfile构建映像。可以在docker build命令中使用 -f 标志指向文件系统中任何位置的Dockerfile。
Dockerfile由一行行命令语句组成,并且支持以#开头的注释行Dockerfile分为四部分:基础镜像信息、维护者信息、 镜像操作指令和容器启动时执行指令
Dockerfile命令介绍
Docker以从上到下的顺序运行Dockerfile的指令。为了指定基本映像,第一条指令必须是FROM。一个声明以 # 字符开头则被视为注释。可以在Docker文件中使用 RUN , CMD , FROM , EXPOSE , ENV 等指令。
命令 | 说明 |
---|---|
FROM | 指定基础镜像,必须为第一个命令 |
MAINTAINER | 维护者(作者)信息 |
ENV | 设置环境变量 |
RUN | 构建镜像时执行的命令 |
CMD | 构建容器后调用,也就是在容器启动时才进行调用。 |
ENTRYPOINT | 指定运行容器启动过程执行命令,覆盖CMD参数ENTRYPOINT与CMD非常类似,不同的是通过docker run执行的命令不会覆盖ENTRYPOINT,而docker run命令中指定的任何参数,都会被当做参数再次传递给ENTRYPOINT。Dockerfile中只允许有一个ENTRYPOINT命令,多指定时会覆盖前面的设置,而只执行最后的ENTRYPOINT指令。 |
ADD | 将本地文件添加到容器中,tar类型文件会自动解压(网络压缩资源不会被解压),可以访问网络资源,类似wget |
COPY | 功能类似ADD,但是是不会自动解压文件,也不能访问网络资源 |
WORKDIR | 工作目录,类似于cd命令 |
ARG | 用于指定传递给构建运行时的变量 |
VOLUMN | 用于指定持久化目录 |
EXPOSE | 指定于外界交互的端口 |
USER | 指定运行容器时的用户名或 UID,后续的 RUN 也会使用指定用户。使用USER指定用户时,可以使用用户名、UID或GID,或是两者的组合。当服务不需要管理员权限时,可以通过该命令指定运行用户。并且可以在之前创建所需要的用户 |
需要注意的几点
FROM命令
所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。而 FROM 就是指定基础镜像,因此一个 Dockerfile 中 FROM 是必备的指令,并且必须是第一条指令。
在 Docker Store 上有非常多的高质量的官方镜像,有可以直接拿来使用的服务类的镜像,如nginx 、 redis 、 mongo 、mysql 等;也有一些方便开发、构建、运行各种语言应用的镜像,如 node 、 openjdk 、 python 等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。
如果没有找到对应服务的镜像,官方镜像中还提供了一些更为基础的操作系统镜像,如ubuntu 、 debian 、 centos 等,这些操作系统的软件库为我们提供了更广阔的扩展空间。
除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 scratch 。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。
1 | FROM scratch |
如果你以 scratch 为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。
不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见,比如 swarm 、 coreos/etcd 。对于 Linux 下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接 FROM scratch 会让镜像体积更加小巧。使用 Go 语言 开发的应用很多会使用这种方式来制作镜像,这也是为什么有人认为 Go是特别适合容器微服务架构的语言的原因之一。
RUN 命令
RUN 指令是用来执行命令行命令的。由于命令行的强大能力, RUN 指令在定制镜像时是最常用的指令之一。其格式有两种:
shell 格式: RUN <命令> ,就像直接在命令行中输入的命令一样。刚才写的 Dockerfile 中的 RUN 指令就是这种格式。
1 | RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html |
exec 格式: RUN [“可执行文件”, “参数1”, “参数2”],这更像是函数调用中的格式。
既然 RUN 就像 Shell 脚本一样可以执行命令,那么我们是否就可以像 Shell 脚本一样把每个命令对应一个 RUN 呢?比如这样:
1 | FROM debian:stretch |
之前说过,Dockerfile 中每一个指令都会建立一层,RUN
也不例外。每一个 RUN
的行为,就和刚才我们手工建立镜像的过程一样:新建立一层,在其上执行这些命令,执行结束后,commit
这一层的修改,构成新的镜像。
而上面的这种写法,创建了 7 层镜像。这是完全没有意义的,而且很多运行时不需要的东西,都被装进了镜像里,比如编译环境、更新的软件包等等。结果就是产生非常臃肿、非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。 这是很多初学 Docker 的人常犯的一个错误。
Union FS 是有最大层数限制的,比如 AUFS,曾经是最大不得超过 42 层,现在是不得超过127 层。
上面的 Dockerfile 正确的写法应该是这样:
1 | FROM debian:stretch |
首先,之前所有的命令只有一个目的,就是编译、安装 redis 可执行文件。因此没有必要建立很多层,这只是一层的事情。因此,这里没有使用很多个 RUN
一一对应不同的命令,而是仅仅使用一个 RUN
指令,并使用 &&
将各个所需命令串联起来。将之前的 7 层,简化为了 1 层。在撰写 Dockerfile 的时候,要经常提醒自己,这并不是在写 Shell 脚本,而是在定义每一层该如何构建。
并且,这里为了格式化还进行了换行。Dockerfile 支持 Shell 类的行尾添加 \
的命令换行方式,以及行首 #
进行注释的格式。良好的格式,比如换行、缩进、注释等,会让维护、排障更为容易,这是一个比较好的习惯。
此外,还可以看到这一组命令的最后添加了清理工作的命令,删除了为了编译构建所需要的软件,清理了所有下载、展开的文件,并且还清理了 apt
缓存文件。这是很重要的一步,我们之前说过,镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。因此镜像构建时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉。
很多人初学 Docker 制作出了很臃肿的镜像的原因之一,就是忘记了每一层构建的最后一定要清理掉无关文件。
Dockerfile实战
Docker Hub里的nginx没有ping已经ip addr两个命令,下面我们用Dockerfile定制一个镜像,让其支持ping及ip addr的命令。
- 编写一个Dockerfile文件(无后缀名)
1 | [root@localhost ~]# cd /opt/dockerfile/nginx |
上面/opt/dockerfile/nginx的内容为:
1 | # 文件内容如下 |
参考: