目录
Dockerfile 基本结构
Dockerfile 由一行行命令语句组成,并且支持以 # 开头的注释行。Dockerfile 的内容分为四个部分:基础镜像信息、维护者信息、镜像操作指令和容器启动时执行指令。例如:
FROM ubuntu
MAINTAINER wangtingyun wangtingyun@email.com
RUN echo “deb http://archive.ubuntu.com/ubuntu/ raring main universe” >> /etc/apt/sources.list
RUN apt-get update && apt-get install -y nginx
RUN echo “\ndaemon off;” >> /etc/nginx/nginx.conf
CMD /usr/sbin/nginx
其中:
- 一开始必须指明所基于的镜像名称,接下来一般会说明维护者的信息;
- 后面则是镜像操作指令,例如 RUN 指令,RUN 指令将对镜像执行跟随的命令,而每运行一条 RUN 指令,镜像添加新的一层,并提交;
- 最后是 CMD 指令,来指定运行容器时的操作命令。
Dockerfile 指令
FROM
功能: 指定一个镜像作为构建自定义镜像的基础镜像,构建的镜像在这个基础镜像之上进行修改定制。
指令格式:
FROM <image>:<tag>
这个指令是 Dockerfile 中的必备指令,同时也必须是第一条指令,如果在同一个 Dockerfile 中创建多个镜像时,可以使用多个 FROM 指令。
MAINTAINER
功能: 指定维护者信息。
格式为:
MAINTAINER <name>
注意: MAINTAINER 已经被抛弃了,建议使用 LABEL 指令。
LABEL
功能: 为镜像添加元数据标签,一个 LABEL 就是一个键值对。
格式如下:
LABEL <key>=<balue> <key>=<balue> ...
示例如下:
LABEL maintainer="wangtingyun@email.com"
LABEL version="1.0"
可以给镜像添加多个 LABEL,需要注意的是:每条 LABEL 指令都会生成一个新的层。所以最好是把添加的多个 LABEL 合并为一条命令:
LABEL maintainer="wangtingyun@email.com" version="1.0"
LABEL maintainer="wangtingyun@email.com" \
version="1.0"
注意: 如果新添加的 LABEL 和已有的 LABEL 同名,则新值会覆盖掉旧值。
ENV
功能: 设置或定义环境变量,定义的环境变量可以在后续的指令中通过 $ 进行使用。
指令格式:
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>
示例如下:
ENV NODE_VERSION 7.2.0
RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz"
ARG
功能: 设置镜像构建参数。与 ENV 指令作用一致,不过作用域不一样:ARG 设置的环境变量仅对 Dockerfile 内有效,也就是说只有 docker build 的过程中才有效,构建好的镜像内不存在此环境变量。
构建命令 docker build 中可以用 --build-arg <key>=<value> 来覆盖。
指令格式:
ARG <key>=<value>
WORKDIR
功能: 指定工作目录。用 WORKDIR 指定的工作目录,会在构建镜像的每一层中都存在(WORKDIR 指定的工作目录,必须是提前创建好的)。
构建镜像过程中的,每一个 RUN 命令都是新建的一层,只有通过 WORKDIR 创建的目录才会一直存在。
指令格式:
WORKDIR <path>
注意: 可以使用多个 WORKDIR 指令,后续命令如果参数是相对路径,则会基于之前命令指定的路径。例如:
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd
最终路径为:/a/b/c
VOLUME
功能: 定义挂载匿名数据卷目录。在启动容器时如果忘记挂载数据卷,会自动挂载到匿名卷。
挂载卷的作用:
- 避免重要的数据,因容器重启而丢失,这是非常致命的;
- 避免容器不断变大;
指令格式:
VOLUME <path>
VOLUME ["<path1>", "<path2>", ...]
重点: 在启动容器 docker run 的时候,我们可以通过 -v 参数修改挂载点。
EXPOSE
功能: 告诉 Docker 服务,容器需要暴露的端口号。
作用:
- 帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;
- 在使用随机端口映射时,也就是
docker run -P (大写的 P)时,会自动随机映射 EXPOSE 的端口(而 -p 参数指定具体的映射端口);
指令格式:
EXPOSE <port1> [<port2> ...]
COPY
功能: 从上下文目录中复制文件或者目录到容器里指定路径。
指令格式:
COPY [--chown=<user>:<group>] <源路径1>... <目标路径>
COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]
[--chown=<user>:<group>] :可选参数,用于改变复制到容器内文件的拥有者和所属组;<源路径> :源文件或目录(以 Dockerfile 的上下文环境为相对路径),可以使用通配符,但通配符要满足 Go 的 filepath.Match 规则;<目标路径> :容器内的路径,如果路径不存在的话,会自动创建;
示例:
COPY data01.txt /home/data/
COPY data*.txt /home/data/
COPY temp/data/ /home/data/
注意: 目标路径最后一定是以 / 结尾的,否则就是视目标路径为文件,相当于拷贝文件并重命名为目标路径最后的名称。
ADD
功能: 从上下文目录中复制文件或者目录到容器里指定路径,功能和 COPY 指令类似。
指令格式:
ADD <src> <dest>
优缺点:
- 优点: 在执行 <源文件> 为
tar 压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的情况下,会自动复制并解压到 <目标路径>; - 缺点: 在不解压的前提下,无法复制
tar 压缩文件,会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。
所以,具体是否使用 ADD 指令,可以根据是否需要自动解压来决定。
RUN
作用: 为构建的镜像指定要运行的命令行命令,而这些命令是在 docker build 的时候执行的。
指令格式有两种:
1、shell 格式
RUN <command>
2、exec 格式
RUN ["executable", "param1", "param2", ...]
- shell 格式将在终端中运行命令,command 则为终端操作的 shell 命令;
- exec 格式使用
exec 命令执行,可以指定其他终端,例如:RUN ["/bin/bash", "-c", "echo hello"] ;
每条 RUN 指令将在当前镜像的基础上执行指定命令,并提交为新的镜像,而 Dockerfile 的指令每执行一次都会在 docker 上新建一层,过多无意义的层,会造成镜像膨胀过大,所以建议将多个命令合并到同一个 RUN 指令上,例如:
RUN yum -y install wget \
&& wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
&& tar -xvf redis.tar.gz
通过使用 && 符号连接命令,这样执行后,只会创建 1 层镜像。
CMD
作用: 为启动的容器指定要运行的命令,类似于 RUN 指令,但 CMD 运行程序的时间是在 docker run 时执行的,命令运行结束,容器也就结束。
格式有三种:
CMD <shell command>
CMD ["<可执行文件或命令>","<param1>","<param2>",...]
CMD ["<param1>","<param2>",...]
推荐使用第二种格式,执行过程比较明确,而第一种格式实际上在运行的过程中也会自动转换成第二种格式运行,并且默认可执行文件是 sh 。
注意:
- 每个 Dockerfile 只能有一条 CMD 命令,如果指定了多条 CMD 命令,只有最后一条会被执行;
- 如果
docker run 命令行参数中指定了要运行的程序命令,则会覆盖CMD 指令指定的程序命令。
ENTRYPOINT
作用: 和 CMD 指令类似,也是指定容器启动时执行的命令,但和 CMD 指令不同的是:
- 即使
docker run 的命令行参数指定了要运行的程序命令,ENTRYPOINT 指令的程序命令也不会被覆盖,并且这些命令行参数会被当作参数传送给 ENTRYPOINT 指令指定的程序; - 但是,如果运行
docker run 时使用了 --entrypoint 选项,将覆盖 ENTRYPOINT 指令指定的程序。
指令格式:
ENTRYPOINT ["<executeable>", "<param1>", "<param2>", ...]
注意: 每个 Dockerfile 中只能有一个 ENTRYPOINT,当指定多个 ENTRYPOINT 时,只有最后一个生效。
使用方式:
ENTRYPOINT 指令一般搭配 CMD 指令一起使用:通过 CMD 的第三种指令格式给 ENTRYPOINT 动态传参。
示例: 构建 nginx:test 镜像
FROM nginx
ENTRYPOINT ["nginx", "-c"]
CMD ["/etc/nginx/nginx.conf"]
1、当 docker run 不传参运行时:
docker run nginx:test
容器内会默认运行以下命令(命令参数来自 CMD 指定提供的参数),启动主进程:
nginx -c /etc/nginx/nginx.conf
2、当传参运行时:
docker run nginx:text -c /etc/nginx/new.conf
容器会运行以下命令(命令行参数覆盖 CMD 指令提供的参数),启动主进程:
nginx -c /etc/nginx/new.conf
ONBUILD
功能: 配置当所创建的镜像作为其他新创建镜像的基础镜像时,所执行的操作指令。
指令格式:
ONBUILD [INSTRUCTION]
示例:
使用如下 Dockerfile 内容创建镜像 myImage
...
ONBUILD ADD . /app/src
ONBUILD RUN /usr/local/bin/python-build –dir /app/src
...
当新的 Dockerfile 中基于镜像 myImage (即使用 FROM myImage )时,会自动执行 ONBUILD 指令的内容,等价于在后面添加了两条指令:
FROM myImage
ADD . /app/src
RUN /usr/local/bin/python-build –dir /app/src
HEALTHCHECK
功能: 指定某个程序或者指令来监控 docker 容器服务的运行状态。
指令格式:
HEALTHCHECK [选项] CMD <命令>
HEALTHCHECK NONE
HEALTHCHECK [选项] CMD <命令>
Dockerfile 编写建议
1、编写 .dockerignore 文件
.dockerignore 的作用和语法类似于 .gitignore ,可以忽略一些不需要的文件,这样可以有效加快镜像构建时间,同时减少 Docker 镜像的大小。
2、容器只运行单个应用
如果在 Docker 容器中运行多个进程会有比较多的麻烦:
- 非常长的构建时间;
- 非常大的镜像文件;
- 多个应用的日志难以处理(不能直接使用stdout,否则多个应用的日志会混合到一起);
- 横向扩展时非常浪费资源(不同的应用需要运行的容器数并不相同);
- 僵尸进程问题(需要选择合适的init进程);
所以,建议每个应用构建单独的 Docker 镜像,然后使用 Docker Compose 运行多个 Docker 容器。
3、将多个 RUN 指令合并为一个
由于 Docker 镜像是分层的:
- Dockerfile 中的每个指令都会创建一个新的镜像层;
- 镜像层会被缓存和复用,当 Dockerfile 的指令修改了,复制的文件变化了,或者构建镜像时指定的变量不同了,对应的镜像层缓存就会失效,而某一层的镜像缓存失效之后,它之后的镜像层缓存都会失效;
- 镜像层是不可变的,如果我们再某一层中添加一个文件,然后在下一层中删除它,则镜像中依然会包含该文件(只是这个文件在 Docker 容器中不可见了);
所以,RUN 指令的多个命令可以通过换行符 \ 和 && 来合并成一行指令,例如下面的 node.js 和 npm 模块的安装命令:
FROM ubuntu
ADD . /app
RUN apt-get update \
&& apt-get install -y nodejs \
&& cd /app \
&& npm install
CMD npm start
注意: 们只能将变化频率一样的指令合并在一起,将 node.js 安装与 npm 模块安装放在一起的话,则每次修改源代码,都需要重新安装 node.js ,这显然不合适。因此,正确的写法是这样的:
FROM ubuntu
RUN apt-get update && apt-get install -y nodejs
ADD . /app
RUN cd /app && npm install
CMD npm start
4、基础镜像的标签不要用 latest
当镜像没有指定标签时,将默认使用 latest 标签。因此, FROM ubuntu 指令等同于 FROM ubuntu:latest 。
但是,当镜像更新时,latest 标签会指向不同的镜像,这时构建镜像有可能失败。如果你的确需要使用最新版的基础镜像,可以使用 latest 标签,否则的话,最好指定确定的镜像标签。
5、每个 RUN 指令后删除多余文件
假设我们更新了 apt-get 源、下载、解压并安装了一些软件包,它们都保存在 /var/lib/apt/lists/ 目录中。但是,运行应用时,Docker 镜像中并不需要这些文件。我们最好将它们删除,因为它会使 Docker 镜像变大:
RUN apt-get update \
&& apt-get install -y nodejs \
&& rm -rf /var/lib/apt/lists/*
6、选择合适的基础镜像 (alpine 版本最好)
比如,我们只是构建运行 node 程序,没有必要选择 ubuntu 这种通用的基础镜像:
FROM ubuntu
选择 node 镜像作为基础镜像更好,并且选择 alpine 版本的更好:
FROM node:7-alpine
alpine 是一个极小化的 Linux 发行版,非常适合作为基础镜像;apk 是 Alpine 的包管理工具,它与 apt-get 有些不同,但是非常容易上手。另外,它还有一些非常有用的特性,比如 no-cache 和 --virtual 选项,它们都可以帮助我们减少镜像的大小;
7、设置 WORKDIR
WORKDIR 指令可以设置默认目录,也就是运行 RUN / CMD / ENTRYPOINT 指令的地方。
8、使用 ENTRYPOINT
ENTRYPOINT 指令并不是必须的,因为它会增加复杂度。ENTRYPOINT 是一个脚本,它会默认执行,并且将指定的命令作为其参数,它通常用于构建可执行的 Docker 镜像。
在 Dockerfile 中指定 entrypoint.sh 脚本:
ENTRYPOINT ["./entrypoint.sh"]
然后在 docker run 中执行该脚本:
docker run -it demo /bin/bash
注意: entrypoint 脚本,应该使用 exec 执行命令,否则无法顺利地关闭容器,因为 SIGTERM 信号会被 bash 脚本进程吞没。exec 命令启动的进程可以取代脚本进程,因此所有的信号都会正常工作。
9、优先使用 COPY 而不是 ADD
COPY 指令非常简单,仅用于将文件拷贝到镜像中,而 ADD 相对来讲复杂一些,可以用于下载远程文件以及解压压缩包。所以除非要解压缩包,否则建议使用 COPY。
10、合理调整 COPY 与 RUN 的顺序
我们应该把变化最少的部分放在 Dockerfile 的前面,这样可以充分利用镜像缓存。
11、设置默认的环境变量、映射端口和数据卷
运行 Docker 容器时很可能需要一些环境变量。在 Dockerfile 设置默认的环境变量、端口映射和数据卷是一种很好的方式:
ENV MEDIA_DIR=/media \
APP_PORT=3000
VOLUME $MEDIA_DIR
EXPOSE $APP_PORT
ENV 指令指定的环境变量在容器中可以使用,如果只是需要指定构建镜像时的变量,可以使用 ARG 指令。
12、 使用 LABEL 设置镜像元数据
使用 LABEL 指令,可以为镜像设置元数据,例如镜像创建者或者镜像说明。旧版的 Dockerfile 语法使用 MAINTAINER 指令指定镜像创建者,但是它已经被弃用了:
FROM node:7-alpine
LABEL maintainer "wang@email.com"
...
13、添加HEALTHCHECK
运行容器时,可以指定 --restart always 选项。这样的话,容器崩溃时,Docker 守护进程会重启容器,对于需要长时间运行的容器,这个选项非常有用。
但是,如果容器的确在运行,但是不可用(陷入死循环,配置错误)怎么办?使用 HEALTHCHECK 指令可以让 Docker 周期性的检查容器的健康状况。
我们只需要指定一个命令,如果一切正常的话返回 0,否则返回 1:
HEALTHCHECK CMD curl --fail http://localhost:3000 || exit 1
完整示例如下(以构建 Node.js 应用为例):
FROM node:7-alpine
LABEL maintainer "jakub.skalecki@example.com"
ENV PROJECT_DIR=/app
WORKDIR $PROJECT_DIR
COPY package.json $PROJECT_DIR
RUN npm install
COPY . $PROJECT_DIR
ENV MEDIA_DIR=/media \
NODE_ENV=production \
APP_PORT=3000
VOLUME $MEDIA_DIR
EXPOSE $APP_PORT
HEALTHCHECK CMD curl --fail http://localhost:$APP_PORT || exit 1
ENTRYPOINT ["./entrypoint.sh"]
CMD ["start"]
|