Docker DockerFile
概述
我们可以把刚才的对容器的所有操作命令都记录到一个文件里,就像写更脚本程序。
之后用 docker build 命令以此文件为基础制作一个镜像,并会自动提交到本地仓库。
这样的话镜像的构建会变的透明化,对镜像的维护起来也更加简单,只修改这个文件即可。
同时分享也更加简单快捷,因为只要分享这个文件即可。
Dokcerfile 是一个普通的文本文件,文件名一般叫 Dockerfile
其中包含了一系列的指令(Instruction), 每一条指令都会构建一层,就是描述该层是如何创建的。
小试牛刀
编辑 Dockerfile 文件
1 | cat Dockerfile |
详细内容
1 | FROM centos:6.7 |
指令介绍
- FORM 定义一个基础镜像
- MAINTAINER 指定作者
- LABEL 定义一些元数据信息,比如作者、版本、关于镜像的描述信息
- RUN 行命令行的命令
- EXPOSE 暴露容器端口
- CMD exec模式
构建镜像
命令语法格式
docker bulid -t 仓库名/镜像名:tag .
构建镜像
1 | docker build -t centos:test . |
打印信息
1 | Sending build context to Docker daemon 35.84kB |
上下文(context)
这个 .
表示当前目录,这实际上是在指定上下文的目录是当前目录,docker build
命令会将该目录下的内容打包交给 Docker 引擎以帮助构建镜像。
docker build
命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。
最佳实战
一般来说,应该会将 Dockerfile
置于一个空目录下,或者项目根目录下。如果该目录下没有所需文件,那么应该把所需文件复制一份过来。如果目录下有些东西确实不希望构建时传给 Docker 引擎,那么可以用 .gitignore
一样的语法写一个 .dockerignore
,该文件是用于剔除不需要作为上下文传递给 Docker 引擎的
Dockerfile
的文件名并不要求必须为 Dockerfile
,而且并不要求必须位于上下文目录中,比如可以用 -f ../Dockerfile.qf
参数指定某个文件作为 Dockerfile
。
一般大家习惯性的会使用默认的文件名 Dockerfile
,以及会将其置于镜像构建上下文目录中。
1 | tree . |
1 | docker build -f ../Dockerfile.qf -t alpine:test.qf . |
1 | docker run -it alpine:test.qf /bin/sh |
Dockerfile 详解
FROM 指令
主要作用是指定一个镜像作为构建自定义镜像的基础镜像,在这个基础镜像之上进行修改定制。
这个指令是 Dockerfile 中的必备指令,同时也必须是第一条指令。
在 Docker Store 上有很多高质量的官方镜像,可以直接作为我们的基础镜像。
除了一些现有的镜像,Docker 还有一个特殊的镜像 scratch
这个镜像是虚拟的,表示空白镜像
1 | FORM scratch |
这以为着这将不以任何镜像为基础镜像。
可以把可执行的二进制文件复制到镜像中直接执行,容器本身就是和宿主机共享 Linux内核的。
使用 Go 语言开发的应用很多会使用这种方式来制作镜像,这也是为什么有人认为 Go 是特别适合容器微服务架构的语言的原因之一。
制作 Hello world
安装 gcc
在任意一台 Linux 机器上,安装 gcc
查看有没有安装
1 | rpm -qa gcc glibc-static |
没有的话,进行安装即可
1 | yum install gcc glibc-static |
编辑 C 源代码文件
1 | cat hello.c |
查看文件内容
1 |
|
编译源码
1 | gcc --static hello.c -o hello |
编译好后,测试一下
1 | ls |
编辑 Dockerfile
在有 hello 二进制的文件目录下,编译 Dockerfile 文件,内容如下:
1 | ls |
- ADD 是把当前目录下的 hello 文档拷贝到 容器中的根目录下
- CMD 执行根目录下的 hello 文件
构建新的镜像
注意命令的最后有个
.
1 | docker build -t xiguatian/hello-yangge . |
查看本地仓库验证
1 | docker image ls xiguatian/hello-yange |
可以看到镜像很小
利用新的镜像运行一个容器
1 | docker run --rm qf/hello-yange |
关于 Alpine
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
值中包含空格,请像在命令行解析中一样使用引号和续行符\
。
示例
几个用法示例
1 | LABEL maintainer="yangge@qf.com" |
一个镜像可以有多个LABEL
标签。您可以在一行中指定多个标签。并且目前的版本不再会影响到镜像的大小了。
但是仍然可以把他们写在一行或用反斜线进行续航
1 | LABEL multi.label1="value1" multi.label2="value2" other="value3" |
有继承关系的镜像,标签也会有面向对象编程中继承的关系和特性
要查看镜像的 LABEL
信息,请使用该docker inspect
命令。
ENV 指令
用于设置环境变量
语法
语法格式有两种
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...
示例
推荐的方式,易读
1 | ENV VERSION=1.0 DEBUG=on \ |
不推荐都方式,不易读
1 | ENV NODE_VERSION 7.2.0 |
其他指令使用
1 | 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 方式,尽量多的使用续行符
\
1 | RUN /bin/bash -c 'source $HOME/.bashrc; \ |
写 Dockerfile 的时候,要经常提醒自己,这并不是在写 Shell 脚本,而是在定义每一层该如何构建。
错误示例
注意当使用 exec
方式时,需要明确指定 shell
路径,否则变量可能不会生效
1 | FROM centos |
可以看到
$name
被作为普通的字符串输出了,因为$name
是 shell 中的用法,而这里里并没有 使用到 shell
正确做法
1 | FROM alpine |
注意: 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 不是虚拟机,容器内没有后台服务的概念。
不要期望这样启动一个程序到后台:
1 | CMD systemctl start nginx |
这行被 Docker
理解为:
1 | CMD ["sh" "-c" "systemctl start nginx"] |
对于容器而言,其启动程序就是容器的应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。
就像上面的示例中,主进程是 sh
, 那么当 service nginx start
命令结束后,sh
也就结束了,sh
作为主进程退出了,自然就会使容器退出。
正确的做法是直接执行 nginx
这个可执行文件,并且关闭后台守护的方式,使程序在前台运行。
1 | CMD ["nginx", "-g", "daemon off;"] |
ENTRYPOINT 指令
ENTRYPOINT
的目的和CMD
一样,都是在指定容器的启动程序及参数。
ENTRYPOINT
在运行时也可以被替代,不过比 CMD
要略显繁琐,需要通过 docker run
的参数 --entrypoint
来指定。
ENTRYPOINT
的格式和 RUN
指令格式一样,也分为 exec
格式和 shell
格式。
ENTRYPOINT和CMD区别
当指定了
ENTRYPOINT
后,CMD
的含义就发生了改变,不再是直接的运行其命令,而是将CMD
的内容作为参数传给ENTRYPOINT
指令,也就是实际执行时,将变为:
1 | <ENTRYPOINT> "<CMD>" |
有了 CMD
后,为什么还要有 ENTRYPOINT
呢?
这种 <ENTRYPOINT> "<CMD>"
给我们带来了什么好处么?
示例
让我们来看几个场景。
场景一
让镜像变成像命令一样使用
1 | FROM centos |
构建镜像后, 运行容器
1 | docker run --rm centos-echo-ip-cmd |
执行下面命令会报错
1 | 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
方式
1 | FROM centos |
再次构建镜像后
1 | docker run --rm centos-echo-ip-entrypoint |
运行容器
1 | docker run --rm centos-echo-ip-entrypoint -i |
这样的话, 最终的指令就变成 ENTRYPOINT ["curl", "-s", "http://ip.cn", "-i"]
场景二
应用运行前的准备工作
启动容器就是启动主进程,但有些时候,启动主进程前,需要一些准备工作。
官方镜像
redis
中的示例:
1 | FROM alpine:3.4 |
可以看到其中为 redis 服务创建了 redis 用户,并在最后指定了 ENTRYPOINT
为 docker-entrypoint.sh
脚本。
1 | !/bin/sh |
该脚本的内容就是根据 CMD
的内容来判断,如果是 redis-server
的话,则切换到 redis
用户身份启动服务器,否则依旧使用 root
身份执行。比如:
1 | docker run -it redis id |
注意: ENTRYPOINT
指令不会被 RUN
指令覆盖,而 CMD
指令会被 RUN
指令覆盖
WORKDIR 指令
用于声明当前的工作目录,以后各层的当前目录就被改为指定的目录。
语法
WORKDIR <工作目录路径>
如该目录不存在,WORKDIR
会帮你建立目录。
示例
再次强调!不要以为编写
Dockerfiel
是在写shell
脚本。
错误示例
下面是一个错误示例
1 | RUN cd /app |
如果将这个 Dockerfile
进行构建镜像运行后,会发现找不到 /app/world.txt
文件,或者其内容不是 hello
。
原因其实很简单,这两行 RUN
命令的执行环境根本不同,是两个完全不同的容器。这就是对 Dockerfile
构建分层存储的概念不了解所导致的错误。
之前说过每一个 RUN
都是启动一个容器、执行命令、然后提交存储层文件变更。
两行 RUN
分别构建了并启动了各自全新的容器。
正确示例
因此如果需要改变以后各层的工作目录的位置,那么应该使用
WORKDIR
指令。
1 | FROM alpine |
运行容器
1 | docker run -it alpine:workdir /bin/sh |
COPY 指令
语法格式
COPY <源路径>... <目标路径>
COPY ["<源路径1>",... "<目标路径>"]
和 RUN
指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。
参数解释
<目标路径>
可以是容器内的绝对路径,也可以是相对于 WORKDIR
指定的工作目录的相对路径。目标路径不需要事先创建,如果目录不存在会在复制文件前先被创建。
示例
COPY
指令将会从构建的上下文目录中,把源路径的文件或目录复制到新的一层的镜像内的<目标路径>
位置。
1 | COPY qf.json /usr/src/app/ |
注意下面是错误的
1 | COPY qf.json /usr/src/app |
这样会把 qf.json
拷贝成为 /usr/src/
目录下的 app
文件
<源路径>
可以是多个,支持通配符,如:
1 | COPY qf* /app/ |
使用 COPY
指令,源文件的各种元数据都会保留。
比如读、写、执行权限、文件变更时间等。
ADD 指令
ADD
指令和COPY
的格式和性质基本一致。但是在COPY
基础上增加了一些功能。
支持自动解压缩,压缩格式支持: gzip
, bzip2
以及 xz
官方推荐使用 COPY
进行文件的复制。
ADD
指定会使构建镜像时的缓存失效,导致构建镜像的速度很慢。
COPY
和 ADD
指令中选择的原则,所有的文件复制均使用 COPY
指令,仅在需要自动解压缩的场合使用 ADD
。
1 | ADD qf.tar.gz / |
USER 指令
USER
则是改变执行RUN
,CMD
以及ENTRYPOINT
这类命令的身份。
这个用户必须是事先在容器内存在(建立好)的,否则无法切换。
如果以 root
执行的脚本,在执行期间希望改变身份,比如希望以某个已经建立好的用户来运行某个服务进程,不要使用 su
或者 sudo
,这些都需要比较麻烦的配置,而且在 TTY 缺失的环境下经常出错。建议使用 gosu
。
1 | # 建立 redis 用户,并使用 gosu 换另一个用户执行命令 |
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
可以这么写
1 | FROM centos |
这里设置了每 5 秒检查一次(这里为了试验所以间隔非常短,实际应该相对较长),如果健康检查命令超过 3 秒没响应就视为失败,并且使用 curl -fs http://localhost/ || exit 1
作为健康检查命令。
构建镜像后, 启动容器,并观察容器的状态变化
1 | docker build -t ali_nginx . |
1 | docker ps |
利用元数据查看容器的健康状态
1 | docker inspect --format '{{json .State.Health}}' vigorous_jang | python -m json.tool |
ONBILUD 指令
ONBILUD
指令用于当其他 Dockerfile 以自己为基础镜像时将会运行的命令。
语法
ONBUILD <其它指令>
其他指令可以是: 比如 RUN
, COPY
等。
基础应用场景
假如有两个项目 A 和 B,两个项目想分别有不同的文件
A 项目下的文件
1 | tree A/ |
B 项目下的文件
1 | tree B |
操作示例
现在任意的空目录下创建一个
Dockerfile
文件内容:
1 | $ cat Dockerfile |
接着用这个 Dockerfile 来构建一个所有项目都要使用的一个基础镜像
镜像名字: alpine-base
1 | docker build -t alpine-base . |
当使用这个镜像去运行容器的时候。查看 /root
目录下,可发现并没有任何东西,
说明
COPY . /root/
并没有此次构建镜像的过程中去执行。
1 | docker run --rm alpine-base:latest ls /root/ |
现在我们在使用刚才构建的镜像为项目 A 的基础镜像,来构建 A 项目的镜像
想看看目前 A 项目下的文件:
1 | cd A |
在项目的
A
目录下编写Dockerfile
文件内容如下:
1 | cat Dockerfile |
是的只需要这一行即可
现在让我们来构建
A
项目的镜像
1 | docker build -t alpine-a . |
接着运行以这个镜像
alpine-a:latest
为基础镜像而运行的容器中的/root/
目录下会有A
项目目录下的所有文件:
1 | docker run --rm alpine-a:latest ls /root/ |
B
项目的Dockerfile
的内容:
1 | ls |
同样构建 B 项目的 镜像,运行容器后可以看到
/root/
目录下会有 B 项目目录下的所有文件
1 | docker build -t alpine-b . |
可以看出,ONBUILD
指令后面内容会在,其他镜像以此镜像为基础镜像构建的时候执行。
高级应用场景
python 项目都有自己的依赖包,通常会放在项目根目录下的一个文件,这个文件名叫:
requirements.txt
此文件可以通过如下命令得到:
1 | pip3 freeze > requirements.txt |
内容一般为:
1 | head -3 requirement.txt |
可以使用如下命令来安装这些项目的依赖模块。
1 | pip3 install -r requirement.txt |
现在假设公司有多个 python3 的项目,每个项目都有自己不同的依赖模块。需要为每个项目制定一个 Dockerfile 或者镜像吗?
操作示例
比如有两个项目: CMDB 和 SUPERMAN
下面我们使用 ONBUILD
指令来构建一个基础 python
镜像,
之后两个项目可以不必修改原来的 Dockerfile
就可以部署自己的环境依赖包了。
CMDB 的 Dockerfile
CMDB
1 | tree CMDB/ |
SUPERMAN
1 | $ tree SUPERMAN/ |
使用 ONBUILD 指令构建 Python 基础镜像
1 | $ cat Dockerfile |
之后分别在各自的项目目录下创建自己的 Dockerfile
CMDB 的 Dockerfile
1 | FROM python3-base |
SUPERMAN 的 Dockerfile
1 | FROM python3-base |
这样就可以很简单的实现不同的项目只需要创建一个同样内容的镜像,而会得到自己的环境了。
另外下面的是在 shell 中的执行 python 的命令:
1 | FROM python |
把这个构建成所有项目的基础镜像,名字为: python-onbuild:v1.0
1 | docker build -t python-onbuild:v1.0 . |
其他
python
项目再使用此镜像为基础镜像时,Dockerfile
中只需一行即可:
1 | FROM python-onbuild:v1.0 |