Docker Dockerfile 定制镜像的方法

来源:脚本之家  责任编辑:小易  

当想让一个容器做两件事情,或者使一个Docker镜像包含来自两个不同镜像的依赖库时,就需要知道每个镜

使用 Dockerfile 定制镜像

  目前系统或基础语言的镜像选官方的就可以了,多数情况下以他们为 base image 做自己的镜像

镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。

如何通过dockerhistory命令来对docker镜像进行反向工程推测它们的Dockerfile

Dockerfile 是一个文本文件,其内包含了一条条的指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。

创建镜像:1、准备jdk、需要运行的spring boot小项目将jdk压缩包解压到Docker

此处以定制 nginx 镜像为例,使用 Dockerfile 来定制。

Dockerfile结构dockerfile由4部分信息组成:基础镜像信息、维护者信息、镜像操作

在一个空白目录中,建立一个文本文件,并命名为 Dockerfile :

只喜欢我颖宝,赵丽颖她跑了七八年的龙套,绝不是一炮而红。在娱乐圈这个浮躁的大环境下,她始终能尊重和热爱演员这个工作。对每一个角色她都是认真理解过,才全情投入去演绎。相对那些天天只会炒作的明星来说,她担得起演员这两个字。她的情商有些低,但绝不是她不聪明,只是他不愿意阿谀奉承。他是那种知世故而不世故的人。看的透彻,不愿意戴假面具。\n你说她出生农民学历低,我就想说:英雄不问出处。农民是我们应该尊重的人。吃着大米饭的我们没有任何理由瞧不起农民。至于学历,就请黑他的人们心自问,除了大学生这个称呼,你哪一点比得上她?我从没认为大学生这个身份有多了不起。只要是努力拼搏的人,不管她的出身和学历,都是值得尊重

$ mkdir mynginx $ cd mynginx $ touch Dockerfile

纯粹是博眼球。拍卖的那颗,来自于母树,而这个女的采摘的并非母树荔枝。拿母树历史最高拍卖价来作为参考价格,纯属扯淡。如果按照这个价格去卖荔枝,那GDP估计得飙到天上去。就仿佛,你损坏了我的电脑,你要知道人类历史上第一台电脑价值好几百万呢。关于荔枝名字叫挂绿。是因为其外壳四分微绿六分红,每个都有一圈绿线而名。古书记载,这荔枝“爽脆如梨,浆液不见,去壳怀之,三日不变.”正是因为如此,所以一直以来备受关注,更是成为贡品。后来,当地人觉得作为贡品每年采摘保存什么的太辛苦了,所以就把挂绿树全咔嚓了,只剩下了一颗母树,存在于增城荔枝镇挂绿园,后来被人抢救后,又可以继续发育了,被称为”西圆挂绿”。55万的来历

其内容为:

2005年,美国著名的财经杂志《福布斯》曾进行了一项名为“历史上最有权势的二十位商人”的评选。在这个财富榜上,囊括了福特、摩根、洛克菲勒等大名鼎鼎的人物,不过排在第7位的梅耶·罗斯柴尔德,虽被冠以“国际金融之父”,却并不太为今人所知。实际上自18世纪末起,梅耶所缔造的金融帝国就始终对整个欧洲经济具有巨大影响力,而罗斯柴尔德家族也被视为历史上最为成功的商业家族之一。只不过在相当长的时间里,由于其独特的行事风格,该家族似乎总披着一层神秘的面纱。第一、梅耶开创“红盾”家族罗斯柴尔德家族的历史可追溯到16世纪。那时,该家族还默默无闻地定居于德国法兰克福的犹太区。直到18世纪时,在梅耶·罗斯柴尔德的努力

FROM nginx RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html

  现在国内的各个城市的宠物都在不断的增加,宠物店处在于一个紧缺的行业。宠物店不能够满足于养宠人的需求,宠物店创业成为了创业者首选的一个项目。那么宠物店创业靠谱吗?下面圣宠宠物小编为大家分析一下。  现在国内各个城市的宠物每年都在不断的持续上升,根据有关部门的调查数据显示,每年的宠物品牌加盟店都在不断的提高,每年都能比去年提高20%多,如此强劲的加盟趋势是不是说明投资者加盟宠物店就真的特别靠谱呢?宠物店加盟店还是很靠谱的一个行业。  现在各个城市的投资市场上面主要的宠物经营模式主要有两种方式,一种就是选择的自己经营宠物店开个人的宠物店,还有一种就是选择一家宠物品牌开连锁店的经营模式。对于这两种

这个 Dockerfile 很简单,一共就两行。涉及到了两条指令, FROM 和 RUN 。

Dockerfile 指令详解

FROM 指定基础镜像

所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。而 FROM 就是指定基础镜像,因此一个 Dockerfile 中 FROM 是必备的指令,并且必须是第一条指令。

在 Docker Store 上有非常多的高质量的官方镜像,有可以直接拿来使用的服务类的镜像,如nginx 、 redis 、 mongo 、mysql 等;也有一些方便开发、构建、运行各种语言应用的镜像,如 node 、 openjdk 、 python 等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。

如果没有找到对应服务的镜像,官方镜像中还提供了一些更为基础的操作系统镜像,如ubuntu 、 debian 、 centos 等,这些操作系统的软件库为我们提供了更广阔的扩展空间。

除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 scratch 。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。

FROM scratch ...

如果你以 scratch 为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。

不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见,比如 swarm 、 coreos/etcd 。对于 Linux 下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接 FROM scratch 会让镜像体积更加小巧。使用 Go 语言 开发的应用很多会使用这种方式来制作镜像,这也是为什么有人认为 Go是特别适合容器微服务架构的语言的原因之一。

RUN 执行命令

RUN 指令是用来执行命令行命令的。由于命令行的强大能力, RUN 指令在定制镜像时是最常用的指令之一。其格式有两种:

shell 格式: RUN <命令> ,就像直接在命令行中输入的命令一样。刚才写的 Dockerfile 中的 RUN 指令就是这种格式。

RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html

exec 格式: RUN ["可执行文件", "参数1", "参数2"],这更像是函数调用中的格式。

既然 RUN 就像 Shell 脚本一样可以执行命令,那么我们是否就可以像 Shell 脚本一样把每个命令对应一个 RUN 呢?比如这样:

FROM debian:jessie RUN apt-get update RUN apt-get install -y gcc libc6-dev make RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.tar.gz" RUN mkdir -p /usr/src/redis RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 RUN make -C /usr/src/redis RUN make -C /usr/src/redis install

之前说过,Dockerfile 中每一个指令都会建立一层, RUN 也不例外。每一个 RUN 的行为,就和刚才我们手工建立镜像的过程一样:新建立一层,在其上执行这些命令,执行结束后, commit 这一层的修改,构成新的镜像。

