Dockerfile文件是定义镜像的每一层是如何构建的,因为Dockerfile是分层构建并且每一条Dockerfile指令就表示构建一层,当前层是基于上一层为基础构建而来,因此每条Dockerfile指令都需要深思熟虑的编写,确保每一层构建的镜像都不存在无用的东西;Dockerfile语法格式支持Shell类的行尾添加“\”的命令换行、行首“#”进行注释等格式。支持使用“&&”将各个所需命令串联起来作为一条Dockerfile指令。Dockerfile提供了十多个指令:
1. FROM 指定基础镜像
所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制,基础镜像是必须指定的。而FROM 就是指定基础镜像,因此一个 Dockerfile 中 FROM 是必备的指令,并且必须是第一条指令。
- 格式:FROM <基础镜像>
- 可以选取现有的docker镜像作为基础镜像(从远程仓库拉或者上传至本地镜像库),例如:FROM nginx;还有一种特殊镜像为scratch,这样使用FROM scratch,表示使用一个空白的镜像,意思就是不以任何镜像为基础,接下来所写的指令作为镜像第一层开始;例如:不以任何系统为基础,直接将可执行文件复制进镜像,并不需要系统的支持,所需要的一切库都已经在可执行文件中,这样制作出来的镜像更加小巧。
2. RUN 执行命令
RUN指令是用来执行命令行命令的。格式有两种:
- Shell格式:RUN <命令>,就像直接在命令行中输入命令一样,如:
- RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
多条命令可以使用&&连接。
- exec格式:RUN ["可执行文件", "参数1", "参数2"] ,这更像是函数调用中的格式。
3. COPY 复制文件
COPY指令将从构建上下文目录中(源路径)的文件或目录复制到新的一层的镜像内的目标路径位置。格式有两种:一种类似命令行,一种类似函数调用。使用COPY指令,源文件的各种元数据都会被保留,如读权限,写权限,执行权限,文件变更等。
- Shell格式:COPY <源路径1> … <目标路径>
- exec格式:COPY ["<源路径1>",…"<目标路径>"]
源路径可以是多个,可以使用"*","?"等通配符去匹配多个;目标路径可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以使用WOEKDIR指令来指定)
COPY hom* /mydir/
COPY hom?.txt /mydir/
4. ADD 更高级的复制文件
ADD指令和COPY的格式和性质基本一致,只是在COPY的基础上增加了一些功能:
- 源路径可以是一个URL,dockerd会去下载这个链接的文件放到<目标路径>中,下载后的文件权限自动设置为600,如果需要修改权限则需要额外增加一层RUN指令进行权限修改;如果下载的是一个压缩包,还需要再增加一层RUN指令进行解压,有点复杂麻烦,还不如直接使用RUN指令编写多条命令(包括下载、解压、权限、清理等),因此不推荐使用这项功能。
- 源路径可以是一个tar压缩文件包,格式可以是tar.gz,tar.bx2,tar.xz等;ADD指令会自动解压缩这个文件到<目标路径>中,此项功能就非常使用,直接复制一个打包好的文件,会自动帮解压缩;推荐有需要的时候使用此功能。
- 如果需要修改所属用户和组,加上--chown=<user>:<group>命令即可,如 ADD --chown=myuser:mygroup files* /mydir/
但是ADD指令会使镜像构建缓存失效,可能会导致镜像构建变得比较慢,而且ADD指令包含功能复杂,行为不是很清晰;因此如果只是涉及文件复制,直接使用COPY指令就好;如果涉及自动解压缩的场景,使用ADD指令更方便。
5. CMD 容器启动命令
Docker不是虚拟机,运行的docker容器就是一个进程;启动容器的时候就需要制定所运行的程序及参数。CMD指令就是用于指定默认的容器主进程的启动命令的。还有一点需要注意的是,容器中的应用都应该以前台执行,容器内没有后台服务的概念,这点是区别于虚拟机的。在运行时使用docker run -it <container_name or container_id> <CMD> 替换启动命令。该指令只可以出现一次,如果写了多个,只有最后一个生效。
- shell 格式:CMD <命令>
- exec 格式:CMD ["可执行文件", "参数1", "参数2"...]
- 参数列表格式:CMD ["参数1", "参数2"...]。在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数。
6. ENTRYPOINT 入口点
ENTRYPOINT的作用跟CMD的作用一样,都是在指定容器启动程序及参数,ENTRYPOINT在运行时也可以被代替,通过docker run的参数--entrypoit来指定,当制定了ENTRYPOINT指令之后,docker run后面输入的命令或者CMD ["参数1", "参数2"...]命令都会被当做参数传入到ENTRYPOINT后面指令中。该指令只可以出现一次,如果写了多个,只有最后一个生效。
格式:
- shell 格式:ENTRYPOINT <命令>
- exec 格式:ENTRYPOINT ["可执行文件", "参数1", "参数2"...]
使用场景:
- 让镜像像使用命令一样使用:<ENTRYPOINT> "<CMD>" ,当使用docker run <container> <CMD>时后面点的CMD会被当做参数传入到ENTRYPOINT的CMD中执行。
- 让容器运行之前做一些准备工作:ENTRYPOINT ["docker-entrypoint.sh"] ,当使用docker run <container> ["参数1",…]时后面携带的参数会传入ENTRYPOINT 的脚本docker-entrypoint.sh中使用,在脚本中可以先利用这些参数做初始化动作,然后在启动程序。
7. ENV设置环境变量
使用ENV设置的环境变量,后面的其他指令RUN等以及容器运行时的应用,都可以直接使用这里定义的环境变量,使用的是key-value键值对的格式,下列指令可以支持环境变量展开: ADD、COPY、ENV、EXPOSE、FROM、LABEL、USER、WORKDIR、VOLUME、STOPSIGNAL、ONBUILD、RUN。
格式支持多个键值对设置:
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>…
8. ARG 构建参数
ARG指令与ENV指令都是设置环境变量,不同的是ARG构建的环境变量作用于仅限于Dockerfile构建时有效,在容器运行时失效,作用于不一样,ARG相当于函数的参数作用域,类似于局部变量,而ENV相当于全局作用域,类似于全局变量。ARG指令虽然在运行时会失效,但是使用docker history还是可以看到其设置的所有值的。
Dockerfile 中的 ARG 指令是定义参数名称,以及定义其默认值。该默认值可以在构建命令 docker build 中用 --build-arg <参数名>=<值> 来覆盖。
格式:ARG <参数名>[=<默认值>]
例如:
ARG DOCKER_USERNAME=library
FROM ${DOCKER_USERNAME}/alpine
# 在FROM 之后使用变量,必须在每个阶段分别指定
ARG DOCKER_USERNAME=library
RUN set -x ; echo ${DOCKER_USERNAME}
FROM ${DOCKER_USERNAME}/alpine
# 在FROM 之后使用变量,必须在每个阶段分别指定
ARG DOCKER_USERNAME=library
RUN set -x ; echo ${DOCKER_USERNAME}
注意:对于 Dockerfile 两个 FROM 指令都可以使用 ${DOCKER_USERNAME},对于在各个阶段中使用的变量都必须在每个阶段分别指定。
9. VOLUME 定义匿名卷
容器运行时应该尽量保持存储层不发生写操作,但是数据库类的容器是需要保存动态数据的,一些数据库文件应该保存在卷volume中,定义挂载卷有两种方法,一种是在Dockerfile文件中定义,另一种是在运行容器的时候使用命令docker run -d -v mydata: /data xxxx,将mydata这个命名卷挂载到/data这个目录路径上,如果Dockerfile文件中定义了还会被覆盖掉。在Dockerfile文件中定义卷:
格式为:
VOLUME ["<路径1>", "<路径2>"...]
VOLUME <路径>
如:VOLUME /data ;容器运行时/data就被挂载为匿名卷,任何向/data中写入的信息都不会记录在容器的存储层,从而保证了容器存储层的无状态化。
10. EXPOSE 声明端口
EXPOSE 指令是声明容器运行时提供服务的端口,这只是一个声明,在容器运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口。
要将 EXPOSE 和在运行时使用 -p <宿主端口>:<容器端口> 区分开来。-p,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 EXPOSE 仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。
格式:EXPOSE <端口1> [<端口2>...]
11. WORKDIR 指定工作目录
WORKDIR 指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR 会帮你建立目录。WORKDIR可以是绝对路径或者相对路径,相对路径会与之前的WOKDIR有关。WORKDIR指令是改变环境并影响后面的层。
格式:WORKDIR <工作目录路径>
例如:
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd
RUN pwd 的工作目录为 /a/b/c。
12. USER 指定当前用户
USER指令跟WORKDIR指令相似,都是改变环境状态并影响以后的层;WORKDIR 是改变工作目录,USER 则是改变之后层的执行 RUN, CMD 以及 ENTRYPOINT 这类命令的身份。USER 只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。
格式: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.12/gosu-amd64" \
&& chmod +x /usr/local/bin/gosu \
&& gosu nobody true
# 设置 CMD,并以另外的用户执行
CMD [ "exec", "gosu", "redis", "redis-server" ]
13. HEALTHCHECK 健康检查
HEALTHCHECK 指令是告诉 Docker 应该如何进行判断容器的状态是否正常;当在一个镜像指定了 HEALTHCHECK 指令后,用其启动容器,初始状态会为 starting,在 HEALTHCHECK 指令检查成功后变为 healthy,如果连续一定次数失败,则会变为 unhealthy。HEALTHCHECK 只可以出现一次,如果写了多个,只有最后一个生效。
格式:
HEALTHCHECK [选项] CMD <命令>:设置检查容器健康状况的命令,命令的返回值决定了该次健康检查的成功与否:0:成功;1:失败;2:保留
HEALTHCHECK NONE:如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令
HEALTHCHECK 支持下列[选项]:
- --interval=<间隔>:两次健康检查的间隔,默认为 30 秒;
- --timeout=<时长>:健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认 30 秒;
- --retries=<次数>:当连续失败指定次数后,则将容器状态视为 unhealthy,默认 3 次。
假设我们有个镜像是个最简单的 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 作为健康检查命令。为了帮助排障,健康检查命令的输出(包括 stdout 以及 stderr)都会被存储于健康状态里,可以用 docker inspect 来查看。
14. ONBUILD 帮助别人定制镜像
ONBUILD 是一个特殊的指令,它后面跟的是其它指令,比如 RUN, COPY 等,而这些指令,在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。Dockerfile 中的其它指令都是为了定制当前镜像而准备的,唯有 ONBUILD 是为了帮助别人定制自己而准备的。
格式:ONBUILD <其它指令>
例如:使用ONBUILD写一个基础镜像的Dockerfile(假设这个基础镜像叫my-node):
FROM node:slim
RUN mkdir /app
WORKDIR /app
ONBUILD COPY ./package.json /app
ONBUILD RUN [ "npm", "install" ]
ONBUILD COPY . /app/
CMD [ "npm", "start" ]
这次将项目相关的指令加上 ONBUILD,这样在构建基础镜像的时候,这三行并不会被执行。我们在这个基础镜像my-node上有定制一个镜像就可以直接调用:FROM my-node;是的,只有这么一行。当在各个项目目录中,用这个只有一行的 Dockerfile 构建镜像时,之前基础镜像的那三行 ONBUILD 就会开始执行,成功的将当前项目的代码复制进镜像、并且针对本项目执行 npm install,生成应用镜像。
15. LABEL 添加元数据
LABEL 指令用来给镜像以键值对的形式添加一些元数据(metadata)。
格式:LABEL <key>=<value> <key>=<value> <key>=<value> ...
我们还可以用一些标签来申明镜像的作者、文档地址等:
LABEL org.opencontainers.image.authors="yeasy"
LABEL org.opencontainers.image.documentation="https://yeasy.gitbooks.io"
16. SHELL 指令
SHELL 指令可以指定 RUN ENTRYPOINT CMD 指令的 shell,Linux 中默认为 ["/bin/sh", "-c"]
格式:SHELL ["executable", "parameters"]
SHELL ["/bin/sh", "-c"]
RUN lll ; ls
SHELL ["/bin/sh", "-cex"]
RUN lll ; ls
两个 RUN 运行同一命令,第二个 RUN 运行的命令会打印出每条命令并当遇到错误时退出。
当 ENTRYPOINT CMD 以 shell 格式指定时,SHELL 指令所指定的 shell 也会成为这两个指令的 shell
SHELL ["/bin/sh", "-cex"]
# /bin/sh -cex "nginx"
ENTRYPOINT nginx
SHELL ["/bin/sh", "-cex"]
# /bin/sh -cex "nginx"
CMD nginx
Dockerfie 官方文档:https://docs.docker.com/engine/reference/builder/