廊坊微信网站建设,成品影视app开发月光宝盒怎么样,t恤在线定制,网站建设厘金手指排名二二一、使用 Dockerfile 定制镜像
1.1、Dockerfile 定制镜像
镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本#xff0c;用这个脚本来构建、定制镜像#xff0c;无法重复的问题、镜像构建透明性的问题、…
一、使用 Dockerfile 定制镜像
1.1、Dockerfile 定制镜像
镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本用这个脚本来构建、定制镜像无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。
Dockerfile 是一个文本文件其内包含了一条条的指令(Instruction)每一条指令构建一层因此每一条指令的内容就是描述该层应当如何构建。
以 nginx 镜像为例这次我们使用 Dockerfile 来定制。
在一个空白目录中建立一个文本文件并命名为 Dockerfile
$ mkdir mynginx
$ cd mynginx
$ touch Dockerfile其内容为
FROM nginx
RUN echo h1Hello, Docker!/h1 /usr/share/nginx/html/index.html这个 Dockerfile 很简单一共就两行。涉及到了两条指令FROM 和 RUN。
1.2、FROM 指定基础镜像
所谓定制镜像那一定是以一个镜像为基础在其上进行定制。就像我们之前运行了一个 nginx 镜像的容器再进行修改一样基础镜像是必须指定的。而 FROM 就是指定基础镜像因此一个 Dockerfile 中 FROM是必备的指令并且必须是第一条指令。
在 Docker Store 上有非常多的高质量的官方镜像有可以直接拿来使用的服务类的镜像如 nginx、redis、mongo、mysql、httpd、php、tomcat 等也有一些方便开发、构建、运行各种语言应用的镜像如node、openjdk、python、ruby、golang等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。
如果没有找到对应服务的镜像官方镜像中还提供了一些更为基础的操作系统镜像如ubuntu、debian、centos、fedora、alpine 等这些操作系统的软件库为我们提供了更广阔的扩展空间。 除了选择现有镜像为基础镜像外Docker 还存在一个特殊的镜像名为 scratch。这个镜像是虚拟的概念并不实际存在它表示一个空白的镜像。
FROM scratch
...如果你以 scratch 为基础镜像的话意味着你不以任何镜像为基础接下来所写的指令将作为镜像第一层开始存在。 不以任何系统为基础直接将可执行文件复制进镜像的做法并不罕见比如swarm、coreos/etcd。对于 Linux 下静态编译的程序来说并不需要有操作系统提供运行时支持所需的一切库都已经在可执行文件里了因此直接 FROM scratch 会让镜像体积更加小巧。
使用 Go 语言开发的应用很多会使用这种方式来制作镜像这也是为什么有人认为 Go 是特别适合容器微服务架构的语言的原因之一。
1.3、RUN 执行命令
RUN 指令是用来执行命令行命令的。由于命令行的强大能力RUN 指令在定制镜像时是最常用的指令之一。其格式有两种
shell 格式RUN 命令就像直接在命令行中输入的命令一样。刚才写的 Dockerfile 中的 RUN 指令就是这种格式。
RUN echo h1Hello, Docker!/h1 /usr/share/nginx/html/index.htmlexec 格式 RUN [“可执行文件”, “参数1”, “参数2”]这更像是函数调用中的格式。
既然 RUN 就像 Shell 脚本一样可以执行命令那么我们是否就可以像 Shell 脚本一样把每个命令对应一个 RUN 呢比如这样
FROM debian:jessieRUN 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-components1
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:jessieRUN buildDepsgcc 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-components1 \ 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 制作出了很臃肿的镜像的原因之一就是忘记了每一层构建的最后一定要清理掉无关文件。
1.4、构建镜像
再回到之前定制的 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 h1Hello, 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构建成功后我们可以像之前运行 nginx:v2 那样来运行这个镜像其结果会和 nginx:v2一样。
1.5、镜像构建上下文Context
如果注意会看到 docker build 命令最后有一个 .. 表示当前目录而 Dockerfile 就在当前目录因此不少初学者以为这个路径是在指定 Dockerfile 所在路径这么理解其实是不准确的。如果对应上面的命令格式你可能会发现这是在指定上下文路径。那么什么是上下文呢
首先我们要理解 docker build 的工作原理。Docker 在运行时分为 Docker 引擎也就是服务端守护进程和客户端工具。Docker 的引擎提供了一组 REST API被称为 Docker Remote 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以及会将其置于镜像构建上下文目录中。
1.6、其他 docker build 的用法
1.6.1、直接用 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 这个项目、切换到指定分支、并进入到指定目录后开始构建。
#1.6.2、用给定的 tar 压缩包构建
$ docker build http://server/context.tar.gz如果所给出的 URL 不是个 Git repo而是个 tar 压缩包那么 Docker 引擎会下载这个包并自动解压缩以其作为上下文开始构建。
1.6.3、从标准输入中读取 Dockerfile 进行构建
docker build - Dockerfile或
cat Dockerfile | docker build -如果标准输入传入的是文本文件则将其视为 Dockerfile并开始构建。这种形式由于直接从标准输入中读取 Dockerfile 的内容它没有上下文因此不可以像其他方法那样可以将本地文件 COPY 进镜像之类的事情。
1.6.4、从标准输入中读取上下文压缩包进行构建
$ docker build - context.tar.gz如果发现标准输入的文件格式是 gzip、bzip2 以及 xz 的话将会使其为上下文压缩包直接将其展开将里面视为上下文并开始构建。
二、Dockerfile 指令
我们已经介绍了 FROMRUN还提及了 COPY, ADD其实 Dockerfile 功能很强大它提供了十多个指令。下面我们继续讲解其他的指令。
2.1、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 进行管理的时候。
2.2、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。
2.3、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 命令则是希望 upstart 来以后台守护进程形式启动 nginx 服务。而刚才说了 CMD service nginx start 会被理解为 CMD [ “sh”, “-c”, “service nginx start”]因此主进程实际上是 sh。那么当 service nginx start 命令结束后sh 也就结束了sh 作为主进程退出了自然就会令容器退出。
正确的做法是直接执行 nginx 可执行文件并且要求以前台形式运行。比如
CMD [nginx, -g, daemon off;]2.4、ENTRYPOINT ENTRYPOINT 的格式和 RUN 指令格式一样分为 exec 格式和 shell格式。
ENTRYPOINT 的目的和 CMD 一样都是在指定容器启动程序及参数。ENTRYPOINT 在运行时也可以替代不过比 CMD 要略显繁琐需要通过 docker run 的参数 –entrypoint 来指定。
当指定了 ENTRYPOINT 后CMD 的含义就发生了改变不再是直接的运行其命令而是将 CMD 的内容作为参数传给 ENTRYPOINT 指令换句话说实际执行时将变为
ENTRYPOINT CMD那么有了 CMD 后为什么还要有 ENTRYPOINT 呢这种有什么好处么让我们来看几个场景。
2.4.1、场景一让镜像变成像命令一样使用
假设我们需要一个得知自己当前公网 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
当前 IP160.155.224.xx 来自XX市 联通这么看起来好像可以直接把镜像当做命令使用了不过命令总有参数如果我们希望加参数呢比如从上面的 CMD 中可以看到实质的命令是 curl那么如果我们希望显示 HTTP 头信息就需要加上-i参数。那么我们可以直接加 -i 参数给 docker run myip 么
$ docker run myip -i
docker: Error response from daemon: invalid header field value oci runtime error: container_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
当前 IP160.155.224.xx 来自XX市 联通$ 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; charsetUTF-8
Vary: Accept-Encoding
X-Powered-By: PHP/5.6.24-1~dotdeb7.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当前 IP160.155.224.xx 来自XX市 联通可以看到这次成功了。这是因为当存在 ENTRYPOINT 后CMD 的内容将会作为参数传给 ENTRYPOINT而这里 -i 就是新的 CMD因此会作为参数传给 curl从而达到了我们预期的效果。
2.4.2、场景二应用运行前的准备工作
启动容器就是启动主进程但有些时候启动主进程前需要一些准备工作。
比如 mysql 类的数据库可能需要一些数据库配置、初始化的工作这些工作要在最终的 mysql 服务器运行之前解决。
此外可能希望避免使用 root 用户去启动服务从而提高安全性而在启动服务前还需要以 root 身份执行一些必要的准备工作最后切换到服务用户身份启动服务。或者除了服务外其它命令依旧可以使用 root 身份执行方便调试等。
这些准备工作是和容器 CMD 无关的无论 CMD 为什么都需要事先进行一个预处理的工作。这种情况下可以写一个脚本然后放入 ENTRYPOINT 中去执行而这个脚本会将接到的参数也就是 CMD作为命令在脚本最后执行。比如官方镜像 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 为 docker-entrypoint.sh 脚本。
#!/bin/sh
...
# allow the container to be started with --user
if [ $1 redis-server -a $(id -u) 0 ]; thenchown -R redis .exec su-exec redis $0 $
fiexec $该脚本的内容就是根据 CMD 的内容来判断如果是 redis-server 的话则切换到 redis 用户身份启动服务器否则依旧使用 root 身份执行。比如
$ docker run -it redis id
uid0(root) gid0(root) groups0(root)2.5、ENV
格式有两种
ENV key valueENV key1value1 key2value2…
这个指令很简单就是设置环境变量而已无论是后面的其它指令如 RUN还是运行时的应用都可以直接使用这里定义的环境变量。
ENV VERSION1.0 DEBUGon \NAMEHappy Feet这个例子中演示了如何换行以及对含有空格的值用双引号括起来的办法这和 Shell 下的行为是一致的。
定义了环境变量那么在后续的指令中就可以使用这个环境变量。比如在官方 node 镜像 Dockerfile 中就有类似这样的代码
ENV NODE_VERSION 7.2.0RUN curl -SLO https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.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-components1 \ 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 制作更多的镜像只需使用不同的环境变量即可。
2.6、VOLUME
格式为
VOLUME [“路径1”, “路径2”…]VOLUME 路径
之前我们说过容器运行时应该尽量保持容器存储层不发生写操作对于数据库类需要保存动态数据的应用其数据库文件应该保存于卷(volume)中后面的章节我们会进一步介绍 Docker 卷的概念。
为了防止运行时用户忘记将动态文件所保存目录挂载为卷在 Dockerfile 中我们可以事先指定某些目录挂载为匿名卷这样在运行时如果用户不指定挂载其应用也可以正常运行不会向容器存储层写入大量数据。
VOLUME /data这里的 /data 目录就会在运行时自动挂载为匿名卷任何向 /data 中写入的信息都不会记录进容器存储层从而保证了容器存储层的无状态化。当然运行时可以覆盖这个挂载设置。比如
docker run -d -v mydata:/data xxxx在这行命令中就使用了 mydata 这个命名卷挂载到了 /data 这个位置替代了 Dockerfile 中定义的匿名卷的挂载配置。
2.7、EXPOSE
格式为 EXPOSE 端口1 [端口2…]。 EXPOSE 指令是声明运行时容器提供服务端口这只是一个声明在运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处一个是帮助镜像使用者理解这个镜像服务的守护端口以方便配置映射另一个用处则是在运行时使用随机端口映射时也就是 docker run -P 时会自动随机映射 EXPOSE 的端口。
此外在早期 Docker 版本中还有一个特殊的用处。以前所有容器都运行于默认桥接网络中因此所有容器互相之间都可以直接访问这样存在一定的安全性问题。于是有了一个 Docker 引擎参数 –iccfalse当指定该参数后容器间将默认无法互访除非互相间使用了 –links 参数的容器才可以互通并且只有镜像中 EXPOSE 所声明的端口才可以被访问。这个 –iccfalse 的用法在引入了 docker network 后已经基本不用了通过自定义网络可以很轻松的实现容器间的互联与隔离。
要将 EXPOSE 和在运行时使用 -p 宿主端口:容器端口 区分开来。-p是映射宿主端口和容器端口换句话说就是将容器的对应端口服务公开给外界访问而 EXPOSE 仅仅是声明容器打算使用什么端口而已并不会自动在宿主进行端口映射。
2.8、WORKDIR
格式为 WORKDIR 工作目录路径。
使用 WORKDIR 指令可以来指定工作目录或者称为当前目录以后各层的当前目录就被改为指定的目录如该目录不存在WORKDIR 会帮你建立目录。
之前提到一些初学者常犯的错误是把 Dockerfile 等同于 Shell 脚本来书写这种错误的理解还可能会导致出现下面这样的错误
UN cd /app
RUN echo hello world.txt如果将这个 Dockerfile 进行构建镜像运行后会发现找不到 /app/world.txt 文件或者其内容不是 hello。原因其实很简单在 Shell 中连续两行是同一个进程执行环境因此前一个命令修改的内存状态会直接影响后一个命令而在 Dockerfile 中这两行 RUN 命令的执行环境根本不同是两个完全不同的容器。这就是对 Dockerfile 构建分层存储的概念不了解所导致的错误。
之前说过每一个 RUN 都是启动一个容器、执行命令、然后提交存储层文件变更。第一层 RUN cd /app 的执行仅仅是当前进程的工作目录变更一个内存上的变化而已其结果不会造成任何文件变更。而到第二层的时候启动的是一个全新的容器跟第一层的容器更完全没关系自然不可能继承前一层构建过程中的内存变化。
因此如果需要改变以后各层的工作目录的位置那么应该使用 WORKDIR指令。