而上面的这种写法,创建了 7 层镜像。这是完全没有意义的,而且很多运行时不需要的东西,都被装进了镜像里,比如编译环境、更新的软件包等等。结果就是产生非常臃肿、非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。 这是很多初学 Docker 的人常犯的一个错误(我也不能原谅自己ε=(´ο`*)))唉)。

Union FS 是有最大层数限制的,比如 AUFS,曾经是最大不得超过 42 层,现在是不得超过127 层。

上面的 Dockerfile 正确的写法应该是这样:

FROM debian:jessie RUN buildDeps='gcc libc6-dev make' \ && apt-get update \ && apt-get install -y $buildDeps \ && wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.tar.gz" \ && mkdir -p /usr/src/redis \ && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \ && make -C /usr/src/redis \ && make -C /usr/src/redis install \ && rm -rf /var/lib/apt/lists/* \ && rm redis.tar.gz \ && rm -r /usr/src/redis \ && apt-get purge -y --auto-remove $buildDeps

首先,之前所有的命令只有一个目的,就是编译、安装 redis 可执行文件。因此没有必要建立很多层,这只是一层的事情。因此,这里没有使用很多个 RUN 对一一对应不同的命令,而是仅仅使用一个 RUN 指令,并使用 && 将各个所需命令串联起来。将之前的 7 层,简化为了1 层。在撰写 Dockerfile 的时候,要经常提醒自己,这并不是在写 Shell 脚本,而是在定义每一层该如何构建。

并且,这里为了格式化还进行了换行。Dockerfile 支持 Shell 类的行尾添加 \ 的命令换行方式,以及行首 # 进行注释的格式。良好的格式,比如换行、缩进、注释等,会让维护、排障更为容易,这是一个比较好的习惯。

此外,还可以看到这一组命令的最后添加了清理工作的命令,删除了为了编译构建所需要的软件,清理了所有下载、展开的文件,并且还清理了 apt 缓存文件。这是很重要的一步,之前有说过,镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。因此镜像构建时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉。

很多人初学 Docker 制作出了很臃肿的镜像的原因之一,就是忘记了每一层构建的最后一定要清理掉无关文件。

构建镜像

好了,让我们再回到之前定制的 nginx 镜像的 Dockerfile 来。现在我们明白了这个 Dockerfile的内容,那么让我们来构建这个镜像吧。

在 Dockerfile 文件所在目录执行:

$ 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

从命令的输出结果中,我们可以清晰的看到镜像的构建过程。在 Step 2 中,如同我们之前所说的那样, RUN 指令启动了一个容器 9cdc27646c7b ,执行了所要求的命令,并最后提交了这一层 44aa4490ce2c ,随后删除了所用到的这个容器 9cdc27646c7b 。

这里我们使用了 docker build 命令进行镜像构建。其格式为:

docker build [选项] <上下文路径/URL/->

在这里我们指定了最终镜像的名称 -t nginx:v3 ,构建成功后,我们可以直接运行这个镜像,其结果就是我们的主页被改变成了Hello, Docker!。

镜像构建上下文(Context)

如果注意,会看到 docker build 命令最后有一个 . 。 . 表示当前目录,而 Dockerfile就在当前目录,因此不少初学者以为这个路径是在指定 Dockerfile 所在路径,这么理解其实是不准确的。如果对应上面的命令格式,你可能会发现,这是在指定上下文路径。那么什么是上下文呢?

首先我们要理解 docker build 的工作原理。Docker 在运行时分为 Docker 引擎(也就是服务端守护进程)和客户端工具。Docker 的引擎提供了一组 REST API,被称为 DockerRemote API,而如 docker 命令这样的客户端工具,则是通过这组 API 与 Docker 引擎交互,从而完成各种功能。因此,虽然表面上我们好像是在本机执行各种 docker 功能,但实际上,一切都是使用的远程调用形式在服务端(Docker 引擎)完成。也因为这种 C/S 设计,让我们操作远程服务器的 Docker 引擎变得轻而易举。

当我们进行镜像构建的时候,并非所有定制都会通过 RUN 指令完成,经常会需要将一些本地文件复制进镜像,比如通过 COPY 指令、 ADD 指令等。而 docker build 命令构建镜像,其实并非在本地构建,而是在服务端,也就是 Docker 引擎中构建的。那么在这种客户端/服务端的架构中,如何才能让服务端获得本地文件呢?

这就引入了上下文的概念。当构建的时候,用户会指定构建镜像上下文的路径, docker build 命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。

如果在 Dockerfile 中这么写:

COPY ./package.json /app/

这并不是要复制执行 docker build 命令所在的目录下的 package.json ,也不是复制 Dockerfile 所在目录下的 package.json ,而是复制 上下文(context) 目录下的 package.json 。

因此, COPY 这类指令中的源文件的路径都是相对路径。这也是初学者经常会问的为什么 COPY ../package.json /app 或者 COPY /opt/xxxx /app 无法工作的原因,因为这些路径已经超出了上下文的范围,Docker 引擎无法获得这些位置的文件。如果真的需要那些文件,应该将它们复制到上下文目录中去。

现在就可以理解刚才的命令 docker build -t nginx:v3 . 中的这个 . ,实际上是在指定上下文的目录, docker build 命令会将该目录下的内容打包交给 Docker 引擎以帮助构建镜像。

如果观察 docker build 输出,我们其实已经看到了这个发送上下文的过程:

$ docker build -t nginx:v3 . Sending build context to Docker daemon 2.048 kB ...

理解构建上下文对于镜像构建是很重要的,避免犯一些不应该的错误。比如有些初学者在发现 COPY /opt/xxxx /app 不工作后,于是干脆将 Dockerfile 放到了硬盘根目录去构建,结果发现 docker build 执行后,在发送一个几十 GB 的东西,极为缓慢而且很容易构建失败。那是因为这种做法是在让 docker build 打包整个硬盘,这显然是使用错误。

一般来说,应该会将 Dockerfile 置于一个空目录下,或者项目根目录下。如果该目录下没有所需文件,那么应该把所需文件复制一份过来。如果目录下有些东西确实不希望构建时传给 Docker 引擎,那么可以用 .gitignore 一样的语法写一个 .dockerignore ,该文件是用于剔除不需要作为上下文传递给 Docker 引擎的。

那么为什么会有人误以为 . 是指定 Dockerfile 所在目录呢?这是因为在默认情况下,如果不额外指定 Dockerfile 的话,会将上下文目录下的名为 Dockerfile 的文件作为 Dockerfile。

这只是默认行为,实际上 Dockerfile 的文件名并不要求必须为 Dockerfile ,而且并不要求必须位于上下文目录中,比如可以用 -f ../Dockerfile.php 参数指定某个文件作为 Dockerfile 。

当然,一般大家习惯性的会使用默认的文件名 Dockerfile ,以及会将其置于镜像构建上下文目录中。

其它 docker build 的用法

直接用 Git repo 进行构建

或许你已经注意到了, docker build 还支持从 URL 构建,比如可以直接从 Git repo 中构建:

$ docker build https://github.com/twang2218/gitlab-ce-zh.git#:8.14 docker build https://github.com/twang2218/gitlab-ce-zh.git\#:8.14 Sending build context to Docker daemon 2.048 kB Step 1 : FROM gitlab/gitlab-ce:8.14.0-ce.0 8.14.0-ce.0: Pulling from gitlab/gitlab-ce aed15891ba52: Already exists 773ae8583d14: Already exists ...

这行命令指定了构建所需的 Git repo,并且指定默认的 master 分支,构建目录为 /8.14/ ,然后 Docker 就会自己去 git clone 这个项目、切换到指定分支、并进入到指定目录后开始构建。

用给定的 tar 压缩包构建

$ docker build http://server/context.tar.gz

如果所给出的 URL 不是个 Git repo,而是个 tar 压缩包,那么 Docker 引擎会下载这个包,并自动解压缩,以其作为上下文,开始构建。

从标准输入中读取 Dockerfile 进行构建

docker build - < Dockerfile

cat Dockerfile | docker build -

如果标准输入传入的是文本文件,则将其视为 Dockerfile ,并开始构建。这种形式由于直接从标准输入中读取 Dockerfile 的内容,它没有上下文,因此不可以像其他方法那样可以将本地文件 COPY 进镜像之类的事情。

从标准输入中读取上下文压缩包进行构建

$ docker build - < context.tar.gz

如果发现标准输入的文件格式是 gzip 、 bzip2 以及 xz 的话,将会使其为上下文压缩包,直接将其展开,将里面视为上下文,并开始构建。

COPY 复制文件

格式: COPY <源路径>... <目标路径> COPY ["<源路径1>",... "<目标路径>"]

和 RUN 指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。COPY 指令将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的 <目标路径> 位置。比如:

COPY package.json /usr/src/app/

<源路径> 可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的 filepath.Match 规则,如:

COPY hom* /mydir/ COPY hom?.txt /mydir/

<目标路径> 可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR 指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。

此外,还需要注意一点,使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git进行管理的时候。

ADD 更高级的复制文件

ADD 指令和 COPY 的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。比如 <源路径> 可以是一个 URL ,这种情况下,Docker 引擎会试图去下载这个链接的文件放到 <目标路径> 去。下载后的文件权限自动设置为 600 ,如果这并不是想要的权限,那么还需要增加额外的一层 RUN 进行权限调整,另外,如果下载的是个压缩包,需要解压缩,也一样还需要额外的一层 RUN 指令进行解压缩。所以不如直接使用 RUN 指令,然后使用 wget 或者 curl 工具下载,处理权限、解压缩、然后清理无用文件更合理。因此,这个功能其实并不实用,而且不推荐使用。

如果 <源路径> 为一个 tar 压缩文件的话,压缩格式为 gzip , bzip2 以及 xz 的情况下, ADD 指令将会自动解压缩这个压缩文件到 <目标路径> 去。

在某些情况下,这个自动解压缩的功能非常有用,比如官方镜像 ubuntu 中:

FROM scratch ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz / ...

但在某些情况下,如果我们真的是希望复制个压缩文件进去,而不解压缩,这时就不可以使用 ADD 命令了。

在 Docker 官方的 Dockerfile 最佳实践文档 中要求,尽可能的使用 COPY ,因为 COPY 的语义很明确,就是复制文件而已,而 ADD 则包含了更复杂的功能,其行为也不一定很清晰。最适合使用 ADD 的场合,就是所提及的需要自动解压缩的场合。

另外需要注意的是, ADD 指令会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。

因此在 COPY 和 ADD 指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用 COPY 指令,仅在需要自动解压缩的场合使用 ADD 。

CMD 容器启动命令

CMD 指令的格式和 RUN 相似,也是两种格式: shell 格式: CMD <命令> exec 格式: CMD ["可执行文件", "参数1", "参数2"...] 参数列表格式: CMD ["参数1", "参数2"...] 。在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数。

之前介绍容器的时候曾经说过,Docker 不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。 CMD 指令就是用于指定默认的容器主进程的启动命令的。

在运行时可以指定新的命令来替代镜像设置中的这个默认命令,比如, ubuntu 镜像默认的CMD 是 /bin/bash ,如果我们直接 docker run -it ubuntu 的话,会直接进入 bash 。我们也可以在运行时指定运行别的命令,如 docker run -it ubuntu cat /etc/os-release 。这就是用 cat /etc/os-release 命令替换了默认的 /bin/bash 命令了,输出了系统版本信息。

在指令格式上,一般推荐使用 exec 格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 " ,而不要使用单引号。

如果使用 shell 格式的话,实际的命令会被包装为 sh -c 的参数的形式进行执行。比如:

CMD echo $HOME

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

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

这就是为什么我们可以使用环境变量的原因,因为这些环境变量会被 shell 进行解析处理。提到 CMD 就不得不提容器中应用在前台执行和后台执行的问题。这是初学者常出现的一个混淆。

Docker 不是虚拟机,容器中的应用都应该以前台执行,而不是像虚拟机、物理机里面那样,用 upstart/systemd 去启动后台服务,容器内没有后台服务的概念。

初学者一般将 CMD 写为:

CMD service nginx start

然后发现容器执行后就立即退出了。甚至在容器内去使用 systemctl 命令结果却发现根本执行不了。这就是因为没有搞明白前台、后台的概念,没有区分容器和虚拟机的差异,依旧在以传统虚拟机的角度去理解容器。

对于容器而言,其启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。

而使用 service nginx start 命令,则是希望 systemd 来以后台守护进程形式启动 nginx 服务。而刚才说了 CMD service nginx start 会被理解为 CMD [ “sh”, “-c”, “service nginxstart”] ,因此主进程实际上是 sh 。那么当 service nginx start 命令结束后, sh 也就结束了, sh 作为主进程退出了,自然就会令容器退出。

正确的做法是直接执行 nginx 可执行文件,并且要求以前台形式运行。比如:

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

ENTRYPOINT 入口点

ENTRYPOINT 的格式和 RUN 指令格式一样,分为 exec 格式和 shell 格式。

ENTRYPOINT 的目的和 CMD 一样,都是在指定容器启动程序及参数。ENTRYPOINT 在运行时也可以替代,不过比 CMD 要略显繁琐,需要通过 docker run 的参数 –entrypoint 来指定。

当指定了 ENTRYPOINT 后, CMD 的含义就发生了改变,不再是直接的运行其命令,而是将CMD 的内容作为参数传给 ENTRYPOINT 指令,换句话说实际执行时,将变为:

<ENTRYPOINT> "<CMD>"

那么有了 CMD 后,为什么还要有 ENTRYPOINT 呢?这种 <ENTRYPOINT> "<CMD>" 有什么好处么?让我们来看几个场景。

场景一:让镜像变成像命令一样使用

假设我们需要一个得知自己当前公网 IP 的镜像,那么可以先用 CMD 来实现:

FROM ubuntu:16.04 RUN apt-get update \ && apt-get install -y curl \ && rm -rf /var/lib/apt/lists/* CMD [ "curl", "-s", "http://ip.cn" ]

假如我们使用 docker build -t myip . 来构建镜像的话,如果我们需要查询当前公网 IP,只需要执行:

$ docker run myip

当前 IP:61.148.226.66 来自:北京市 联通

嗯,这么看起来好像可以直接把镜像当做命令使用了,不过命令总有参数,如果我们希望加参数呢?比如从上面的 CMD 中可以看到实质的命令是 curl ,那么如果我们希望显示 HTTP头信息,就需要加上 -i 参数。那么我们可以直接加 -i 参数给 docker run myip 么?

$ docker run myip -i docker: Error response from daemon: invalid header field value "oci runtime error: con tainer_linux.go:247: starting container process caused \"exec: \\\"-i\\\": executable file not found in $PATH\"\n".

我们可以看到可执行文件找不到的报错, executable file not found 。之前我们说过,跟在镜像名后面的是 command ,运行时会替换 CMD 的默认值。因此这里的 -i 替换了原来的 CMD ,而不是添加在原来的 curl -s http://ip.cn 后面。而 -i 根本不是命令,所以自然找不到。

那么如果我们希望加入 -i 这参数,我们就必须重新完整的输入这个命令:

$ docker run myip curl -s http://ip.cn -i

这显然不是很好的解决方案,而使用 ENTRYPOINT 就可以解决这个问题。现在我们重新用 ENTRYPOINT 来实现这个镜像:

FROM ubuntu:16.04 RUN apt-get update \ && apt-get install -y curl \ && rm -rf /var/lib/apt/lists/* ENTRYPOINT [ "curl", "-s", "http://ip.cn" ]

这次我们再来尝试直接使用 docker run myip -i :

$ docker run myip

当前 IP:61.148.226.66 来自:北京市 联通

$ docker run myip -i HTTP/1.1 200 OK Server: nginx/1.8.0 Date: Tue, 22 Nov 2016 05:12:40 GMT Content-Type: text/html; charset=UTF-8 Vary: Accept-Encoding X-Powered-By: PHP/5.6.24-1~dotdeb+7.1 X-Cache: MISS from cache-2 X-Cache-Lookup: MISS from cache-2:80 X-Cache: MISS from proxy-2_6 Transfer-Encoding: chunked Via: 1.1 cache-2:80, 1.1 proxy-2_6:8006 Connection: keep-alive

当前 IP:61.148.226.66 来自:北京市 联通

可以看到,这次成功了。这是因为当存在 ENTRYPOINT 后, CMD 的内容将会作为参数传给 ENTRYPOINT ,而这里 -i 就是新的 CMD ,因此会作为参数传给 curl ,从而达到了我们预期的效果。

场景二:应用运行前的准备工作

启动容器就是启动主进程,但有些时候,启动主进程前,需要一些准备工作。比如 mysql 类的数据库,可能需要一些数据库配置、初始化的工作,这些工作要在最终的 mysql 服务器运行之前解决。

此外,可能希望避免使用 root 用户去启动服务,从而提高安全性,而在启动服务前还需要以 root 身份执行一些必要的准备工作,最后切换到服务用户身份启动服务。或者除了服务外,其它命令依旧可以使用 root 身份执行,方便调试等。

这些准备工作是和容器 CMD 无关的,无论 CMD 为什么,都需要事先进行一个预处理的工作。这种情况下,可以写一个脚本,然后放入 ENTRYPOINT 中去执行,而这个脚本会将接到的参数(也就是 )作为命令,在脚本最后执行。比如官方镜像 redis 中就是这么做的:

FROM alpine:3.4 ... RUN addgroup -S redis && adduser -S -G redis redis ... ENTRYPOINT ["docker-entrypoint.sh"] EXPOSE 6379 CMD [ "redis-server" ]

可以看到其中为了 redis 服务创建了 redis 用户,并在最后指定了 ENTRYPOINT 为 dockerentrypoint.sh 脚本。

#!/bin/sh ... # allow the container to be started with `--user` if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then chown -R redis . exec su-exec redis "$0" "$@" fi exec "$@"

该脚本的内容就是根据 CMD 的内容来判断,如果是 redis-server 的话,则切换到 redis 用户身份启动服务器,否则依旧使用 root 身份执行。比如:

$ docker run -it redis id uid=0(root) gid=0(root) groups=0(root)

ENV 设置环境变量

格式有两种:

    ENV <key> <value> ENV <key1>=<value1> <key2>=<value2>...

这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN ,还是运行时的应用,都可以直接使用这里定义的环境变量。

ENV VERSION=1.0 DEBUG=on \ NAME="Happy Feet"

这个例子中演示了如何换行,以及对含有空格的值用双引号括起来的办法,这和 Shell 下的行为是一致的。

定义了环境变量,那么在后续的指令中,就可以使用这个环境变量。比如在官方 node 镜像 Dockerfile 中,就有类似这样的代码:

ENV NODE_VERSION 7.2.0 RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.ta r.xz" \ && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \ && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \ && grep " node-v$NODE_VERSION-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c - \ && tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \ && rm "node-v$NODE_VERSION-linux-x64.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \ && ln -s /usr/local/bin/node /usr/local/bin/nodejs

在这里先定义了环境变量 NODE_VERSION ,其后的 RUN 这层里,多次使用 $NODE_VERSION 来进行操作定制。可以看到,将来升级镜像构建版本的时候,只需要更新 7.2.0 即可, Dockerfile 构建维护变得更轻松了。

下列指令可以支持环境变量展开:

ADD 、 COPY 、 ENV 、 EXPOSE 、 LABEL 、 USER 、 WORKDIR 、 VOLUME 、 STOPSIGNAL 、 ONBUILD 。

可以从这个指令列表里感觉到,环境变量可以使用的地方很多,很强大。通过环境变量,我们可以让一份 Dockerfile 制作更多的镜像,只需使用不同的环境变量即可。

ARG 构建参数

格式: ARG <参数名>[=<默认值>]

构建参数和 ENV 的效果一样,都是设置环境变量。所不同的是, ARG 所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的。但是不要因此就使用 ARG 保存密码之类的信息,因为 docker history 还是可以看到所有值的。

Dockerfile 中的 ARG 指令是定义参数名称,以及定义其默认值。该默认值可以在构建命令 docker build 中用 --build-arg <参数名>=<值> 来覆盖。

在 1.13 之前的版本,要求 –build-arg 中的参数名,必须在 Dockerfile 中用 ARG 定义过了,换句话说,就是 –build-arg 指定的参数,必须在 Dockerfile 中使用了。如果对应参数没有被使用,则会报错退出构建。从 1.13 开始,这种严格的限制被放开,不再报错退出,而是显示警告信息,并继续构建。这对于使用 CI 系统,用同样的构建流程构建不同的 Dockerfile 的时候比较有帮助,避免构建命令必须根据每个 Dockerfile 的内容修改。

VOLUME 定义匿名卷

格式为: VOLUME ["<路径1>", "<路径2>"...] VOLUME <路径>

之前我们说过,容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中。为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 Dockerfile 中,我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。

VOLUME /data

这里的 /data 目录就会在运行时自动挂载为匿名卷,任何向 /data 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。当然,运行时可以覆盖这个挂载设置。比如:

docker run -d -v mydata:/data xxxx

在这行命令中,就使用了 mydata 这个命名卷挂载到了 /data 这个位置,替代了 Dockerfile 中定义的匿名卷的挂载配置。

EXPOSE 声明端口

格式为 EXPOSE <端口1> [<端口2>...]。

EXPOSE 指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口。

此外,在早期 Docker 版本中还有一个特殊的用处。以前所有容器都运行于默认桥接网络中,因此所有容器互相之间都可以直接访问,这样存在一定的安全性问题。于是有了一个 Docker 引擎参数 --icc=false ,当指定该参数后,容器间将默认无法互访,除非互相间使用了 --links 参数的容器才可以互通,并且只有镜像中 EXPOSE 所声明的端口才可以被访问。这个 --icc=false 的用法,在引入了 docker network 后已经基本不用了,通过自定义网络可以很轻松的实现容器间的互联与隔离。

要将 EXPOSE 和在运行时使用 -p <宿主端口>:<容器端口> 区分开来。 -p ,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 EXPOSE 仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。

WORKDIR 指定工作目录

格式为 WORKDIR <工作目录路径> 。

使用 WORKDIR 指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在, WORKDIR 会帮你建立目录。

之前提到一些初学者常犯的错误是把 Dockerfile 等同于 Shell 脚本来书写,这种错误的理解还可能会导致出现下面这样的错误:

RUN cd /app RUN echo "hello" > world.txt

如果将这个 Dockerfile 进行构建镜像运行后,会发现找不到 /app/world.txt 文件,或者其内容不是 hello 。原因其实很简单,在 Shell 中,连续两行是同一个进程执行环境,因此前一个命令修改的内存状态,会直接影响后一个命令;而在 Dockerfile 中,这两行 RUN 命令的执行环境根本不同,是两个完全不同的容器。这就是对 Dockerfile 构建分层存储的概念不了解所导致的错误。

之前说过每一个 RUN 都是启动一个容器、执行命令、然后提交存储层文件变更。第一层 RUNcd /app 的执行仅仅是当前进程的工作目录变更,一个内存上的变化而已,其结果不会造成任何文件变更。而到第二层的时候,启动的是一个全新的容器,跟第一层的容器更完全没关系,自然不可能继承前一层构建过程中的内存变化。

因此如果需要改变以后各层的工作目录的位置,那么应该使用 WORKDIR 指令。

USER 指定当前用户

格式: USER <用户名>

USER 指令和 WORKDIR 相似,都是改变环境状态并影响以后的层。 WORKDIR 是改变工作目录, USER 则是改变之后层的执行 RUN , CMD 以及 ENTRYPOINT 这类命令的身份。当然,和 WORKDIR 一样, USER 只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。

RUN groupadd -r redis && useradd -r -g redis redis USER redis RUN [ "redis-server" ]

如果以 root 执行的脚本,在执行期间希望改变身份,比如希望以某个已经建立好的用户来运行某个服务进程,不要使用 su 或者 sudo ,这些都需要比较麻烦的配置,而且在 TTY 缺失的环境下经常出错。建议使用 gosu 。

# 建立 redis 用户,并使用 gosu 换另一个用户执行命令 RUN groupadd -r redis && useradd -r -g redis redis # 下载 gosu RUN wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.7/ gosu-amd64" \ && chmod +x /usr/local/bin/gosu \ && gosu nobody true # 设置 CMD,并以另外的用户执行 CMD [ "exec", "gosu", "redis", "redis-server" ]

HEALTHCHECK 健康检查

格式: HEALTHCHECK [选项] CMD <命令> :设置检查容器健康状况的命令 HEALTHCHECK NONE :如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令

HEALTHCHECK 指令是告诉 Docker 应该如何进行判断容器的状态是否正常,这是 Docker 1.12 引入的新指令。

在没有 HEALTHCHECK 指令前,Docker 引擎只可以通过容器内主进程是否退出来判断容器是否状态异常。很多情况下这没问题,但是如果程序进入死锁状态,或者死循环状态,应用进程并不退出,但是该容器已经无法提供服务了。在 1.12 以前,Docker 不会检测到容器的这种状态,从而不会重新调度,导致可能会有部分容器已经无法提供服务了却还在接受用户请求。

而自 1.12 之后,Docker 提供了 HEALTHCHECK 指令,通过该指令指定一行命令,用这行命令来判断容器主进程的服务状态是否还正常,从而比较真实的反应容器实际状态。

当在一个镜像指定了 HEALTHCHECK 指令后,用其启动容器,初始状态会为 starting ,在 HEALTHCHECK 指令检查成功后变为 healthy ,如果连续一定次数失败,则会变为 unhealthy 。

HEALTHCHECK 支持下列选项: interval=<间隔> :两次健康检查的间隔,默认为 30 秒; timeout=<时长> :健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认 30 秒; retries=<次数> :当连续失败指定次数后,则将容器状态视为 unhealthy ,默认 3 次。

和 CMD , ENTRYPOINT 一样, HEALTHCHECK 只可以出现一次,如果写了多个,只有最后一个生效。

在 HEALTHCHECK [选项] CMD 后面的命令,格式和 ENTRYPOINT 一样,分为 shell 格式,和 exec 格式。命令的返回值决定了该次健康检查的成功与否: 0 :成功; 1 :失败; 2 :保留,不要使用这个值。

假设我们有个镜像是个最简单的 Web 服务,我们希望增加健康检查来判断其 Web 服务是否在正常工作,我们可以用 curl 来帮助判断,其 Dockerfile 的 HEALTHCHECK 可以这么写:

FROM nginx RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* HEALTHCHECK --interval=5s --timeout=3s \ CMD curl -fs http://localhost/ || exit 1

这里我们设置了每 5 秒检查一次(这里为了试验所以间隔非常短,实际应该相对较长),如果健康检查命令超过 3 秒没响应就视为失败,并且使用 curl -fs http://localhost/ || exit 1 作为健康检查命令。

使用 docker build 来构建这个镜像:

$ docker build -t myweb:v1 .

构建好了后,我们启动一个容器:

$ docker run -d --name web -p 80:80 myweb:v1

当运行该镜像后,可以通过 docker container ls 看到最初的状态为 (health: starting) :

$ docker container ls CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 03e28eb00bd0 myweb:v1 "nginx -g 'daemon off" 3 seconds ago Up 2 seconds (health: starting) 80/tcp, 443/tcp web

在等待几秒钟后,再次 docker container ls ,就会看到健康状态变化为了 (healthy) :

$ docker container ls CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 03e28eb00bd0 myweb:v1 "nginx -g 'daemon off" 18 seconds ago Up 16 seconds (health: healthy) 80/tcp, 443/tcp web

如果健康检查连续失败超过了重试次数,状态就会变为 (unhealthy) 。

为了帮助排障,健康检查命令的输出(包括 stdout 以及 stderr )都会被存储于健康状态里,可以用 docker inspect 来查看。

$ docker inspect --format '{{json .State.Health}}' upbeat_allen | python -m json.tool { "FailingStreak": 0, "Log": [ { "End": "2018-06-14T04:55:37.477730277-04:00", "ExitCode": 0, "Output": "<!DOCTYPE html>\n<html>\n<head>\n<title>Welcome to nginx!</title>\n<style>\n body {\n width: 35em;\n margin: 0 auto;\n font-family: Tahoma, Verdana, Arial, sans-serif;\n }\n</style>\n</head>\n<body>\n<h1>Welcome to nginx!</h1>\n<p>If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.</p>\n\n<p>For online documentation and support please refer to\n<a href=\"http://nginx.org/\">nginx.org</a>.<br/>\nCommercial support is available at\n<a href=\"http://nginx.com/\">nginx.com</a>.</p>\n\n<p><em>Thank you for using nginx.</em></p>\n</body>\n</html>\n", "Start": "2018-06-14T04:55:37.408045977-04:00" }, { "End": "2018-06-14T04:55:42.553816257-04:00", "ExitCode": 0, "Output": "<!DOCTYPE html>\n<html>\n<head>\n<title>Welcome to nginx!</title>\n<style>\n body {\n width: 35em;\n margin: 0 auto;\n font-family: Tahoma, Verdana, Arial, sans-serif;\n }\n</style>\n</head>\n<body>\n<h1>Welcome to nginx!</h1>\n<p>If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.</p>\n\n<p>For online documentation and support please refer to\n<a href=\"http://nginx.org/\">nginx.org</a>.<br/>\nCommercial support is available at\n<a href=\"http://nginx.com/\">nginx.com</a>.</p>\n\n<p><em>Thank you for using nginx.</em></p>\n</body>\n</html>\n", "Start": "2018-06-14T04:55:42.480940888-04:00" }, { "End": "2018-06-14T04:55:47.631694051-04:00", "ExitCode": 0, "Output": "<!DOCTYPE html>\n<html>\n<head>\n<title>Welcome to nginx!</title>\n<style>\n body {\n width: 35em;\n margin: 0 auto;\n font-family: Tahoma, Verdana, Arial, sans-serif;\n }\n</style>\n</head>\n<body>\n<h1>Welcome to nginx!</h1>\n<p>If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.</p>\n\n<p>For online documentation and support please refer to\n<a href=\"http://nginx.org/\">nginx.org</a>.<br/>\nCommercial support is available at\n<a href=\"http://nginx.com/\">nginx.com</a>.</p>\n\n<p><em>Thank you for using nginx.</em></p>\n</body>\n</html>\n", "Start": "2018-06-14T04:55:47.557214953-04:00" }, { "End": "2018-06-14T04:55:52.708195002-04:00", "ExitCode": 0, "Output": "<!DOCTYPE html>\n<html>\n<head>\n<title>Welcome to nginx!</title>\n<style>\n body {\n width: 35em;\n margin: 0 auto;\n font-family: Tahoma, Verdana, Arial, sans-serif;\n }\n</style>\n</head>\n<body>\n<h1>Welcome to nginx!</h1>\n<p>If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.</p>\n\n<p>For online documentation and support please refer to\n<a href=\"http://nginx.org/\">nginx.org</a>.<br/>\nCommercial support is available at\n<a href=\"http://nginx.com/\">nginx.com</a>.</p>\n\n<p><em>Thank you for using nginx.</em></p>\n</body>\n</html>\n", "Start": "2018-06-14T04:55:52.63499573-04:00" }, { "End": "2018-06-14T04:55:57.795117794-04:00", "ExitCode": 0, "Output": "<!DOCTYPE html>\n<html>\n<head>\n<title>Welcome to nginx!</title>\n<style>\n body {\n width: 35em;\n margin: 0 auto;\n font-family: Tahoma, Verdana, Arial, sans-serif;\n }\n</style>\n</head>\n<body>\n<h1>Welcome to nginx!</h1>\n<p>If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.</p>\n\n<p>For online documentation and support please refer to\n<a href=\"http://nginx.org/\">nginx.org</a>.<br/>\nCommercial support is available at\n<a href=\"http://nginx.com/\">nginx.com</a>.</p>\n\n<p><em>Thank you for using nginx.</em></p>\n</body>\n</html>\n", "Start": "2018-06-14T04:55:57.714289056-04:00" } ], "Status": "healthy" }

ONBUILD 为他人做嫁衣裳

格式: ONBUILD <其它指令>。

ONBUILD 是一个特殊的指令,它后面跟的是其它指令,比如 RUN , COPY 等,而这些指令,在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。

Dockerfile 中的其它指令都是为了定制当前镜像而准备的,唯有 ONBUILD 是为了帮助别人定制自己而准备的。

假设我们要制作 Node.js 所写的应用的镜像。我们都知道 Node.js 使用 npm 进行包管理,所有依赖、配置、启动信息等会放到 package.json 文件里。在拿到程序代码后,需要先进行 npm install 才可以获得所有需要的依赖。然后就可以通过 npm start 来启动应用。因此,一般来说会这样写 Dockerfile :

FROM node:slim RUN mkdir /app WORKDIR /app COPY ./package.json /app RUN [ "npm", "install" ] COPY . /app/ CMD [ "npm", "start" ]

把这个 Dockerfile 放到 Node.js 项目的根目录,构建好镜像后,就可以直接拿来启动容器运行。但是如果我们还有第二个 Node.js 项目也差不多呢?好吧,那就再把这个 Dockerfile 复制到第二个项目里。那如果有第三个项目呢?再复制么?文件的副本越多,版本控制就越困难,让我们继续看这样的场景维护的问题。

如果第一个 Node.js 项目在开发过程中,发现这个 Dockerfile 里存在问题,比如敲错字了、或者需要安装额外的包,然后开发人员修复了这个 Dockerfile ,再次构建,问题解决。第一个项目没问题了,但是第二个项目呢?虽然最初 Dockerfile 是复制、粘贴自第一个项目的,但是并不会因为第一个项目修复了他们的 Dockerfile ,而第二个项目的 Dockerfile 就会被自动修复。

那么我们可不可以做一个基础镜像,然后各个项目使用这个基础镜像呢?这样基础镜像更新,各个项目不用同步 Dockerfile 的变化,重新构建后就继承了基础镜像的更新?好吧,可以,让我们看看这样的结果。那么上面的这个 Dockerfile 就会变为:

FROM node:slim RUN mkdir /app WORKDIR /app CMD [ "npm", "start" ]

这里我们把项目相关的构建指令拿出来,放到子项目里去。假设这个基础镜像的名字为 mynode 的话,各个项目内的自己的 Dockerfile 就变为:

FROM my-node COPY ./package.json /app RUN [ "npm", "install" ] COPY . /app/

基础镜像变化后,各个项目都用这个 Dockerfile 重新构建镜像,会继承基础镜像的更新。

那么,问题解决了么?没有。准确说,只解决了一半。如果这个 Dockerfile 里面有些东西需要调整呢?比如 npm install 都需要加一些参数,那怎么办?这一行 RUN 是不可能放入基础镜像的,因为涉及到了当前项目的 ./package.json ,难道又要一个个修改么?所以说,这样制作基础镜像,只解决了原来的 Dockerfile 的前4条指令的变化问题,而后面三条指令的变化则完全没办法处理。

ONBUILD 可以解决这个问题。让我们用 ONBUILD 重新写一下基础镜像的 Dockerfile :

FROM node:slim RUN mkdir /app WORKDIR /app ONBUILD COPY ./package.json /app ONBUILD RUN [ "npm", "install" ] ONBUILD COPY . /app/ CMD [ "npm", "start" ]

 这次我们回到原始的 Dockerfile ,但是这次将项目相关的指令加上 ONBUILD ,这样在构建基础镜像的时候,这三行并不会被执行。然后各个项目的 Dockerfile 就变成了简单地:

FROM my-node

 是的,只有这么一行。当在各个项目目录中,用这个只有一行的 Dockerfile 构建镜像时,之前基础镜像的那三行 ONBUILD 就会开始执行,成功的将当前项目的代码复制进镜像、并且针对本项目执行 npm install ,生成应用镜像。

参考:https://github.com/yeasy/docker_practice

扩展阅读,根据您访问的内容系统为您准备了以下内容,希望对您有帮助。

如何用Dockerfile创建镜像

制作镜像的方式主要有两种:

通过docker commit 制作镜像

通过docker build 制作镜像

这两种方式都是通过改进已有的镜像来达到自己的目的。制作基础镜像,会在另外一篇文章“从零开始制作基础镜像”中介绍。

docker commit

docker commit 是往版本控制系统里提交一次变更。使用这种方式制作镜像,本质上是运行一个基础镜像,然后在基础镜像上进行软件安装和修改。最后再将改动提交到版本系统中。

如何使用Dockerfile构建镜像

你好,使用方法如下:

Dockerfile结构

dockerfile由4部分信息组成:基础镜像信息、维护者信息、镜像操作指令和容器启动时执行指令。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

# This dockerfile uses the ubuntu image

# VERSION 2 - EDITION 1

# Author: docker_user

# Command format: Instruction [arguments / command] ..

# Base image to use, this must be set as the first line

FROM ubuntu

# Maintainer: docker_user <docker_user at email.com> (@docker_user)

MAINTAINER docker_user docker_user@email.com

# Commands to update the image

RUN echo "deb http://archive.ubuntu.com/ubuntu/ raring main universe" >> /etc/apt/sources.list

RUN apt-get update && apt-get install -y nginx

RUN echo "\ndaemon off;" >> /etc/nginx/nginx.conf

# Commands when creating a new container

CMD /usr/sbin/nginx

其中#表注释,可以标注一些说明性的文字。

FROM关键字指定镜像的来源,默认为DockerHub,也可以写私有仓库的镜像,例如:localhost:5000/centos:6.7,如果本地已经存在指定的镜像名称,则会从本地缓存直接获取。MAINTAINER 指定镜像的作者,之后为镜像操作执行RUN、ADD等,最后是容器启动时发起的指令。

Dockerfile中的指令

FROM: 指定镜像名称,格式为FROM <image> 或FROM <image>:<tag>,例如FROM ubuntu 或 FROM ubuntu:12.04 

MAINTAINER: 镜像作者 ,格式为 MAINTAINER <name>

RUN:格式为 RUN <command> 或 RUN ["executable", "param1", "param2"]。

前者将在 shell 终端中运行命令,即 /bin/sh -c;后者则使用 exec 执行。指定使用其它终端可以通过第二种方式实现,例如 RUN ["/bin/bash", "-c", "echo hello"]。

每条 RUN 指令将在当前镜像基础上执行指定命令,并提交为新的镜像。当命令较长时可以使用 \ 来换行。

CMD:支持三种格式

  1.CMD ["executable","param1","param2"] 使用 exec 执行,推荐方式;

  2.CMD command param1 param2 在 /bin/sh 中执行,提供给需要交互的应用;

  3.CMD ["param1","param2"] 提供给 ENTRYPOINT 的默认参数;

指定启动容器时执行的命令,每个 Dockerfile 只能有一条 CMD 命令。如果指定了多条命令,只有最后一条会被执行。如果用户启动容器时候指定了运行的命令,则会覆盖掉 CMD 指定的命令。

EXPOSE:格式为 EXPOSE <port> [<port>...]。

告诉 Docker 服务端容器暴露的端口号,供互联系统使用。在启动容器时需要通过 -P,Docker 主机会自动分配一个端口转发到指定的端口。

ENV:格式为 ENV <key> <value>。 指定一个环境变量,会被后续 RUN 指令使用,并在容器运行时保持。这就对应程序语言中的变量定义,可在需要的时候引用。例如:

1

2

3

4

ENV PG_MAJOR 9.3

ENV PG_VERSION 9.3.4

RUN curl -SL http://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgress && …

ENV PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH

ADD:格式为 ADD <src> <dest>。

该命令将复制指定的 <src> 到容器中的 <dest>。 其中 <src> 可以是Dockerfile所在目录的一个相对路径;也可以是一个 URL;还可以是一个 tar 文件(自动解压为目录)。

COPY:格式为 COPY <src> <dest>。

复制本地主机的 <src>(为 Dockerfile 所在目录的相对路径)到容器中的 <dest>。当使用本地目录为源目录时,推荐使用 COPY。

COPY和ADD的不同就是:ADD多了自动解压和支持URL路径的功能。

ENTRYPOINT:

两种格式:

ENTRYPOINT ["executable", "param1", "param2"]

ENTRYPOINT command param1 param2(shell中执行)。

配置容器启动后执行的命令,并且不可被 docker run 提供的参数覆盖。

每个 Dockerfile 中只能有一个 ENTRYPOINT,当指定多个时,只有最后一个起效。

CMD和ENTRYPOINT比较:两个命令都是只能使用一次,并且都是在执行docker run指令时运行,如果有多个,只执行最后一条。

两者的不同在于参数的传递方式,如果在Dockerfile中定义如下指令

1

CMD echo hello

1

ENTRYPOINT ["echo","hello"] 

那么在运行命令docker run containerId echo hello时,指定了CMD的输入结果为world,可以看出Dockerfile中指定的命令被覆盖了,而指定了ENTRYPOINT时,输出结果为hello echo world,可以看出指定的命令被作为ENTRYPOINT指定指令的参数了。

VOLUME:格式为 VOLUME ["/data"]。创建一个可以从本地主机或其他容器挂载的挂载点,一般用来存放数据库和需要保持的数据等。不过此属性在Dockerfile中指定并没有什么意义,因为没有办法指定本地主机的目录。如果需要指定挂载点可以在执行docker run命令时指定:

1

docker run -it -v /home/fengzheng/ftp/:/data 859666d51c6d /bin/bash

USER:格式为 USER daemon。指定运行容器时的用户名或 UID,后续的 RUN 也会使用指定用户。

当服务不需要管理员权限时,可以通过该命令指定运行用户。并且可以在之前创建所需要的用户,例如:RUN groupadd -r postgres && useradd -r -g postgres postgres。要临时获取管理员权限可以使用 gosu,而不推荐 sudo。

WORKDIR:格式为 WORKDIR /path/to/workdir。为后续的 RUN、CMD、ENTRYPOINT 指令配置工作目录。可以使用多个 WORKDIR 指令,后续命令如果参数是相对路径,则会基于之前命令指定的路径。例如

1

2

3

4

WORKDIR /a

WORKDIR b

WORKDIR c

RUN pwd

则最终路径为 /a/b/c。

ONBUILD:格式为 ONBUILD [INSTRUCTION]。

配置当所创建的镜像作为其它新创建镜像的基础镜像时,所执行的操作指令。

例如,Dockerfile 使用如下的内容创建了镜像 image-A。

1

2

3

4

[...]

ONBUILD ADD . /app/src

ONBUILD RUN /usr/local/bin/python-build --dir /app/src

[...]

如果基于 image-A 创建新的镜像时,新的Dockerfile中使用 FROM image-A指定基础镜像时,会自动执行ONBUILD 指令内容,等价于在后面添加了两条指令。

1

2

3

4

5

FROM image-A

#Automatically run the following

ADD . /app/src

RUN /usr/local/bin/python-build --dir /app/src

使用 ONBUILD 指令的镜像,推荐在标签中注明,例如 ruby:1.9-onbuild。

基于CentOS6.7并源码安装nginx

首先准备了nginx-1.9.9.tar.gz安装包和CentOS6-Base-163.repo(163源),将这两个文件放到同一目录下,并在此目录下创建名称为Dockerfile的文件。之后在此文件中实现源替换、nginx编译安装、及一些依赖包的安装,Dockerfile内容如下:

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

# this is a test ubuntu 12.04 image dockerfile

# Author:fengzheng

# Base image,this must be set as the first line

#localhost:5000/centos:6.7是我的私有仓库的镜像,可替换为centos:6.7(DockerHub中的镜像)

FROM localhost:5000/centos:6.7

MAINTAINER fengzheng

# Commands to update the image

RUN mkdir /usr/nginx1.9.9

ADD nginx-1.9.9.tar.gz /usr/nginx1.9.9/

#RUN yum -y install tar

#RUN tar -zxvf /usr/nginx1.9.9/nginx-1.9.9.tar.gz

RUN cd /etc/yum.repos.d/ && mv CentOS-Base.repo CentOS-Base.repo.bak

ADD CentOS6-Base-163.repo /etc/yum.repos.d/

RUN cd /etc/yum.repos.d/ && mv CentOS6-Base-163.repo CentOS-Base.repo \

&& yum clean all && yum makecache \

&& yum -y install gcc \

&& yum -y install yum install -y pcre-devel \

&& yum -y install zlib zlib-devel \

&& yum -y install openssl openssl--devel \

&& cd /usr/nginx1.9.9/nginx-1.9.9/ && ./configure && make && make install

#如果设置daemon off; nginx无法启动

#RUN echo "\ndaemon off;" >> /etc/nginx/nginx.conf

# Commands when creating a new container

# 启动nginx 需进入/usr/local/nginx/sbin 执行./configure

CMD /bin/bash

最后执行命令"docker build -t nginx-centos:6.7 ."

其中.表示在当前目录下搜索Dockerfile文件,-t参数指定镜像名称和tag。

如何不通过Dockerfile从已有镜像构建新镜像

在使用Docker的过程中,我们除了从Docker Hub上下载已经做好的镜像,很多时候需要我们自己制作镜像。下面想在这个文章中说明一下镜像的制作方法。

制作镜像的方式主要有两种:

通过docker commit 制作镜像

通过docker build 制作镜像

这两种方式都是通过改进已有的镜像来达到自己的目的。制作基础镜像,会在另外一篇文章“从零开始制作基础镜像”中介绍。

docker commit

docker commit 是往版本控制系统里提交一次变更。使用这种方式制作镜像,本质上是运行一个基础镜像,然后在基础镜像上进行软件安装和修改。最后再将改动提交到版本系统中。

Docker中如何定制运行容器镜像

1.先下载centos镜像

[root@docker ~]# docker pull centos

2.启动容器并进行配置

启动容器,

[root@docker ~]# docker run -it -d --name test-centos1 centos

d72250ecaa5e3e36226a1edd749f494d9f00eddc4143c81ac3565aa4e551791a

命令注释:-it : 进行交互式操作

-d : 等同于 -d=true,容器将会在后台运行,不然执行一次命令后,退出后,便是exit状态了。

--name : 容器启动后的名字,默认不指定,将会随机产生一个名字。或者使用 -name="containers_name"

centos:使用的镜像名称

进入容器,安装ssh server,以及配置开机启动

[root@docker ~]# docker exec -it test-centos1 /bin/bash

[root@d72250ecaa5e /]# ifconfig

bash: ifconfig: command not found

注:命令最后参数 /bin/bash: 指进入容器时执行的命令(command)

我们检查了下容器,暂时安装以下必用的软件吧 net-tools,openssh-server

[root@d72250ecaa5e /]# yum install openssh-server net-tools -y

创建ssh 所需的目录,并在根目录创建sshd 启动脚本

复制代码

[root@d72250ecaa5e /]# mkdir -pv /var/run/sshd

mkdir: created directory '/var/run/sshd'

[root@d72250ecaa5e /]# cat /auto_sshd.sh

#!/bin/bash

/usr/sbin/sshd -D

[root@d72250ecaa5e /]# chmod +x /auto_sshd.sh

复制代码

修改容器内root 的账户密码

[root@d72250ecaa5e /]# echo "root:iloveworld" | chpasswd

生成ssh 主机dsa 密钥(不然ssh 该容器时,会出现错误。)

[root@d72250ecaa5e /]# ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key

[root@d72250ecaa5e /]# ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key

我们加一个history记录的时间功能吧,这样方便后期查看

echo 'export HISTTIMEFORMAT="%F %T `whoami` "' >> /etc/profile

OK,配置基本完毕咯。清理命令历史纪录,之后退出容器。现在可以生成一个新的docker 镜像了。

3.配置完成后,进行打包成新的镜像

复制代码

[root@docker ~]# docker commit test-centos1 centos_sshd:7.0

sha256:6e3330b30dfff5f029f102874e54cfffffbc37dcf2a4eb7304c817148fbc944d

[root@docker ~]# docker images

REPOSITORY TAG IMAGE ID CREATED SIZE

centos_sshd 7.0 6e3330b30dff 8 seconds ago 310.1 MB

docker.io/ubuntu latest e4415b714b62 12 days ago 128.1 MB

复制代码

命令注释:commit: 提交一个具有新配置的容器成为镜像,后面跟容器的name 或者容器Id ,最后是生成新镜像的名字

更新:这条命令更方便以后启动,如下:

[root@docker ~]# docker commit --change='CMD ["/auto_sshd.sh"]' -c "EXPOSE 22" test-centos1 centos_sshd:7.0

sha256:7bb4efd82c4ff1f241cbc57ee45aab1b05d214b1e9fcd51196696c67d480e70b

命令注释: --change : 将后期使用此镜像运行容器时的命令参数、开放的容器端口提前设置好。

Docker 用镜像方式迁移和用Dockerfile动态生成有什么区别

  情况一:假定这个image build之后没有run过,那么通过save只是将这个image所有的data layer导成二进制tar文件。通过load导入后,和之前build好的image相比没有区别,所以一样。

  情况二:假定这个image 已经被run过但未commit(如果commit,则会是一个新的image),那么通过save仍然只是将image的data layer导成二进制tar文件,而container所产生的新data layer不会导出。因此通过load导入后,和正在处于run状态的container相比,数据不一致(缺少run之后产生的data layer)。

  • 本文相关:
  • docker中镜像构建文件dockerfile与相关命令的详细介绍
  • dockerfile制作官方tomcat镜像及镜像使用详解
  • 如何通过vs2017的dockerfile来生成镜像
  • 利用dockerfile制作java运行环境的镜像的方法步骤
  • 详解docker中dockerfile指令创建镜像
  • docker使用dockerfile来创建镜像
  • docker中dockerfile方式建立镜像helloworld
  • docker如何使用dockerfile构建镜像
  • docker使用dockerfile构建镜像的方法
  • 使用dockerfile构建nginx镜像的方法示例
  • 基于alpine用dockerfile创建的爬虫scrapy镜像的实现
  • ubuntu docker 的安装部署及简单应用
  • 详解docker如何启动一个centos镜像
  • docker如何快速搭建几个常用的第三方服务详解
  • docker部署laravel应用实现队列&任务调度
  • centos 7 sshd 链接被拒绝问题解决办法
  • docker数据存储之tmpfs mounts详解
  • docker搭建私有镜像仓库的方法
  • 使用jib打包docker镜像实战
  • docker安装kong网关的方法示例
  • docker 实现浏览器里开发android应用的功能
  • 如何用Dockerfile创建镜像
  • 如何使用Dockerfile构建镜像
  • 如何不通过Dockerfile从已有镜像构建新镜像
  • Docker中如何定制运行容器镜像
  • Docker 用镜像方式迁移和用Dockerfile动态生成有什么区别
  • dockerfile 基础镜像 有哪些
  • DockerHub里的镜像太多了,怎么选择
  • 如何通过vs2017的Dockerfile来生成镜像
  • 如何用Dockerfile创建镜像
  • 如何使用Dockerfile构建镜像
  • 免责声明 - 关于我们 - 联系我们 - 广告联系 - 友情链接 - 帮助中心 - 频道导航
    Copyright © 2017 www.zgxue.com All Rights Reserved