述
上文中,简单的使用了一个Dockerfile构建镜像,使用到了三个指令就是FROM
,MAINTAINER
和RUN
,本文在来看一些其他的指令.他们的作用以及使用方式
指令详解
COPY 复制文件
格式
1 | COPY <源路径>... <目标路径> |
和RUN指令一样,也是两种格式,一种类似于命令行,一种类似于函数调用,COPY指令将从构建上下文目录中<源路径>
的文件/目录复制到新的一层的镜像内的<目标路径>
位置. 比如:1
COPY package.json /usr/src/app/
<源路径>
可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的 filepath.Match 规则,如:1
2COPY hom* /mydir/
COPY hom?.txt /mydir/
<目标路径>
可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用WORKDIR指令来指定).目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录.
此外,还需要注意一点,使用 COPY 指令,源文件的各种元数据都会保留.比如读、写、执行权限、文件变更时间等.这个特性对于镜像定制很有用.特别是构建相关文件都在使用 Git 进行管理的时候.
ADD 更高级的复制文件
格式
1 | ADD <源路径>... <目标路径> |
ADD 指令和 COPY 的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。
比如 <源路径> 可以是一个 URL,这种情况下,Docker 引擎会试图去下载这个链接的文件放到 <目标路径> 去。下载后的文件权限自动设置为 600,如果这并不是想要的权限,那么还需要增加额外的一层 RUN 进行权限调整,另外,如果下载的是个压缩包,需要解压缩,也一样还需要额外的一层 RUN 指令进行解压缩。所以不如直接使用 RUN 指令,然后使用 wget 或者 curl 工具下载,处理权限、解压缩、然后清理无用文件更合理。因此,这个功能其实并不实用,而且不推荐使用。
如果 <源路径> 为一个 tar 压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的情况下,ADD 指令将会自动解压缩这个压缩文件到 <目标路径> 去。
但在某些情况下,如果我们真的是希望复制个压缩文件进去,而不解压缩,这时就不可以使用 ADD 命令了。
另外需要注意的是,ADD 指令会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。
因此在 COPY 和 ADD 指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用 COPY 指令,仅在需要自动解压缩的场合使用 ADD。
在使用该指令的时候还可以加上 –chown=\<user>:\<group> 选项来改变文件的所属用户及所属组。1
2
3
4ADD --chown=55:mygroup files* /mydir/
ADD --chown=bin files* /mydir/
ADD --chown=1 files* /mydir/
ADD --chown=10:11 files* /mydir/
CMD 设置容器启动时执行的操作
指定容器启动时执行的命令,每个Dockerfile只能有一条CMD命令
格式
使用 exec 执行,推荐方式:1
CMD ["executable","param1","param2"]
Shell格式:1
CMD <命令>
参数列表格式:1
CMD ["参数1", "参数2"...]
在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数。
这里与RUN指令的区别就是,RUN是在构建容器的时候执行的,CMD是在容器运行的时候执行的.
ENTRYPOINT 入口点
ENTRYPOINT用于给容器配置一个可执行程序,也就是说,每次使用镜像创建容器时,通过 ENTRYPOINT 指定的程序都会被设置为默认程序
格式
数组/exec格式,推荐:1
ENTRYPOINT ["executable", "param1", "param2"]
Shell格式1
ENTRYPOINT command param1 param2
ENTRYPOINT 与 CMD 非常类似,不同的是通过docker run
执行的命令不会覆盖 ENTRYPOINT,而docker run
命令中指定的任何参数,都会被当做参数再次传递给 ENTRYPOINT,Dockerfile中只允许有一个 ENTRYPOINT 命令,多指定时会覆盖前面的设置,而只执行最后的 ENTRYPOINT 指令.
docker run
运行容器时指定的参数都会被传递给 ENTRYPOINT ,且会覆盖 CMD 命令指定的参数.如,执行docker run \<image> -d时,-d 参数将被传递给入口点.
也可以通过docker run --entrypoint
重写 ENTRYPOINT 入口点.如:可以像下面这样指定一个容器执行程序:1
ENTRYPOINT ["/usr/bin/nginx"]
完整构建代码:1
2
3
4
5
6
7
8
9# Version: 0.0.3
FROM ubuntu:16.04
MAINTAINER 何民三 "cn.liuht@gmail.com"
RUN apt-get update
RUN apt-get install -y nginx
RUN echo 'Hello World, 我是个容器' \
> /var/www/html/index.html
ENTRYPOINT ["/usr/sbin/nginx"]
EXPOSE 80
使用docker build构建镜像,并将镜像指定为 itbilu/test:1
docker build -t="itbilu/test" .
构建完成后,使用itbilu/test启动一个容器:1
docker run -i -t itbilu/test -g "daemon off;"
在运行容器时,我们使用了-g "daemon off;"
,这个参数将会被传递给 ENTRYPOINT,最终在容器中执行的命令为 /usr/sbin/nginx -g "daemon off;"
ENV 设置环境变量
这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN,还是运行时的应用,都可以直接使用这里定义的环境变量。
格式
1 | ENV <key> <value> |
1 | ENV VERSION=1.0 DEBUG=on \ |
这个例子中演示了如何换行,以及对含有空格的值用双引号括起来的办法,这和 Shell 下的行为是一致的。
ARG构建参数
ARG用于指定传递给构建运行时的变量:1
ARG <name>[=<default value>]
如,通过ARG指定两个变量:1
2ARG site
ARG build_user=IT笔录
以上我们指定了 site 和 build_user 两个变量,其中 build_user 指定了默认值。在使用 docker build 构建镜像时,可以通过 –build-arg \<varname>=\<value> 参数来指定或重设置这些变量的值。
1 | docker build --build-arg site=itiblu.com -t itbilu/test . |
这样我们构建了 itbilu/test 镜像,其中site会被设置为 itbilu.com,由于没有指定 build_user,其值将是默认值 IT 笔录。
VOLUME 定义匿名卷
格式
1 | VOLUME ["<路径1>", "<路径2>"...] |
之前我们说过,容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中,后面的章节我们会进一步介绍 Docker 卷的概念。为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 Dockerfile 中,我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。
1 | VOLUME /data |
这里的 /data
目录就会在运行时自动挂载为匿名卷,任何向 /data
中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。当然,运行时可以覆盖这个挂载设置。比如:1
docker run -d -v mydata:/data xxxx
在这行命令中,就使用了 mydata 这个命名卷挂载到了 /data 这个位置,替代了 Dockerfile 中定义的匿名卷的挂载配置。
EXPOSE 声明端口
格式
1 | 格式为 EXPOSE <端口1> [<端口2>...] |
EXPOSE 指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口。
要将 EXPOSE 和在运行时使用 -p <宿主端口>:<容器端口> 区分开来。-p,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 EXPOSE 仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。
WORKDIR 指定工作目录
格式
1 | WORKDIR <工作目录路径> |
使用 WORKDIR 指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR 会帮你建立目录。
之前提到一些初学者常犯的错误是把 Dockerfile 等同于 Shell 脚本来书写,这种错误的理解还可能会导致出现下面这样的错误:1
2RUN cd /app
RUN echo "hello" > world.txt
如果将这个 Dockerfile 进行构建镜像运行后,会发现找不到 /app/world.txt 文件,或者其内容不是 hello。原因其实很简单,在 Shell 中,连续两行是同一个进程执行环境,因此前一个命令修改的内存状态,会直接影响后一个命令;而在 Dockerfile 中,这两行 RUN 命令的执行环境根本不同,是两个完全不同的容器。这就是对 Dockerfile 构建分层存储的概念不了解所导致的错误。
之前说过每一个 RUN 都是启动一个容器、执行命令、然后提交存储层文件变更。第一层 RUN cd /app 的执行仅仅是当前进程的工作目录变更,一个内存上的变化而已,其结果不会造成任何文件变更。而到第二层的时候,启动的是一个全新的容器,跟第一层的容器更完全没关系,自然不可能继承前一层构建过程中的内存变化。
因此如果需要改变以后各层的工作目录的位置,那么应该使用 WORKDIR 指令。
USER 指定当前用户
格式
1 | USER <用户名>[:<用户组>] |
USER 指令和 WORKDIR 相似,都是改变环境状态并影响以后的层。WORKDIR 是改变工作目录,USER 则是改变之后层的执行 RUN, CMD 以及 ENTRYPOINT 这类命令的身份。
当然,和 WORKDIR 一样,USER 只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。
1 | RUN groupadd -r redis && useradd -r -g redis redis |
如果以 root 执行的脚本,在执行期间希望改变身份,比如希望以某个已经建立好的用户来运行某个服务进程,不要使用 su 或者 sudo,这些都需要比较麻烦的配置,而且在 TTY 缺失的环境下经常出错。建议使用 gosu
1 | # 建立 redis 用户,并使用 gosu 换另一个用户执行命令 |
HEALTHCHECK 健康检查
格式
1 | HEALTHCHECK [选项] CMD <命令>:设置检查容器健康状况的命令 |
HEALTHCHECK 指令是告诉 Docker 应该如何进行判断容器的状态是否正常 当在一个镜像指定了 HEALTHCHECK 指令后,用其启动容器,初始状态会为 starting,在 HEALTHCHECK 指令检查成功后变为 healthy,如果连续一定次数失败,则会变为 unhealthy。
HEALTHCHECK 支持下列选项:
--interval=<间隔>
:两次健康检查的间隔,默认为 30 秒;--timeout=<时长>
: 健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认 30 秒;--retries=<次数>
:当连续失败指定次数后,则将容器状态视为 unhealthy,默认 3 次。
和 CMD, ENTRYPOINT 一样,HEALTHCHECK 只可以出现一次,如果写了多个,只有最后一个生效。