目录
Dockerfile详解
1. 前言
我们可以把对容器的所有操作命令都记录到一个文件里,就像是写脚本程序。
之后用 docker build 命令以此文件为基础制作一个镜像,并会自动提交到本地仓库。
这样的话镜像的构建会变的透明化,对镜像的维护起来也更加简单,只修改这个文件即可。
同时分享也更加简单快捷,因为只要分享这个文件即可。
Dokcerfile 是一个普通的文本文件,文件名一般叫 Dockerfile
其中包含了一系列的指令(Instruction), 每一条指令都会构建一层,就是描述该层是如何创建的。
示例:
编辑 Dockerfile
文件
[root@localhost ~]# mkdir centos_dockerfile
[root@localhost ~]# cd centos_dockerfile/
[root@localhost centos_dockerfile]# vi Dockerfile
FROM centos:latest
LABEL maintainer="yangge <yangge@qf.com>" description="Install tree vim*"
RUN rpm -qa | grep tree || yum install -y tree vim*
指令介绍:
FORM
定义一个基础镜像
LABEL
定义一些元数据信息,比如作者、版本、关于镜像的描述信息
RUN
执行命令行的命令
编辑完,保存退出
2. 开始构建镜像
命令语法格式:
docker bulid -t 仓库名/镜像名:tag .
docker build [选项] <上下文路径/URL/->
示例:
[root@localhost centos_dockerfile]# docker build -t centos:1.20 .
Sending build context to Docker daemon 2.048kB
Step 1/3 : FROM centos:latest
---> e934aafc2206
Step 2/3 : LABEL maintainer="shark<dockerhub@163.com>" description="Install tree vim*"
---> Using cache
---> 1207b2848015
Step 3/3 : RUN rpm -qa | grep tree || yum install -y tree vim*
---> Running in 33d321b249d7
Loaded plugins: fastestmirror, ovl
Determining fastest mirrors
...略...
Complete!
Removing intermediate container 33d321b249d7
---> adc30981bc84
Successfully built adc30981bc84 # 表示构建成功
Successfully tagged centos:1.20 # TAG 标签
构建镜像的上下文(context)
这个 . 表示当前目录,这实际上是在指定上下文的目录是当前目录,docker build 命令会将该目录下的内容打包交给 Docker 引擎以帮助构建镜像。
docker build 命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。
最佳实战
一般来说,应该会将 Dockerfile 置于一个空目录下,或者项目根目录下。如果该目录下没有所需文件,那么应该把所需文件复制一份过来。如果目录下有些东西确实不希望构建时传给 Docker 引擎,那么可以用 .gitignore 一样的语法写一个 .dockerignore,该文件是用于剔除不需要作为上下文传递给 Docker 引擎的
3. Dockerfile 详解
FROM 指令
主要作用是指定一个镜像作为构建自定义镜像的基础镜像,在这个基础镜像之上进行修改定制。
这个指令是 Dockerfile 中的必备指令,同时也必须是第一条指令。
在 Docker Store 上有很多高质量的官方镜像,可以直接作为我们的基础镜像。
作为服务类的,如 Nginx Mongo 等
用于开发的, 如 Python golang
操作系统类, 如 Centos ubuntu
除了一些现有的镜像,Docker 还有一个特殊的镜像 scratch
这个镜像是虚拟的,表示空白镜像
FORM scratch
...
这以为着这将不以任何镜像为基础镜像。
可以把可执行的二进制文件复制到镜像中直接执行,容器本身就是和宿主机共享 Linux内核的。
使用 Go 语言开发的应用很多会使用这种方式来制作镜像,这也是为什么有人认为 Go 是特别适合容器微服务架构的语言的原因之一。
关于 Alpine
官网:https://alpinelinux.org/
WIKI https://wiki.alpinelinux.org/wiki/Main_Page
Alpine Linux是一款独立的非商业性通用Linux发行版,专为那些了解安全性,简单性和资源效率的高级用户而设计。
小
Alpine Linux围绕musl libc和busybox构建。这使得它比传统的GNU / Linux发行版更小,更节省资源。一个容器需要不超过8 MB的空间,而对磁盘的最小安装需要大约130 MB的存储空间。您不仅可以获得完整的Linux环境,还可以从存储库中选择大量的软件包。
二进制软件包被缩减和拆分,使您可以更好地控制安装的内容,从而使您的环境尽可能地小巧高效。
简单
Alpine Linux是一个非常简单的发行版,它会尽量避免使用。它使用自己的包管理器,称为apk,OpenRC init系统,脚本驱动的设置,就是这样!这为您提供了一个简单,清晰的Linux环境,没有任何噪音。然后,您可以添加项目所需的软件包,因此无论是构建家用PVR还是iSCSI存储控制器,薄型邮件服务器容器或坚如磐石的嵌入式交换机,其他都不会挡道。
安全
Alpine Linux的设计考虑到了安全性。内核修补了一个非官方的grsecurity / PaX端口,并且所有的用户级二进制文件被编译为位置独立可执行文件(PIE)和堆栈粉碎保护。这些主动安全功能可防止利用整个类别的零日等漏洞。
LABEL 指令
LABEL 指令用于指定一个镜像的描述信息
该LABEL指令将元数据添加到镜像中。
LABEL是一个键值对。
要在LABEL值中包含空格,请像在命令行解析中一样使用引号和续行符\。
几个用法示例:
LABEL maintainer="yangge@qf.com"
LABEL "com.example.vendor"="ACME Incorporated"
LABEL com.example.label-with-value="foo"
LABEL version="1.0"
LABEL description="This text illustrates \
that label-values can span multiple lines."
一个镜像可以有多个LABEL标签。您可以在一行中指定多个标签。并且目前的版本不再会影响到镜像的大小了。
但是仍然可以把他们写在一行或用反斜线进行续航
LABEL multi.label1="value1" multi.label2="value2" other="value3"
1
LABEL multi.label1="value1" \
multi.label2="value2" \
other="value3"
有继承关系的镜像,标签也会有面向对象编程中继承的关系和特性
要查看镜像的 LABEL 信息,请使用该docker inspect命令。
ENV 指令
用于设置环境变量
格式有两种:
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...
示例:
推荐的方式,易读
ENV VERSION=1.0 DEBUG=on \
NAME="Happy Feet"
不推荐的方式,不易读
ENV NODE_VERSION 7.2.0
其他指令使用:
RUN echo $NODE_VERSION
...
下列指令可以支持环境变量: ADD、COPY、ENV、EXPOSE、LABEL、USER、WORKDIR、VOLUME、STOPSIGNAL、ONBUILD。
RUN 指令
RUN 指令是在容器内执行 shell 命令,默认会是用 /bin/sh -c 的方式执行。
执行命令的两种方式
RUN <command>(shell形式,该命令在shell中运行)
RUN ["executable", "param1", "param2"](exec形式)
之前说过,Dockerfile 中每一个指令都会建立一层,RUN 也不例外。每一个 RUN 的行为,就和刚才我们手工建立镜像的过程一样:新建立一层,在其上执行这些命令,执行结束后,commit 这一层的修改,构成新的镜像。
注意:
Union FS 是有最大层数限制的,比如 AUFS,曾经是最大不得超过 42 层,现在是不得超过 127 层。
所以,在使用 shell 方式,尽量多的使用续行符\
RUN /bin/bash -c 'source $HOME/.bashrc; \
echo $HOME'
写 Dockerfile 的时候,要经常提醒自己,这并不是在写 Shell 脚本,而是在定义每一层该如何构建。
注意当使用 exec 方式时,需要明确指定 shell 路径,否则变量可能不会生效
FROM centos
ENV name="yangge"
RUN ["/bin/echo", "$name"]
可以看到 $name 被作为普通的字符串输出了,因为 $name 是 shell 中的用法,而这里里并没有 使用到 shell
下面是正确的做法
FROM alpine
ENV name="yangge"
RUN ["/bin/sh", "-c", "/bin/echo $name"]
注意: exec的方式下,列表中的内容会被解析为JSON数组,这意味着您必须在单词周围使用双引号(“) 而非单引号(’)。
CMD 指令
Dockerfile
中只能有一条CMD
指令。如果列出多个,CMD
则只有最后一个CMD
会生效。
CMD
主要目的是为运行容器时提供默认值
Docker
不是虚拟机,容器就是进程,CMD
指令就是用于指定默认的容器主进程的启动命令的。在启动(运行)一个容器时可以指定新的命令来替代镜像设置中的这个默认命令。
可以包含可执行文件,当然也可以省略。
CMD
指令的格式和 RUN
相似,也是两种格式:
shell
格式:CMD <命令>
exec
格式:CMD ["可执行文件", "参数1", "参数2"...]
参数列表格式:CMD ["参数1", "参数2"...]
。在指定了 ENTRYPOINT
指令后,用 CMD
指定具体的参数。
注意:不要混淆`RUN` 和 `CMD`。`RUN`实际上运行一个命令并提交结果; `CMD`在构建时不执行任何操作,但指定镜像的默认命令。
Docker 不是虚拟机,容器内没有后台服务的概念。
不要期望这样启动一个程序到后台:
CMD systemctl start nginx
这行被 Docker 理解为:
CMD ["sh" "-c" "systemctl start nginx"]
对于容器而言,其启动程序就是容器的应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。
就像上面的示例中,主进程是 sh , 那么当 service nginx start 命令结束后,sh 也就结束了,sh 作为主进程退出了,自然就会使容器退出。
正确的做法是直接执行 nginx 这个可执行文件,并且关闭后台守护的方式,使程序在前台运行。
CMD ["nginx", "-g", "daemon off;"]
ENTRYPOINT 指令
ENTRYPOINT
的目的和 CMD 一样,都是在指定容器的启动程序及参数。
ENTRYPOINT
在运行时也可以被替代,不过比 CMD 要略显繁琐,需要通过 docker run 的参数 --entrypoint
来指定。
ENTRYPOINT
的格式和 RUN 指令格式一样,也分为 exec 格式和 shell 格式。
当指定了 ENTRYPOINT
后,CMD 的含义就发生了改变,不再是直接的运行其命令,而是将 CMD 的内容作为参数传给 ENTRYPOINT
指令,也就是实际执行时,将变为:
<ENTRYPOINT> "<CMD>"
有了 CMD 后,为什么还要有 ENTRYPOINT 呢?
这种 <ENTRYPOINT> "<CMD>"
给我们带来了什么好处么?
让我们来看几个场景。
场景一:让镜像变成像命令一样使用
CMD 方式
FROM centos
RUN yum update \
&& yum install -y curl
CMD [ "curl", "-s", "http://ip.cn" ]
构建镜像后, 运行容器
# docker run --rm centos-echo-ip-cmd
执行下面命令会报错
# docker run --rm centos-echo-ip-cmd -i
我们可以看到报错,executable file not found。之前我们说过,跟在镜像名后面的是 command,运行时会替换 CMD 的默认值。因此这里的 -i 并不是添加在原来的 curl -s http://ip.cn 后面。
而是替换了原来的 CMD,变成了 CMD ["-i"],而 -i 根本不是命令,所以报了可执行文件找不到。
所以应该使用 ENTRYPOINT 方式
FROM centos
RUN yum install -y curl
ENTRYPOINT ["curl", "-s", "http://ip.cn"]
再次构建镜像后, 运行容器
# docker run --rm centos-echo-ip-entrypoint
# docker run --rm centos-echo-ip-entrypoint -i
这样的话, 最终的指令就变成 ENTRYPOINT ["curl", "-s", "http://ip.cn", "-i"]
WORKDIR 指令
用于声明当前的工作目录,以后各层的当前目录就被改为指定的目录。
格式为 WORKDIR <工作目录路径>
。
如该目录不存在,WORKDIR 会帮你建立目录。
再次强调!不要以为编写 Dockerfiel 是在写 shell 脚本。
下面是一个错误示例:
RUN cd /app
RUN echo "hello" > world.txt
如果将这个 Dockerfile 进行构建镜像运行后,会发现找不到 /app/world.txt
文件,或者其内容不是 hello。
原因其实很简单,这两行 RUN 命令的执行环境根本不同,是两个完全不同的容器。这就是对 Dockerfile 构建分层存储的概念不了解所导致的错误。
之前说过每一个 RUN 都是启动一个容器、执行命令、然后提交存储层文件变更。
两行 RUN 分别构建了并启动了各自全新的容器。
因此如果需要改变以后各层的工作目录的位置,那么应该使用 WORKDIR 指令。
FROM alpine
WORKDIR /a/b
RUN touch a_b_f.txt
WORKDIR /a
RUN touch a_f.txt
[root@localhost workdir]# docker run -it alpine:workdir /bin/sh
/a # ls
a_f.txt b
/a # cd b
/a/b # ls
a_b_f.txt
COPY 指令
格式:
COPY <源路径>... <目标路径>
COPY ["<源路径1>",... "<目标路径>"]
和 RUN 指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。
<目标路径>
可以是容器内的绝对路径,也可以是相对于 WORKDIR 指定的工作目录的相对路径。目标路径不需要事先创建,如果目录不存在会在复制文件前先被创建。
COPY
指令将会从构建的上下文目录中,把源路径的文件或目录复制到新的一层的镜像内的 <目标路径> 位置。比如:
COPY qf.json /usr/src/app/
注意下面是错误的
COPY qf.json /usr/src/app
这样会把 qf.json 拷贝成为 /usr/src/ 目录下的 app 文件
<源路径>
可以是多个,支持通配符,如:
COPY qf* /app/
COPY q?.txt /app/
使用 COPY 指令,源文件的各种元数据都会保留。
比如读、写、执行权限、文件变更时间等。
COPY 命令的源如果是文件夹,复制的是文件夹的内容而不是其本身
ADD 指令
ADD
指令和 COPY 的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。
支持自动解压缩,压缩格式支持: gzip, bzip2 以及 xz
官方推荐使用 COPY 进行文件的复制。
ADD 指定会使构建镜像时的缓存失效,导致构建镜像的速度很慢。
COPY 和 ADD 指令中选择的原则,所有的文件复制均使用 COPY 指令,仅在需要自动解压缩的场合使用 ADD。
ADD qf.tar.gz /
USER 指令
USER
则是改变执行 RUN, CMD 以及 ENTRYPOINT 这类命令的身份。
这个用户必须是事先在容器内存在(建立好)的,否则无法切换。
如果以 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 指令后,用其启动容器后的状态变化会是下面的演变过程:
初始状态会为 starting
在 HEALTHCHECK 指令检查成功后变为 healthy
如果连续一定次数失败,则会变为 unhealthy。
HEALTHCHECK 支持下列选项:
--interval=<间隔>
:两次健康检查的间隔,默认为 30 秒;
--timeout=<时长>
:健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认 30 秒;
--retries=<次数>
:当连续失败指定次数后,则将容器状态视为 unhealthy,默认 3 次。
--start-period=<时长>
: 容器的初始化实长,默认0秒,不计入健康检测时间内。
和 CMD, ENTRYPOINT
一样,HEALTHCHECK
在 Dockerfile
中只可以出现一次,如果写了多个,只有最后一个生效。
后面的命令同样支持 shell 方式和 exec 方式。
命令的返回值决定了该次健康检查的成功与否:
0:成功;1:失败。
示例:
使用 curl 命令来判断 nginx 提供的 web 服务是否正常。
其 Dockerfile 的 HEALTHCHECK 可以这么写:
FROM nginx
COPY index.html /usr/share/nginx/html/index.html
HEALTHCHECK --interval=5s --timeout=3s CMD curl -fs \
http://localhost/ || exit 1
这里设置了每 5 秒检查一次(这里为了试验所以间隔非常短,实际应该相对较长),如果健康检查命令超过 3 秒没响应就视为失败,并且使用 curl -fs http://localhost/ || exit 1 作为健康检查命令。
构建镜像后, 启动容器,并观察容器的状态变化
# docker build -t ali_nginx .
# docker run -d ali_nginx
[root@localhost ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
09a8b90b0f67 ali_nginx "nginx -g 'daemon of…" 4 seconds ago Up 3 seconds (health: starting) 80/tcp vigorous_jang
[root@localhost ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
09a8b90b0f67 ali_nginx "nginx -g 'daemon of…" 19 seconds ago Up 18 seconds (healthy) 80/tcp vigorous_jang
利用元数据查看容器的健康状态
docker inspect --format '{{json .State.Health}}' vigorous_jang | python -m json.tool
ONUILD 指令
ONUILD
指令用于当其他 Dockerfile 以自己为基础镜像时将会运行的命令。
格式:ONBUILD <其它指令>
。
其他指令可以是: 比如 RUN, COPY 等。
更多参考官方 Docker Demo 和官网
1.doker demo
https://github.com/docker-library
2.官网
https://docs.docker.com/engine/reference/builder/
https://docs.docker.com/develop/develop-images/dockerfile_best-practices/